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
- Call
refundTransaction(amount, transactionId)wheretransactionIdis the UUID from the original sale. - The Payment App prompts for supervisor approval (using a 6-digit PIN).
- On callback, apply the refund success rule from the table above.
- Check
isRefundablevia Transaction lookup before offering refund in your UI.
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
- Call
performVoid(transactionId)for the original sale UUID. - The Payment App processes the void.
- Evaluate
VoidResponse- void uses a separate model withoutisApproved.
Check isVoidable on the original transaction before exposing void in your POS.
Transaction lookup and recovery flow
Use when your app crashes, the WebView is killed, or you receive PAYMENT_APP_CLOSED_BEFORE_RESPONSE.
- On resume, call
lastTransaction()orsearchTransaction. - Apply the same success rules for the returned
transactionType. - Match your order via
merchantTransactionIdinposData- set this inexternalPosDatabefore starting the sale.
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 a card on the terminal. It does not include attempts that failed before card presentment (for example, validation errors, user cancel before tap, 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 merchantTransactionId (where supported).
TransactionLookup.Builder(context)
.lastTransaction()
.setListener(lookupListener)
.startTransaction()Order reference (Dashboard and reconciliation)
Pass your order ID in externalPosData using the merchantTransactionId key.
This appears in the Intent response posData, Peach Payments Dashboard, reconciliation exports, and point of sale webhooks.
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) | Request documentation from support |
Match payments to orders using merchantTransactionId in Intent posData, Dashboard exports, reconciliation API responses, and webhook transaction.posData.
Duplicate submissions
The Intent API does not reject a second sale when you reuse the same merchantTransactionId. Implement idempotency in your POS: do not call startTransaction twice for the same open order; use lookup before retrying.
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.