Terminal integration flows

Use the Intent API when your POS app runs on the same Android device as the Peach Payments Payment App. Amounts are in minor units (cents). For till-point QR (POS on a separate device), see Till-point QR integration.

Prerequisites

  • Add com.peach:intent_api to your Android POS project - see App-to-app integration.
  • Install the Peach Payments Payment App on the terminal (UAT or production build - APKs on Test your integration).
  • For dedicated POS deployments, ask Peach Payments to enable integrated mode (disables stand-alone numpad payments on the terminal).

Sale flow

The POS app initiates payment; the Payment App handles card processing and returns a result to your app.

  1. Build a Sale with amount (Long, cents) and optional externalPosData.
  2. Call startTransaction - the Payment App opens in the foreground.
  3. The customer completes payment on the terminal.
  4. The Payment App returns a ResultObject JSON payload (Activity result or AIDL callback).
  5. Evaluate success: transactionType is SALE or SALE_WITH_CASHBACK and isApproved == true (typically transactionResult == "approved_confirmed").
  6. Persist transactionId - required for refunds and voids.
  7. If webhooks are configured, your backend receives a separate JSON notification - see Point of sale webhooks (no decryption required).

Integrated POS: Set disableReceiptingOnOutcome(true) so the Payment App auto-returns to your POS after ~10 seconds without a manual dismiss step.

Sale.Builder(context)
    .amount(amountInCents)
    .disableReceiptingOnOutcome(true)
    .setExternalPosData(mapOf("merchantTransactionId" to orderId))
    .setListener(object : SaleTransactionListener {
        override fun alertSaleTransactionResponse(response: PosTransactionSummary) {
            // Check transactionType + isApproved - see success table below
        }
        override fun alertError(error: IntentError) { /* handle */ }
    })
    .startTransaction()

Success checks by operation

CallbackSuccess
SaletransactionType is SALE or SALE_WITH_CASHBACK and isApproved == true
RefundtransactionType == REFUND and isApproved == true
VoidVoidResponse.transactionResult indicates success (no isApproved field)

Refund flow

The original sale and refund must be processed on the same terminal.

  1. Call refundTransaction(amount, transactionId) where transactionId is the UUID from the original sale.
  2. The Payment App prompts for supervisor approval (using a 6-digit PIN).
  3. The operator enters a refund reason.
  4. On callback, apply the refund success rule from the table above.
  5. Check isRefundable via Transaction lookup before offering refund in your UI.

Multiple partial refunds are supported for up to 180 days, provided that their total does not exceed the original sale amount. Refund access is controlled by the supervisor PIN. Limit access to this PIN to reduce refund fraud risk.

There is no separate integrator-facing "reversal" operation - choose void or refund based on isVoidable / isRefundable.

Refund.Builder(context)
    .refundTransaction(amountInCents, originalTransactionId)
    .setListener(refundListener)
    .startTransaction()

Void flow (temporarily unavailable)

🚧

Intent-triggered voids have a known issue. Use a refund instead until Peach Payments resolves it.

Transaction lookup and recovery flow

Use when your app crashes, the WebView is killed, or you receive PAYMENT_APP_CLOSED_BEFORE_RESPONSE.

Use these result paths:

  1. Process the normal Intent result returned to your POS app.
  2. For limited on-terminal recovery, call lastTransaction() or searchTransaction.
  3. Use a merchant-level point of sale webhook on your backend as the primary recovery path.
  4. If webhook delivery fails, pull the full merchant day from the Reconciliation API.

Mark the payment attempt as in progress. If you receive no final result, treat the attempt as failed.

🚧

Card authorisation may still complete on the terminal even if your app is killed after handing off to the Payment App.

lastTransaction() behaviour

lastTransaction() returns only the most recent transaction where the customer tapped, inserted, or swiped a card on the terminal. It does not include attempts where no card details were provided, such as validation errors, cancellation before card presentment, or connectivity errors during initiation.

If lookup returns an error (alertError) or you cannot match a successful card-present result to your open order, always treat the payment as unsuccessful in your application. Do not mark the order paid based on lookup alone unless you receive a result that passes the success checks in the table above.

For recovery when lastTransaction() does not cover your scenario, use searchTransaction with filters such as amount, time window, or RRN. You cannot search by posData.merchantTransactionId.

TransactionLookup.Builder(context)
    .lastTransaction()
    .setListener(lookupListener)
    .startTransaction()

Order reference (Dashboard and reconciliation)

Pass your order ID in externalPosData using the merchantTransactionId key.

Set and persist this value before you start the payment. It appears in the Intent response posData, point of sale webhooks, the Peach Payments Dashboard, and Dashboard exports. The Reconciliation API returns it in the transactionId field.

You can include additional externalPosData keys (String, Number, Boolean), but Peach Payments does not include them in standard reconciliation files.

Reconciliation flow

Your POS does not trigger settlement or batch close - Peach Payments handles daily settlement.

ChannelFormatHow to access
DashboardCSVDashboard reporting
Reconciliation APIJSON (/transactions-recon)Reconciliation API
Point of sale webhooksJSON (pushed)Point of sale webhooks

Match payments to orders using merchantTransactionId in Intent posData and webhook transaction.posData. The Reconciliation API returns the same value in transactionId.

Reversal timing

Peach Payments sends successful and declined webhooks immediately. If the terminal cannot show the Transaction successful screen because of connectivity loss, an app crash, or app closure, the outcome is unclear on the terminal and Peach Payments does not send a successful webhook.

Peach Payments reverses the payment within five minutes. The customer might still receive a successful payment notification on their phone during this period. Decide whether to wait for the reversal before you attempt a new payment. If no success is confirmed, you can retry the payment.

Duplicate submissions

The Intent API does not reject a second sale when you reuse the same merchantTransactionId. Implement idempotency in your POS and use the webhook or Reconciliation API to confirm the outcome.

Sample response (successful sale)

{
  "finalResultStatus": "CODE_SUCCESS",
  "receiveAction": "SaleIntent",
  "transaction": {
    "amount": 1000,
    "currencyCode": "ZAR",
    "transactionId": "358f45f6-3818-4e3f-9af8-9cc3eca8f140",
    "isApproved": true,
    "transactionResult": "approved_confirmed",
    "transactionType": "SALE",
    "rrn": "340368496951",
    "posData": {
      "merchantTransactionId": "ORDER-12345"
    }
  }
}

Test and go-live

StepAction
1(Optional) Install Mock Payments App on any Android device - no UAT account or Maven token required
2Install UAT Payment App on a Sunmi terminal; log in with UAT account credentials from Peach Payments
3Run test matrix (sale, decline, refund, void, recovery) - see Test your integration
4Publish your POS APK via Sunmi (public upload); Peach Payments enables it on merchant terminals
📘

UAT rule: Only round amounts (for example, R 100.00) succeed as live approvals on UAT terminals. Non-round amounts decline - use this to test decline handling. For Intent errors, see Intent error codes.

Error handling

Flow-level errors return via alertError(IntentError) on your listener. For the full code list and descriptions, see Intent error codes.

Common recovery-related codes:

CodeMeaningAction
PAYMENT_APP_CLOSED_BEFORE_RESPONSEPayment App closed before callbackRun transaction lookup; treat as unsuccessful unless lookup returns a passing success result
PAYMENT_APP_NOT_INSTALLEDPayment App missing on deviceInstall UAT or production Payment App
INVALID_AMOUNTAmount ≤ 0 or over limitFix amount before retry

Lookup errors: If alertError fires on a lookup call, treat the payment as unsuccessful in your POS.


Did this page help you?