Skip to content

Flute Terminal SDK for Android

Overview

The Flute Terminal SDK for Android is a native integration layer for ISV point-of-sale applications running on Flute-provisioned Android terminals. It handles authentication, token management, payment configuration, and the full transaction lifecycle, letting your application focus on business logic rather than payment infrastructure.

The SDK's primary feature is Deeplink Integration, which lets your app initiate a card-present transaction and hand off to the Flute Terminal app on the same device for card collection. Additional features may be added to the SDK over time and documented here as new sections.

The Flute Terminal SDK is the on-device counterpart to the POS Terminal Integration REST flow:

Flute Terminal SDK (this guide)POS Terminal Integration (REST)
Where your app runsOn the payment terminal itselfAnywhere: back office, web POS, another till
How the terminal is reachedDeeplink to the Flute Terminal app on-deviceCloud connection to the terminal
Terminal must be Online/Ready at create timeNoYes
How you get the outcomeCallback, delivered exactly oncePoll GET /v2/pos/transactions/{id}
Integration effortOne method and one callbackCreate, poll, interpret status

Prerequisites

Confirm the following hardware, software, and credentials are in place before you integrate the SDK.

RequirementDetail
HardwareA Flute-provisioned Android payment terminal (for example, Sunmi P2 Pro)
Flute Terminal appInstalled and activated on the same device, in Semi-Integrated mode
AndroidminSdk 25 or higher; the SDK targets compileSdk 34
CredentialsMerchant-scoped clientId and clientSecret (managed via the Flute dashboard or API)
LanguageKotlin or Java (the public API is Java-interoperable)

Installation

The Flute Terminal SDK is available on Flute's official GitHub. Download the SDK package from the releases page, unzip it next to your project, and register it in settings.gradle.kts:

dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
        maven { url = uri("../flute-terminal-sdk") }
    }
}

Then add the dependency to your app module:

dependencies {
    implementation("com.flute.terminal:sdk:1.0.0")
}

Third-party dependencies (Retrofit, OkHttp, Coroutines) resolve from Maven Central automatically.

The SDK includes its own consumer ProGuard rules. No additional keep rules are required in your app, and R8 minification can remain enabled on release builds.

Environments

The following environments are available:

ProfileAPI Base URLOAuth Base URL
Sandboxhttps://sandbox.api.flute.comhttps://sandbox.oauth.api.flute.com
Productionhttps://api.flute.comhttps://oauth.api.flute.com

In the production environment, every payment and refund is real. Cards are charged and funds are transferred. The sandbox environment is for integration and testing.

Credentials are scoped to one environment:

  • A clientId and clientSecret issued for the sandbox environment are rejected in the production environment.
  • A clientId and clientSecret issued for the production environment are rejected in the sandbox environment.

Either case returns a 401 result during initialization.

Initialization

This section covers starting the SDK, supplying credentials at runtime, checking their status, and releasing the SDK when it's no longer needed.

Initialize

Initializes the SDK for the selected environment and credentials.

FluteTerminal.initialize(context, config, onReady)

Call once, typically from Application.onCreate().

FluteTerminal.initialize(
    applicationContext,
    FluteTerminalConfig(
        environment = FluteTerminalConfig.Environment.SANDBOX,
        clientId = BuildConfig.FLUTE_CLIENT_ID,
        clientSecret = BuildConfig.FLUTE_CLIENT_SECRET,
    ),
) { result ->
    when (result) {
        is FluteResult.Success -> Log.i("Flute", "SDK ready")
        is FluteResult.Failure -> Log.e("Flute", "Initialization failed: ${result.error.message}")
    }
}

Initialization runs in the background: it fetches and caches an OAuth token, loads the merchant's payment configuration, identifies the terminal for this device, and keeps the session current automatically. The callback is optional; on-demand calls still work if warm-up is skipped or fails.

Never hardcode credentials in source. Inject them through local.properties and BuildConfig, or collect them once in an onboarding screen and let the SDK persist them.

FluteTerminalConfig parameters

This lists each parameter accepted by FluteTerminalConfig.

ParameterTypeRequiredDescription
environmentEnvironmentYesTarget environment (sandbox or production).
clientIdString?First run onlyPersisted encrypted after the first run; omit on subsequent launches.
clientSecretString?First run onlyPersisted encrypted after the first run; omit on subsequent launches.
serialNumberString?NoOverrides the auto-detected device serial. Required only on an emulator or when the Flute Terminal app cannot publish the serial.
enableHttpLoggingBooleanNoLogs HTTP requests and responses. Disable in production.
loggerFluteLogger?NoReceives redacted SDK diagnostics.
terminalResultTimeoutSecondsLongNoWatchdog timeout for a terminal that never returns a result.

