Tapaya
Integration GuideTapaya Terminal App

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

OptionBest forResult delivery
Accept SDK (recommended)Native Android appsAccept.payments.pay() emits a Flow<PaymentEvent>: creation, launch, and the terminal result, handled for you
DeeplinkWebViews, cross-platform frameworks, QR codesYou 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

  1. Your app starts a payment with Accept.payments.pay(amount, currency).
  2. The SDK registers the payment with the Tapaya backend and launches the Tapaya Terminal plugin on the same device.
  3. The customer taps their card or phone on the Tapaya Terminal screen.
  4. Tapaya Terminal settles the payment with the Tapaya backend.
  5. The SDK observes the payment until it reaches a final state and emits PaymentEvent.Result carrying a PayResult. If the payment can't be created at all, it emits PaymentEvent.CreationFailed instead, usually within a second.

Prerequisites

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:

PayResultMeaning
Success(paymentToken)Authorized and captured. Funds will settle to the merchant.
DeclinedRejected by the card processor or issuing bank.
CanceledCancelled 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 UI

Stop 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:

ExceptionWhen it happensWhat to do
PluginUnavailableThe 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.
LocationPermissionRequiredYour app has not been granted location permission.Request ACCESS_FINE_LOCATION, then retry.
LocationUnavailable / LocationTimeoutA device location fix could not be resolved.Ask the user to enable location and retry.
SessionExpiredThe authenticated session expired.Re-authenticate with Accept.auth.authenticate().
NoInternetConnectionNo network connectivity.Ask the user to reconnect and retry.
PaymentInProgressA 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:

  1. Create a payment from your backend via the Payments API and keep its paymentId.
  2. Open Tapaya Terminal with that paymentId using the pay deeplink.
  3. 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.

The scheme selects which build of Tapaya Terminal handles the link:

EnvironmentScheme
Productiontapaya-accept
Sandboxtapaya-accept-sandbox

If a link doesn't resolve, that build of Tapaya Terminal is not installed.

tapaya-accept://pay?paymentId=<paymentId>&amount=<amount>&currency=<ISO4217>
Query parameterRequiredNotes
paymentIdYesThe payment created in step 1.
amountYesAmount in major units as a whole number — amount=15 charges 15.00. Note this differs from the SDK, which takes minor units.
currencyYesISO 4217 currency code, e.g. CZK.
val uri = Uri.parse("tapaya-accept://pay?paymentId=$paymentId&amount=15&currency=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
}

Sign a merchant in to Tapaya Terminal with a merchant token from your backend:

tapaya-accept://login?token=<merchantToken>
Query parameterRequiredNotes
tokenYesMerchant token identifying the account to sign in.
activationCodeNoPre-fills the terminal activation code.

Test the integration

  1. Initialize with isProduction = false (the default); sandbox payments never move real money.
  2. Install the Tapaya Terminal plugin on your test device, sign in with a sandbox merchant, and activate.
  3. Run a payment for a small amount and verify the Success flow end to end, including your receipt UI.
  4. Exercise the failure paths: decline a payment on the terminal, cancel one, and start one with an unsupported currency; your PayResult/CreationFailed handling 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.

On this page