Zoorya Pay Developers

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 withSet the callback URL in
Create one-time linkadditionalFields — the item with id: "callbackUrl"
C2B Direct or a QR paymentcallbackUrl, 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#

statusMeaningWhat to do
SUCCESSFULThe customer paid.Confirm it (see below), then fulfil the order.
ERROR_PERMThe 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.
TIMEOUTThe customer did not answer in time.Let the customer try again.
CANCELLEDThe payment was cancelled.Let the customer try again.

Every new try is a new payment with a new externalUniqueId.

Fields#

paymentIdinteger

Zoorya Pay's ID for this payment attempt. Use it to spot a webhook you already handled.

externalUniqueIdstring

The ID you sent when you created the payment. Use it to find your order.

statusstring

The final status: SUCCESSFUL, ERROR_PERM, TIMEOUT or CANCELLED.

errorDescriptionstring

Why the payment failed, in words — e.g. The user cancels the payment of the USSD Push service. Empty when it succeeded.

amountnumber

The amount in ETB.

currencystring

Always ETB.

feenumber

The fee for this payment.

descriptionstring

The description you sent.

gatewayTransactionIdstring

The reference from telebirr, M-Pesa or the bank. Quote it when you contact support about this payment.

organisationIdinteger

Your organisation ID.

otherWalletIdinteger

The wallet that receives the money.

walletIdinteger

Your receiving wallet on payment links. null on C2B Direct.

createdstring

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.

json
{
  "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.