Provision Credentials

When credentials aren't included in at build time, or they need to change without a rebuild, the app can supply them at runtime instead.

This supplies or rotates credentials at runtime without a rebuild.

FluteTerminal.provisionCredentials(clientId, clientSecret, callback)

The SDK stores them encrypted for each environment, invalidates the existing token, and the re-warms them.

FluteTerminal.provisionCredentials(clientId, clientSecret) { result ->
    // FluteResult.Failure with httpStatus 401 means wrong keys or keys for a different environment
}

Check Credentials

Before starting a payment, the app can check whether valid credentials are already stored for the current environment.

FluteTerminal.hasCredentials()

val ready = FluteTerminal.hasCredentials()

This returns true if this device holds valid credentials for the selected environment.

Shut Down

When your app no longer needs the SDK, such as on sign-out or when switching merchant accounts, it should be shut down to free its resources.

This releases the SDK and stops the token-refresh loop.

FluteTerminal.shutdown()

After calling shutdown(), re-register the payment result launcher before taking new payments.

FluteTerminal.shutdown()

Deeplink Integration is the SDK's mechanism for initiating a card-present transaction directly from your POS application. The SDK launches the Flute Terminal app on the same device to handle card collection, then returns a typed result to your callback once the payment is resolved.

How It Works

The following steps trace what happens between calling startPayment() and receiving a result:

  1. Your app calls startPayment() with an amount.
  2. The SDK resolves the terminal and currency, then requests the amounts to display (surcharge, cash discount, dual pricing, and tip).
  3. The SDK creates the POS transaction through the Flute API.
  4. The SDK launches the Flute Terminal app, which collects the card and processes the payment.
  5. The Flute Terminal app returns control to your app.
  6. The SDK fetches the authoritative transaction outcome from the API and delivers it to your callback.

The result returned to your callback reflects the transaction record as the gateway sees it, not just what the terminal app reported.

Flute Terminal AppFlute APIFlute Terminal SDKPOS ApplicationFlute Terminal AppFlute APIFlute Terminal SDKPOS ApplicationregisterForPaymentResult(callback)startPayment(PaymentRequest)Create POS transactionPOS Transaction IDLaunch Flute Terminal appCollect card and processsetResult() (ends deeplink session)Fetch authoritative outcomeTransaction recordPaymentResult callback
Flute Terminal AppFlute APIFlute Terminal SDKPOS ApplicationFlute Terminal AppFlute APIFlute Terminal SDKPOS ApplicationregisterForPaymentResult(callback)startPayment(PaymentRequest)Create POS transactionPOS Transaction IDLaunch Flute Terminal appCollect card and processsetResult() (ends deeplink session)Fetch authoritative outcomeTransaction recordPaymentResult callback

parseResult(activityResult) is available as an alternative for apps that handle the ActivityResult directly, bypassing the callback. The callback path is the recommended approach. parseResult(activityResult) is also used for process-death recovery. See Recovery After Process Death.

Register for Payment Result

Before a payment can be started, the app must register a callback to receive its result.

This registers the result callback and returns the launcher used to start payments.

FluteTerminal.registerForPaymentResult(owner, callback)

It must be called before the Activity reaches STARTED, making onCreate() the only safe place.

class CheckoutActivity : ComponentActivity() {
    private lateinit var launcher: FluteTerminalLauncher

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        launcher = FluteTerminal.registerForPaymentResult(this) { result ->
            handleResult(result)
        }
    }
}

The callback fires exactly once per payment, on the main thread, for every outcome: approval, decline, cancellation, timeout, or a failure before the terminal was launched.

Registering after STARTED throws RegistrationLifecycleException.

Start a Payment

With credentials provisioned and a result callback registered, the app is ready to initiate a card-present payment.

This starts a card-present payment for the given amount and hands off to the Flute Terminal app to collect the card.

launcher.startPayment(PaymentRequest)

launcher.startPayment(
    PaymentRequest.Builder(BigDecimal("25.00"))
        .posDeviceId("TILL-1")
        .referenceId(orderId)
        .build(),
)

PaymentRequest parameters

This lists each parameter accepted by PaymentRequest.

