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 runs | On the payment terminal itself | Anywhere: back office, web POS, another till |
| How the terminal is reached | Deeplink to the Flute Terminal app on-device | Cloud connection to the terminal |
| Terminal must be Online/Ready at create time | No | Yes |
| How you get the outcome | Callback, delivered exactly once | Poll GET /v2/pos/transactions/{id} |
| Integration effort | One method and one callback | Create, poll, interpret status |
| Requirement | Detail |
|---|---|
| Hardware | A Flute-provisioned Android payment terminal (for example, Sunmi P2 Pro) |
| Flute Terminal app | Installed and activated on the same device, in Semi-Integrated mode |
| Android | minSdk 25 or higher; the SDK targets compileSdk 34 |
| Credentials | Merchant-scoped clientId and clientSecret (managed via the Flute dashboard or API) |
| Language | Kotlin or Java (the public API is Java-interoperable) |
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.
| Environment | API Base URL | OAuth Base URL |
|---|---|---|
SANDBOX | https://sandbox.api.flute.com | https://sandbox.oauth.api.flute.com |
PRODUCTION | https://api.flute.com | https://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.
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
| Parameter | Type | Required | Description |
|---|---|---|---|
environment | Environment | Yes | Target environment (SANDBOX or PRODUCTION). |
clientId | String? | First run only | Persisted encrypted after the first run; omit on subsequent launches. |
clientSecret | String? | First run only | As above. |
serialNumber | String? | No | Overrides the auto-detected device serial. Required only on an emulator or when the Flute Terminal app cannot publish the serial. |
enableHttpLogging | Boolean | No | Logs HTTP requests and responses. Disable in production. |
logger | FluteLogger? | No | Receives redacted SDK diagnostics. |
terminalResultTimeoutSeconds | Long | No | Watchdog timeout for a terminal that never returns a result. |
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
}FluteTerminal.hasCredentials()
Returns true if this device holds credentials for the selected environment.
val ready = FluteTerminal.hasCredentials()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.
- Your app calls
startPayment()with an amount. - The SDK resolves the terminal and currency, then requests the amounts to display (surcharge, cash discount, dual pricing, and tip).
- The SDK creates the POS transaction through the Flute API.
- The SDK launches the Flute Terminal app, which collects the card and processes the payment.
- The Flute Terminal app returns control to your app.
- 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.
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).
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.
launcher.startPayment(PaymentRequest)
launcher.startPayment(
PaymentRequest.Builder(BigDecimal("25.00"))
.posDeviceId("TILL-1")
.referenceId(orderId)
.build(),
)PaymentRequest parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
baseAmount | BigDecimal | Yes | Amount before ZCP or tip math. Two decimal places maximum. |
pricingType | PricingType? | Conditional | CARD or CASH. Required for Dual Pricing merchants to indicate which price baseAmount represents. Must be omitted otherwise. |
captureMethod | CaptureMethod | No | AUTO (sale, default) or MANUAL (authorization to capture later). |
readingMethod | ReadingMethod? | No | KEYED_ENTRY opens manual card entry on the terminal. Omit to use tap, insert, or swipe. |
tipAmount | BigDecimal? | No | Preset tip amount. Omit to let the terminal prompt the customer. |
tipRatePercent | BigDecimal? | No | Preset tip as a percentage. |
referenceId | String? | No | Your order reference, echoed on the transaction record. |
posDeviceId | String? | No | Identifies the till within your estate. |
paymentProcessorId | String? | No | Overrides the merchant's default processor. |
customerId | String? | No | Associates the payment with a stored customer. |
requestPaymentMethodStorageConsent | Boolean | No | When 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.
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.
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.
PaymentResult is a sealed class with three cases.
PaymentResult.Approved
| Property | Type | Description |
|---|---|---|
posTransactionId | String | The POS transaction record identifier. |
transactionId | String? | The gateway transaction identifier. |
authCode | String? | Processor authorization code. |
responseCode | String? | Processor response code. |
processedAmount | BigDecimal? | Total actually charged. Reconcile against this, not the requested base amount. |
amounts | AmountBreakdown? | Breakdown of base, tip, surcharge, discount, and their rates. |
card | CardInfo? | Masked PAN, brand, type, entry method, and CVM. |
processor | ProcessorReferences? | Auth code, RRN, MID, and TID for receipt and dispute reference. |
avs | AvsResult? | Address verification outcome. |
availableRefundAmount | BigDecimal? | Refundable remainder. |
transactionType | String? | "Sale", "Authorization", or "Refund". |
transactionDateTime | String? | Gateway timestamp. |
receiptData | String? | Opaque receipt payload to render or print. |
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.
PaymentResult.Error
| Property | Type | Description |
|---|---|---|
reason | ErrorReason | Machine-readable cause (see table below). |
message | String? | Human-readable detail. |
posTransactionId | String? | Present when the transaction record was created before the failure. |
correlationId | String? | Flute trace identifier. Include this when contacting support. |
ErrorReason values
| Value | Meaning | Suggested handling |
|---|---|---|
USER_CANCELLED | Cancelled on the terminal or by your app. | Return to the order. |
TIMEOUT | No result within the configured timeout. | The transaction may still complete. Reconcile with checkPendingPayment(). |
ALREADY_IN_PROGRESS | Another payment is running. | Wait for the current payment result. |
TRANSACTION_CREATION_FAILED | The transaction was never created. | Show the message; safe to retry. |
AUTHENTICATION_FAILED | Credentials rejected. | Re-provision credentials. |
APP_NOT_INSTALLED | Flute Terminal app is missing or cannot handle the deeplink. | Install and activate the terminal app in Semi-Integrated mode. |
UNAUTHORIZED_CALLER | This app is not permitted to start payments. | Contact Flute. |
TERMINAL_FAILED | The terminal flow failed. | Retry. |
MALFORMED_RESPONSE | Unexpected response shape. | Retry and report with the correlationId. |
NOT_INITIALIZED | initialize() was not called, or the SDK was shut down. | Call initialize() first. |
UNKNOWN | Unclassified 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)
}
}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
| Property | Type | Description |
|---|---|---|
hasPending | Boolean | Whether a payment was left in flight. |
stillInProgress | Boolean | Whether the terminal has not yet produced an outcome. |
result | PaymentResult? | The resolved outcome, when available. |
Recovery methods
| Method | Description |
|---|---|
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. |
All methods are available as @JvmStatic on FluteTerminal and deliver results via FluteCallback on the main thread.
| Method | Description |
|---|---|
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. |
| Method | Returns | Description |
|---|---|---|
fetchTerminals(callback) | List<TerminalInfo> | Terminals on the merchant account, with online status. |
fetchPaymentConfig(callback) | PaymentConfig | Currency, 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.
- 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 aninitialize()with changed configuration, re-register to obtain a new launcher.
| Symptom | Cause | Resolution |
|---|---|---|
401 during initialization | Credentials belong to a different environment. | Provision keys for the correct environment. |
Transaction is already in progress | A previous POS transaction is unresolved on the terminal. | Cancel it or wait for the SDK to release it. |
Merchant uses Dual Pricing; pricingType is required | pricingType omitted on a Dual Pricing merchant. | Set CARD or CASH on every payment request. |
APP_NOT_INSTALLED | Flute Terminal app is missing or not in Semi-Integrated mode. | Install and activate the terminal app. |
| Result never arrives | Launcher registered after STARTED, or against a shut-down SDK. | Register in onCreate() and re-register after re-initializing. |