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_apito 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.
- Build a Sale with
amount(Long, cents) and optionalexternalPosData. - Call
startTransaction- the Payment App opens in the foreground. - The customer completes payment on the terminal.
- The Payment App returns a
ResultObjectJSON payload (Activity result or AIDL callback). - Evaluate success:
transactionTypeisSALEorSALE_WITH_CASHBACKandisApproved == true(typicallytransactionResult == "approved_confirmed"). - Persist
transactionId- required for refunds and voids. - 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
| Callback | Success |
|---|---|
| Sale | transactionType is SALE or SALE_WITH_CASHBACK and isApproved == true |
| Refund | transactionType == REFUND and isApproved == true |
| Void | VoidResponse.transactionResult indicates success (no isApproved field) |
Refund flow
The original sale and refund must be processed on the same terminal.
- Call
refundTransaction(amount, transactionId)wheretransactionIdis the UUID from the original sale. - The Payment App prompts for supervisor approval (using a 6-digit PIN).
- The operator enters a refund reason.
- On callback, apply the refund success rule from the table above.
- Check
isRefundablevia 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:
- Process the normal Intent result returned to your POS app.
- For limited on-terminal recovery, call
lastTransaction()orsearchTransaction. - Use a merchant-level point of sale webhook on your backend as the primary recovery path.
- 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() behaviourlastTransaction() 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.
| Channel | Format | How to access |
|---|---|---|
| Dashboard | CSV | Dashboard reporting |
| Reconciliation API | JSON (/transactions-recon) | Reconciliation API |
| Point of sale webhooks | JSON (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
| Step | Action |
|---|---|
| 1 | (Optional) Install Mock Payments App on any Android device - no UAT account or Maven token required |
| 2 | Install UAT Payment App on a Sunmi terminal; log in with UAT account credentials from Peach Payments |
| 3 | Run test matrix (sale, decline, refund, void, recovery) - see Test your integration |
| 4 | Publish 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:
| Code | Meaning | Action |
|---|---|---|
PAYMENT_APP_CLOSED_BEFORE_RESPONSE | Payment App closed before callback | Run transaction lookup; treat as unsuccessful unless lookup returns a passing success result |
PAYMENT_APP_NOT_INSTALLED | Payment App missing on device | Install UAT or production Payment App |
INVALID_AMOUNT | Amount ≤ 0 or over limit | Fix amount before retry |
Lookup errors: If alertError fires on a lookup call, treat the payment as unsuccessful in your POS.
Updated 20 days ago