Tapaya Terminal App
Accept contactless card payments by handing the card interaction off to the Tapaya Terminal plugin app.
Take contactless card payments from your Android app by handing the card interaction off to the Tapaya Terminal
plugin app. Your app stays in control of the sale; the plugin handles the NFC tap, PIN entry, and card
processing, then the result flows back through the Accept.payments.pay() event stream.
Use this integration when you want Tap to Pay without embedding a payment processor in your own app. If your platform is enabled for embedded Stripe Tap to Pay instead, see the Embedded Tap to Pay guide.
Integration options
| Option | Best for | Result delivery |
|---|---|---|
| Accept SDK (recommended) | Native Android apps | Accept.payments.pay() emits a Flow<PaymentEvent>: creation, launch, and the terminal result, handled for you |
| Deeplink | WebViews, cross-platform frameworks, QR codes | You create the payment via the Tapaya API and poll its status; the plugin opens via a deeplink |
The SDK path is covered first; see Integrate without the SDK for the deeplink contract.
How it works
- Your app starts a payment with
Accept.payments.pay(amount, currency). - The SDK registers the payment with the Tapaya backend and launches the Tapaya Terminal plugin on the same device.
- The customer taps their card or phone on the Tapaya Terminal screen.
- Tapaya Terminal settles the payment with the Tapaya backend.
- The SDK observes the payment until it reaches a final state and emits
PaymentEvent.Resultcarrying aPayResult. If the payment can't be created at all, it emitsPaymentEvent.CreationFailedinstead, usually within a second.
Prerequisites
- The Tapaya Terminal plugin installed on the device and activated for your merchant (see Install and activate the terminal).
- A device with NFC, running Android 11 (API 30) or later.
- The Accept SDK installed and initialized, with the merchant authenticated.
- Location permission (
ACCESS_FINE_LOCATION) granted at runtime by your app.
Install and activate the terminal
Activate once per device. Activation links the Tapaya Terminal plugin to the same merchant account your app is authenticated with.
if (!Accept.plugin.isInstalled()) {
// Opens the store listing so the user can install Tapaya Terminal
Accept.plugin.install()
return
}
// suspend; launches the plugin's activation flow and waits for the outcome
when (val result = Accept.plugin.activateTerminal()) {
ActivateTerminalResult.Success -> { /* device linked, ready to take payments */ }
ActivateTerminalResult.Canceled -> { /* user backed out */ }
is ActivateTerminalResult.Failed -> result.reason // ActivateTerminalFailureReason
}ActivateTerminalFailureReason is one of INVALID_INTENT, HANDOVER_FAILED, CONFIRM_FAILED,
MERCHANT_NOT_ONBOARDED, or UNKNOWN.
Note
Accept.plugin.isInstalled() returns false when the Tapaya Terminal plugin is missing. Read the plugin's
current state at any time with Accept.plugin.status(), and guard your payment UI accordingly.
Create a payment
Amounts are always in the smallest unit of the currency: hellers for CZK, cents for EUR. 1500 with
currency "CZK" charges 15.00 Kč. Currencies are ISO 4217 codes.
pay() returns a Flow<PaymentEvent>; collect it to observe progress and the terminal outcome. It never throws —
failures surface as PaymentEvent.CreationFailed.
Accept.payments.pay(amount = 1500, currency = "CZK") // 15.00 Kč
.collect { event ->
when (event) {
PaymentEvent.Creating -> { /* registering the payment */ }
is PaymentEvent.Created -> { /* backend payment created: event.paymentToken */ }
PaymentEvent.Launched -> { /* Tapaya Terminal is in the foreground */ }
is PaymentEvent.Result -> when (val r = event.payResult) {
is PayResult.Success -> showReceipt(r.paymentToken)
PayResult.Declined -> showDeclined()
PayResult.Canceled -> showCancelled()
is PayResult.Failed -> showUnexpected(r.reason) // PayFailureReason
}
is PaymentEvent.CreationFailed -> handleError(event.cause) // AcceptException
}
}While the payment is in progress the Tapaya Terminal plugin is in the foreground. Your app resumes automatically
when the payment finishes. To resume an interrupted payment, pass its paymentToken back into
pay(amount, currency, paymentToken).
Payment outcome
The terminal PaymentEvent.Result carries a PayResult:
PayResult | Meaning |
|---|---|
Success(paymentToken) | Authorized and captured. Funds will settle to the merchant. |
Declined | Rejected by the card processor or issuing bank. |
Canceled | Cancelled by the customer or merchant before completion. |
Failed(reason) | Did not complete; reason is a PayFailureReason. |
PayFailureReason is one of INVALID_INTENT, NOT_ACTIVATED, TERMINAL_INIT_FAILED, PAYMENT_SETUP_FAILED,
PLUGIN_UNAVAILABLE, or UNKNOWN.
Reading payment details
PayResult.Success gives you the paymentToken. Fetch the full record — including receiptUrl, requested and
settlement amounts, and timestamps — with Accept.payments.status():
val status: PaymentStatus = Accept.payments.status(paymentToken)
status.state // PENDING, SUCCESS, CANCELLED, REFUNDED, FAILED, ACTION_NEEDED
status.receiptUrl // for your receipt UIStop an in-flight payment with Accept.payments.cancel(paymentToken), which returns the resulting PaymentState.
Error handling
A payment that reaches the terminal but does not succeed is not an exception — it arrives as
PaymentEvent.Result carrying PayResult.Declined, Canceled, or Failed(reason). Failures that stop the
payment from being created arrive as PaymentEvent.CreationFailed(cause), where cause is a typed
AcceptException. The ones specific to this integration:
| Exception | When it happens | What to do |
|---|---|---|
PluginUnavailable | The Tapaya Terminal plugin is not installed or its service could not be bound. | Prompt installation with Accept.plugin.install(). |
CurrencyNotAvailableForMerchant(code) | The merchant is not configured for the requested currency. | Charge in an enabled currency, or update the merchant's configuration. |
AmountBelowMinimum(minimum, currency) | The amount is below the configured minimum. | Charge at or above minimum. |
LocationPermissionRequired | Your app has not been granted location permission. | Request ACCESS_FINE_LOCATION, then retry. |
LocationUnavailable / LocationTimeout | A device location fix could not be resolved. | Ask the user to enable location and retry. |
SessionExpired | The authenticated session expired. | Re-authenticate with Accept.auth.authenticate(). |
NoInternetConnection | No network connectivity. | Ask the user to reconnect and retry. |
PaymentInProgress | A previous pay() call is still awaiting a result. | Wait for the current payment to finish. |
Accept.payments.status() and cancel() are suspend functions and throw these exceptions (rather than
emitting events); wrap them in try-catch. status()/cancel() also throw PaymentNotFound for an unknown
token. See the full list in Error Handling.
Timeouts
A card payment must be completed on the terminal shortly after it starts. If the customer never taps, or the
Tapaya Terminal plugin is closed mid-payment, the payment ends as PayResult.Canceled or
PayResult.Failed, and the underlying payment is cancelled server-side and never settles. Always confirm the
final state with Accept.payments.status() before treating a payment as complete.
Integrate without the SDK
If you can't embed the Accept SDK (a cross-platform framework, a thin POS shell, a web-based till), you can drive Tapaya Terminal directly with a deeplink. The division of work changes: you create the payment through the Tapaya API and you poll its status for the result; Tapaya Terminal only handles the card interaction.
The flow:
- Create a payment from your backend via the Payments API and keep
its
paymentId. - Open Tapaya Terminal with that
paymentIdusing the pay deeplink. - Poll the payment status via the Payments API until it leaves
PENDING. Deeplinks have no return channel — polling is the only source of truth.
Note
Tapaya Terminal must be installed, signed in to the merchant account, and configured for the payment's currency. There is no activation step in this mode; the signed-in account is the merchant of record. Sign a merchant in with the login deeplink if needed.
Deeplink schemes
The scheme selects which build of Tapaya Terminal handles the link:
| Environment | Scheme |
|---|---|
| Production | tapaya-accept |
| Sandbox | tapaya-accept-sandbox |
If a link doesn't resolve, that build of Tapaya Terminal is not installed.
Pay deeplink
tapaya-accept://pay?paymentId=<paymentId>&amount=<amount>¤cy=<ISO4217>| Query parameter | Required | Notes |
|---|---|---|
paymentId | Yes | The payment created in step 1. |
amount | Yes | Amount in major units as a whole number — amount=15 charges 15.00. Note this differs from the SDK, which takes minor units. |
currency | Yes | ISO 4217 currency code, e.g. CZK. |
val uri = Uri.parse("tapaya-accept://pay?paymentId=$paymentId&amount=15¤cy=CZK")
try {
startActivity(Intent(Intent.ACTION_VIEW, uri).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
} catch (e: ActivityNotFoundException) {
// Tapaya Terminal is not installed, send the user to Google Play
}Login deeplink
Sign a merchant in to Tapaya Terminal with a merchant token from your backend:
tapaya-accept://login?token=<merchantToken>| Query parameter | Required | Notes |
|---|---|---|
token | Yes | Merchant token identifying the account to sign in. |
activationCode | No | Pre-fills the terminal activation code. |
Test the integration
- Initialize with
isProduction = false(the default); sandbox payments never move real money. - Install the Tapaya Terminal plugin on your test device, sign in with a sandbox merchant, and activate.
- Run a payment for a small amount and verify the
Successflow end to end, including your receipt UI. - Exercise the failure paths: decline a payment on the terminal, cancel one, and start one with an unsupported
currency; your
PayResult/CreationFailedhandling should cover each.
When you're ready to go live, set isProduction = true in initialize and authenticate with production
credentials. The payment code does not change.