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

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

EnvironmentAPI Base URLOAuth Base URL
SANDBOXhttps://sandbox.api.flute.comhttps://sandbox.oauth.api.flute.com
PRODUCTIONhttps://api.flute.comhttps://oauth.api.flute.com

On PRODUCTION, every payment and refund is real: cards are charged and money moves. Use SANDBOX for integration and testing.

Credentials are scoped to one environment. A clientId and clientSecret issued for SANDBOX are rejected on PRODUCTION and vice versa, surfacing as a 401 during initialization.

Initialization

Initialize

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

ParameterTypeRequiredDescription
environmentEnvironmentYesTarget environment (SANDBOX or PRODUCTION).
clientIdString?First run onlyPersisted encrypted after the first run; omit on subsequent launches.
clientSecretString?First run onlyAs above.
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

FluteTerminal.provisionCredentials(clientId, clientSecret, callback)

Supplies or rotates credentials at runtime without a rebuild. The SDK stores them encrypted per environment, invalidates the existing token, and re-warms.

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

Check Credentials

FluteTerminal.hasCredentials()

Returns true if this device holds credentials for the selected environment.

val ready = FluteTerminal.hasCredentials()

Shut Down

FluteTerminal.shutdown()

Releases the SDK and stops the token-refresh loop. 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

  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. It is also used for process-death recovery (see Recovery After Process Death).

Register for Payment Result

FluteTerminal.registerForPaymentResult(owner, callback)

Registers the result callback and returns the launcher used to start payments. 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

launcher.startPayment(PaymentRequest)

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

PaymentRequest parameters

ParameterTypeRequiredDescription
baseAmountBigDecimalYesAmount before ZCP or tip math. Two decimal places maximum.
pricingTypePricingType?ConditionalCARD or CASH. Required for Dual Pricing merchants to indicate which price baseAmount represents. Must be omitted otherwise.
captureMethodCaptureMethodNoAUTO (sale, default) or MANUAL (authorization to capture later).
readingMethodReadingMethod?NoKEYED_ENTRY opens manual card entry on the terminal. Omit to use tap, insert, or swipe.
tipAmountBigDecimal?NoPreset tip amount. Omit to let the terminal prompt the customer.
tipRatePercentBigDecimal?NoPreset tip as a percentage.
referenceIdString?NoYour order reference, echoed on the transaction record.
posDeviceIdString?NoIdentifies the till within your estate.
paymentProcessorIdString?NoOverrides the merchant's default processor.
customerIdString?NoAssociates the payment with a stored customer.
requestPaymentMethodStorageConsentBooleanNoWhen true, asks the cardholder to consent to saving their card. Defaults to false.

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

launcher.startRefund(RefundRequest)

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

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

FluteTerminal.cancelPayment(posTransactionId, callback)

Cancels the payment currently in flight, before the cardholder completes it.

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

PaymentResult is a sealed class with three cases.

Approved Case

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

PaymentResult.Declined

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

A decline is a completed transaction with a negative outcome, not an error. AVS and CVV rejections arrive here, with the reason in message.

Error Case

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

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

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

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

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

  • 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

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.