Webhook shape
What Zoorya Pay sends to your callback URL when a payment finishes.
When a payment reaches its final status, Zoorya Pay sends a POST with a JSON body to the callback URL you gave. There is no webhook setting in the portal — you set the URL on each payment.
The body is the whole payment. Find your order with externalUniqueId — the ID you sent when you created the payment. Where you set the callback URL depends on how you created the payment:
| You created the payment with | Set the callback URL in |
|---|---|
| Create one-time link | additionalFields — the item with id: "callbackUrl" |
| C2B Direct or a QR payment | callbackUrl, at the top level |
Answer 200 quickly and confirm the result before you ship the order — see Handling webhooks.
Examples#
{
"paymentId": 4581646,
"paymentReference": "",
"externalUniqueId": "zplink0001",
"status": "SUCCESSFUL",
"errorDescription": "",
"amount": 150,
"currency": "ETB",
"fee": 0,
"description": "",
"gatewayTransactionId": "DJL0ABC123",
"organisationId": 12345678,
"walletId": 123456789,
"otherWalletId": 123456789,
"created": "2026-10-21T10:48:08Z"
}These show the fields that matter. The real body has more — see Full body below.
Status#
status | Meaning | What to do |
|---|---|---|
SUCCESSFUL | The customer paid. | Confirm it (see below), then fulfil the order. |
ERROR_PERM | The payment failed for good — for example the customer cancelled, or the PIN was wrong. errorDescription says why. | Show the reason and let the customer try again. |
TIMEOUT | The customer did not answer in time. | Let the customer try again. |
CANCELLED | The payment was cancelled. | Let the customer try again. |
Every new try is a new payment with a new externalUniqueId.
Fields#
Zoorya Pay's ID for this payment attempt. Use it to spot a webhook you already handled.
The ID you sent when you created the payment. Use it to find your order.
The final status: SUCCESSFUL, ERROR_PERM, TIMEOUT or CANCELLED.
Why the payment failed, in words — e.g. The user cancels the payment of the USSD Push service. Empty when it succeeded.
The amount in ETB.
Always ETB.
The fee for this payment.
The description you sent.
The reference from telebirr, M-Pesa or the bank. Quote it when you contact support about this payment.
Your organisation ID.
The wallet that receives the money.
Your receiving wallet on payment links. null on C2B Direct.
When the payment was created (UTC).
Full body#
Show the full webhook body
The body also carries card and 3-D Secure fields (authentication, cardIssuerDetails, paymentInstrumentInfo, …). They stay empty for mobile money and bank payments, so you can ignore them.
{
"acceptedCardSchemes": [],
"acceptedPaymentMechanisms": [],
"accountType": "",
"additionalFields": [],
"amount": 250.5,
"amountOption": "",
"associatedPaymentId": null,
"authCode": "",
"authentication": {
"authIndicator": "",
"dsTransId": "",
"eci": "",
"enrolmentStatus": "",
"mdErrorMsg": "",
"mdStatus": "",
"method": "",
"protocolVersion": "",
"xid": ""
},
"bankResponse": "",
"cardIssuerDetails": {
"bin": "",
"brand": "",
"classification": "",
"issuer": "",
"productType": ""
},
"completionUrl": "",
"created": "2026-10-21T10:48:08Z",
"currency": "ETB",
"customerId": null,
"description": "Order 10045",
"errorDescription": "",
"externalUniqueId": "zplink0001",
"fee": 0,
"gatewayTransactionId": "DJL0ABC123",
"location": "",
"merchantId": "",
"merchantName": "",
"note": "",
"organisationId": 12345678,
"otherWalletId": 123456789,
"paymentId": 4589032,
"paymentInstrumentInfo": {
"cardBin": "",
"cardExpires": "",
"cardIssuer": "",
"cardLast4": "",
"cardName": "",
"cardOnFileId": "",
"cardType": "",
"externalWalletId": "",
"externalWalletType": ""
},
"paymentReference": "",
"paymentTerminalData": {
"terminalId": "null"
},
"paymentType": "",
"retrievalReferenceNumber": 0,
"status": "SUCCESSFUL",
"walletId": 123456789
}Confirm before you ship#
Do not trust a webhook on its own — anyone can send a request to your callback URL. Call Get a payment with the paymentId, and act only when status is SUCCESSFUL and amount matches your order.
Repeats#
A webhook can arrive more than once for the same payment. Keep the paymentId of every webhook you handled, and ignore one you have already seen.
Want to watch webhooks arrive on your laptop? See Test webhooks locally.

