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 |
Confirm the following hardware, software, and credentials are in place before you integrate the SDK.
| 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.
The following environments are available:
| Profile | 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 |
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
clientIdandclientSecretissued for the sandbox environment are rejected in the production environment. - A
clientIdandclientSecretissued for the production environment are rejected in the sandbox environment.
Either case returns a 401 result during initialization.
This section covers starting the SDK, supplying credentials at runtime, checking their status, and releasing the SDK when it's no longer needed.
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.
| 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 | Persisted encrypted after the first run; omit on subsequent launches. |
| 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. |
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
}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.
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.
The following steps trace what happens between calling startPayment() and receiving a result:
- 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. parseResult(activityResult) is also used for process-death recovery. See Recovery After Process Death.
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.
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.
| Parameter | Type | Required | Description |
|---|---|---|---|
| baseAmount | BigDecimal | Yes | Specifies the amount before ZCP (zero cost processing) or tip adjustments. Two decimal places maximum. |
| pricingType | PricingType? | Conditional | Specifies CARD or CASH. Required for Dual Pricing merchants to indicate which price baseAmount represents. Must be omitted otherwise. |
| captureMethod | CaptureMethod | No | Specifies AUTO (sale, default) or MANUAL (authorization to capture later). |
| readingMethod | ReadingMethod? | No | Specifies the input method for the terminal.KEYED_ENTRY opens manual card entry on the terminal.Omit to use tap, insert, or swipe. |
| tipAmount | BigDecimal? | No | Specifies a preset tip amount. Omit to let the terminal prompt the customer. |
| tipRatePercent | BigDecimal? | No | Specifies a preset tip as a percentage. |
| referenceId | String? | No | Specifies uour order reference, echoed on the transaction record. |
| posDeviceId | String? | No | Specifies the till within your estate. |
| paymentProcessorId | String? | No | Specifies a payment processor. This override the merchant's default processor. |
| customerId | String? | No | Specifies the associated payment with a stored customer. |
| requestPaymentMethodStorageConsent | Boolean | No | Specifies 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.
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.
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.
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.
An approved payment returns the following properties describing the completed transaction.
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. |
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.
An error indicates the payment did not reach a definitive outcome and carries the following properties to help you diagnose and recover.
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
This lists each ErrorReason value and how to handle it.
| 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
This describes each property returned by PendingPaymentCheck.
| 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
This lists the methods available for recovering from a process death.
| 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. |
These methods return read-only information about the merchant's terminals, payment configuration, and this device.
| 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.
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 aninitialize()with changed configuration, re-register to obtain a new launcher.
The following table lists common integration issues, their causes, and how to resolve them.
| 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. |