POS Integrations API

Peach Payments enables in-store card payments through the Payment App on Sunmi terminals in South Africa. Use the POS Integrations API when your POS runs on a separate device from the Peach Payments terminal. Your till calls Peach Payments over HTTPS to push the payment to the terminal, the customer pays on the terminal, and your backend receives the outcome via a point of sale webhook.

📘

Ask Peach Payments to enable the POS Integrations API on the terminal.

What you need from Peach Payments

ItemRequired forHow to get it
Sandbox accountUAT terminal testingContact support
UAT serial numberSending requestsPeach Payments or your channel partner
Point of sale webhook set upReceiving the payment outcomeContact support with your HTTPS URL
POS Integrations API keyAuthenticating requestsContact support

Get an API key

Contact support to have Peach Payments issue a merchant-scoped API key.

Send it on every request: Authorization: Bearer pk_live_xxx

EnvironmentBase URL
Sandboxhttps://pos-sandbox.peachpayments.com
Livehttps://pos.peachpayments.com

Sale flow

  1. Make sure the terminal app is open, connected to Wifi, and displays Remote payments online.
  2. Your till calls POST /integrations/v1/terminals/{terminalId}/payment-requests with the payment details.
  3. Peach Payments pushes the payment to the terminal and returns 202 (received), 409 (busy), 422 (rejected), 504 (no ack), or 503 (offline).
  4. The customer completes payment on the terminal.
  5. Your backend receives a point of sale webhook with the outcome, matched to your order via merchantTransactionId. You can also poll for the outcome.
  6. Evaluate success: transactionType.value is 0 and transactionResult is approved_confirmed.

Payment request (sale)

curl --location "https://pos-sandbox.peachpayments.com/integrations/v1/terminals/{terminalId}/payment-requests" \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer pk_live_xxx' \
--data '{
  "amount": 3000,
  "merchantTransactionId": "your_order_id",
  "paymentMethod": "CARD",
  "posData": {"customField": "customValue"}
}'
FieldRequiredDescription
amountYesAmount in minor units (cents)
merchantTransactionIdYesYour order reference for reconciliation
paymentMethodNoRouting hint, for example "CARD"
posDataNoAdditional custom fields echoed on the transaction and Dashboard

{terminalId} in the URL is the terminal's serial number, for example TJ03P28120090. You can find it at the back of the terminal. It typically starts with PC, TJ, or P3.

Response codes

HTTP statusMeaningHow to handle
202Received - Peach Payments accepted the request and pushed it to the terminalWait for the point of sale webhook to learn the outcome
409Busy - the terminal is already processing another transactionWait, then retry when the terminal is free
422Rejected - the request was invalid, for example a malformed payload or a terminal not enabled for the POS Integrations APIFix the request; do not retry unchanged
503Offline - Peach Payments could not reach the terminalCheck the Remote payments online chip on the terminal; if it shows online, allow the cashier to push the payment again
504No acknowledgement - the terminal did not confirm receipt in timeCheck terminal connectivity; retry if the terminal is online

The response body includes the requestId. Peach Payments echoes the same value back as posData.requestId on every webhook this payment request generates, so you can correlate incoming webhooks to the request that triggered them. A single requestId can produce more than one webhook event, for example if the terminal automatically starts a new transaction attempt after an incorrect PIN.

Status polling

You can poll for the outcome using the requestId from the payment request response.

The requestId expires after 5 minutes. You cannot poll it after that time.

curl --location "https://pos-sandbox.peachpayments.com/integrations/v1/payment-requests/{requestId}" \
--header 'Authorization: Bearer pk_live_xxx'
{
  "requestId": "req_58510d57ca684ca78039521fb9118ea5",
  "merchantTransactionId": "sample-order-id-1",
  "transactionResult": "approved_confirmed",
  "failureReason": null,
  "amount": 10000,
  "currency": "ZAR",
  "transactionId": "a6060f962c19402580c0d54dfe28ca0c",
  "rrn": "457368760253",
  "updatedAt": "2026-09-23T11:39:40.653Z"
}
FieldDescription
transactionResultapproved_confirmed is the only value that reflects a successful transaction
failureReasonPopulated on a decline; null once transactionResult is approved_confirmed
transactionIdPeach transaction ID, - store this for refunds. The webhook returns the same value but includes dashes 8-4-4-4-12
rrnRetrieval reference number from the card scheme

Poll every couple of seconds until transactionResult leaves pending.

Webhook setup

Your backend can receive a point of sale webhook to learn the outcome - this is the lowest-latency way to find out a payment has completed. Contact support with your HTTPS URL and optional custom headers (that is, for Authorization).

Refunds

Refund a completed sale, in full or in part, using the transactionId you stored at the time of sale.

curl --location --request POST "https://pos-sandbox.peachpayments.com/integrations/v1/transactions/{transactionId}/refund" \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer pk_live_xxx' \
--data '{
  "amount": 3000,
  "note": "Customer returned item",
  "reason": "refund"
}'
FieldRequiredDescription
amountNoAmount to refund, in minor units (cents). Omit to refund the full remaining balance
noteNoFree-text note stored against the refund
reasonNoRefund reason, for example "refund"

{transactionId} must be the no-dash form - strip any dashes if you copied it from the webhook.

A successful refund returns 200:

{
  "transactionResult": "approved_confirmed",
  "transactionType": { "value": 20, "description": "Refund" },
  "message": "Refund completed",
  "amount": 3000,
  "currency": "ZAR"
}

A refund declined by the card provider returns 402:

{
  "error": "declined",
  "code": "refund_declined",
  "message": "The refund was declined by the provider",
  "transactionResult": "declined",
  "transactionType": { "value": 20, "description": "Refund" }
}

You can refund the same transaction more than once, as long as the total refunded doesn't exceed the original sale amount:

HTTP statuscodeMeaning
200-Refund completed
402refund_declinedThe provider declined the refund - nothing moved
400refund_amount_not_availableRequested amount is more than what's left to refund on this transaction
400transaction_not_refundableNothing left to refund - the transaction is already fully refunded
404transaction_not_foundUnknown transactionId, or it belongs to a different merchant

Reconciliation

Match till orders to payments using merchantTransactionId on the webhook payload (or the status poll response above).
The Reconciliation API returns the same value in its transactionId field - use it as a fallback if webhook delivery fails. You can also download transaction reports from the Dashboard - see Dashboard reporting.

Also store the transactionId from the webhook payload (or the status poll) against the order when it arrives - you need it to request a refund later.

Next steps

  1. Contact support for an Integrations API key.
  2. Configure point of sale webhooks.
  3. Test end-to-end with a UAT terminal - see Test your integration.

Did this page help you?