ParameterTypeRequiredDescription
baseAmountBigDecimalYesSpecifies the amount before ZCP (zero cost processing) or tip adjustments. Two decimal places maximum.
pricingTypePricingType?ConditionalSpecifies CARD or CASH. Required for Dual Pricing merchants to indicate which price baseAmount represents. Must be omitted otherwise.
captureMethodCaptureMethodNoSpecifies AUTO (sale, default) or MANUAL (authorization to capture later).
readingMethodReadingMethod?NoSpecifies the input method for the terminal.
KEYED_ENTRY opens manual card entry on the terminal.
Omit to use tap, insert, or swipe.
tipAmountBigDecimal?NoSpecifies a preset tip amount. Omit to let the terminal prompt the customer.
tipRatePercentBigDecimal?NoSpecifies a preset tip as a percentage.
referenceIdString?NoSpecifies uour order reference, echoed on the transaction record.
posDeviceIdString?NoSpecifies the till within your estate.
paymentProcessorIdString?NoSpecifies a payment processor. This override the merchant's default processor.
customerIdString?NoSpecifies the associated payment with a stored customer.
requestPaymentMethodStorageConsentBooleanNoSpecifies asking the cardholder to consent to saving their card.
If true, asks the cardholder to consent to saving their card.
If false, the cardholder is not asked to consent to saving their card.
Defaults to false.

The values for terminalId and currencyCode are resolved by the SDK from the device and merchant configuration. You do not supply them.

Only one payment may be in flight at a time. A second call while a payment is running is rejected immediately with ErrorReason.ALREADY_IN_PROGRESS.

When using captureMethod: MANUAL, the transaction is authorized but not captured. Call capture() (see Post-Payment Operations) to settle the funds.

Start a Refund

When a merchant needs to return funds to a customer without an original transaction to reference, the app can initiate a refund directly.

This initiates an unreferenced refund: funds are returned to whatever card the customer presents, with no originating transaction required.

launcher.startRefund(RefundRequest)

launcher.startRefund(
    RefundRequest(
        refundAmount = BigDecimal("25.00"),
        posDeviceId = "TILL-1",
    ),
)

The outcome arrives on the same payment callback as PaymentResult.Approved, with transactionType identifying it as a refund and a negative processedAmount.

An unreferenced refund credits funds to whatever card the customer presents, with no link to a prior sale. Confirm the amount with the operator before calling this method.

Cancel a Payment

If a payment needs to be stopped before the cardholder completes it, the app can request its cancellation.

This cancels the payment currently in flight, before the cardholder completes it.

FluteTerminal.cancelPayment(posTransactionId, callback)

FluteTerminal.pendingPosTransactionId()?.let { id ->
    FluteTerminal.cancelPayment(id) { /* accepted or rejected */ }
}

The return value indicates only whether the cancellation request was accepted by the SDK. The backend rejects it once the transaction has already reached the processor. The final outcome still arrives on the payment callback.

Handling the Result

Once the payment completes, the callback delivers a PaymentResult describing how it was resolved.

PaymentResult is a sealed class with three cases: Approved Case, Declined Case, and Error Case.

Approved Case

An approved payment returns the following properties describing the completed transaction.

PaymentResult.Approved

PropertyTypeDescription
posTransactionIdStringThe POS transaction record identifier.
transactionIdString?The gateway transaction identifier.
authCodeString?Processor authorization code.
responseCodeString?Processor response code.
processedAmountBigDecimal?Total actually charged. Reconcile against this, not the requested base amount.
amountsAmountBreakdown?Breakdown of base, tip, surcharge, discount, and their rates.
cardCardInfo?Masked PAN, brand, type, entry method, and CVM.
processorProcessorReferences?Auth code, RRN, MID, and TID for receipt and dispute reference.
avsAvsResult?Address verification outcome.
availableRefundAmountBigDecimal?Refundable remainder.
transactionTypeString?"Sale", "Authorization", or "Refund".
transactionDateTimeString?Gateway timestamp.
receiptDataString?Opaque receipt payload to render or print.

Declined Case

When the issuer or processor rejects the payment, the callback delivers a PaymentResult.Declined instead of an error.

A decline is a completed transaction with a negative outcome, not an error.

PaymentResult.Declined

Carries posTransactionId, transactionId, responseCode, message, plus processedAmount, amounts, card, avs, gatewayReferenceId, and transactionDateTime where available.

AVS and CVV rejections arrive here, with the reason in message.

Error Case

An error indicates the payment did not reach a definitive outcome and carries the following properties to help you diagnose and recover.

PaymentResult.Error

PropertyTypeDescription
reasonErrorReasonMachine-readable cause (see table below).
messageString?Human-readable detail.
posTransactionIdString?Present when the transaction record was created before the failure.
correlationIdString?Flute trace identifier. Include this when contacting support.

ErrorReason values

This lists each ErrorReason value and how to handle it.

ValueMeaningSuggested handling
USER_CANCELLEDCancelled on the terminal or by your app.Return to the order.
TIMEOUTNo result within the configured timeout.The transaction may still complete. Reconcile with checkPendingPayment().
ALREADY_IN_PROGRESSAnother payment is running.Wait for the current payment result.
TRANSACTION_CREATION_FAILEDThe transaction was never created.Show the message; safe to retry.
AUTHENTICATION_FAILEDCredentials rejected.Re-provision credentials.
APP_NOT_INSTALLEDFlute Terminal app is missing or cannot handle the deeplink.Install and activate the terminal app in Semi-Integrated mode.
UNAUTHORIZED_CALLERThis app is not permitted to start payments.Contact Flute.
TERMINAL_FAILEDThe terminal flow failed.Retry.
MALFORMED_RESPONSEUnexpected response shape.Retry and report with the correlationId.
NOT_INITIALIZEDinitialize() was not called, or the SDK was shut down.Call initialize() first.
UNKNOWNUnclassified error.Show the message and report with the correlationId.

Example result handler:

private fun handleResult(result: PaymentResult) = when (result) {
    is PaymentResult.Approved -> completeOrder(result.transactionId, result.processedAmount)
    is PaymentResult.Declined -> showDeclined(result.message)
    is PaymentResult.Error -> when (result.reason) {
        ErrorReason.USER_CANCELLED -> returnToOrder()
        ErrorReason.TIMEOUT -> reconcileLater(result.posTransactionId)
        else -> showError(result.message, result.correlationId)
    }
}

Recovery After Process Death

If your app is killed while the terminal is collecting the card, the in-flight transaction ID is persisted. Call checkPendingPayment() at startup and resolve the outstanding result before initiating new payments:

FluteTerminal.checkPendingPayment { check ->
    check.onSuccess { pending ->
        when {
            !pending.hasPending -> Unit                        // nothing was interrupted
            pending.stillInProgress -> waitForTerminal()       // terminal has not finished yet
            else -> pending.result?.let { handleResult(it) }  // resolved outcome
        }
    }
}

PendingPaymentCheck properties

This describes each property returned by PendingPaymentCheck.

PropertyTypeDescription
hasPendingBooleanWhether a payment was left in flight.
stillInProgressBooleanWhether the terminal has not yet produced an outcome.
resultPaymentResult?The resolved outcome, when available.

Recovery methods

This lists the methods available for recovering from a process death.

MethodDescription
checkPendingPayment(callback)Resolves a payment left in flight, returning its outcome or indicating it is still running.
pendingPosTransactionId()Returns the persisted in-flight transaction ID, if any.
parseResult(activityResult)Parses a terminal ActivityResult directly. This is an escape hatch; the callback is the recommended integration path.

Post-Payment Operations

All methods are available as @JvmStatic on FluteTerminal and deliver results via FluteCallback on the main thread.

MethodDescription
capture(transactionId, amount?, callback)Captures an authorization, fully or partially.
adjustTip(transactionId, tipAmount?, tipRate?, callback)Adjusts the tip on a captured transaction.
reverseTransaction(transactionId, amount?, callback)Voids or refunds an existing transaction, fully or partially.
getTransaction(transactionId, callback)Fetches the current transaction record.
printReceipt(posTransactionId, callback)Reprints the receipt on the terminal.
shareReceipt(transactionId, method, recipient, hasCustomerConsent, callback)Sends the receipt by SMS. Pass hasCustomerConsent = true only when the cardholder has agreed to be contacted.

Device and Merchant Information

These methods return read-only information about the merchant's terminals, payment configuration, and this device.

MethodReturnsDescription
fetchTerminals(callback)List<TerminalInfo>Terminals on the merchant account, with online status.
fetchPaymentConfig(callback)PaymentConfigCurrency, zero-cost processing option, and processors. Live fetch.
deviceSerialNumber()String?Serial number the SDK resolved for this device.

Check PaymentConfig.requiresPricingType to determine whether pricingType must be set on every payment. It is true for Dual Pricing merchants.

Threading and Lifecycle

Keep the following threading and lifecycle behaviors in mind when integrating the SDK.

  • Every callback is delivered on the main thread, so you can update the UI directly.
  • The payment flow runs on a process-wide scope and survives Activity recreation while the terminal app is in the foreground.
  • After shutdown() or an initialize() with changed configuration, re-register to obtain a new launcher.

Troubleshooting

The following table lists common integration issues, their causes, and how to resolve them.

SymptomCauseResolution
401 during initializationCredentials belong to a different environment.Provision keys for the correct environment.
Transaction is already in progressA previous POS transaction is unresolved on the terminal.Cancel it or wait for the SDK to release it.
Merchant uses Dual Pricing; pricingType is requiredpricingType omitted on a Dual Pricing merchant.Set CARD or CASH on every payment request.
APP_NOT_INSTALLEDFlute Terminal app is missing or not in Semi-Integrated mode.Install and activate the terminal app.
Result never arrivesLauncher registered after STARTED, or against a shut-down SDK.Register in onCreate() and re-register after re-initializing.