Webhooks
What to do when a callback arrives
- Read
data.externalReferenceand find your own pending order. If you cannot find exactly one order, stop. - Call
POST /api/get_status/{externalReference}with your API credentials. Ignore the callback's claim of success. - Fulfill only when that status call returns
SUCCEEDED, the reference matches the order, the currency is UGX, and the amount matches. - Do not fulfill
FAILED,PENDING, an unknown status, a mismatched amount, or a second notice for an order you already fulfilled. - Do not fulfill a cancelled or refunded order because a late notice arrives. Record it and review it.
Signing is off until that organization enables it on the API keys page. The signing secret is shown once there. Until signing is enabled, the callback is one unsigned POST, it is not retried, and the timeout is five seconds. A signature does not replace the status API.
Signed delivery
When an organization turns signatures on, later settlements store an event and a scheduler delivers it. The event id, created_at, and JSON body stay the same across retries. Each attempt sends a new Ssentezo-Timestamp and a new HMAC. Bank transfers do not send callbacks.
- The signing secret is not the API key. It is shown once. During rotation the previous key verifies for 24 hours, and only the new key signs.
- Headers are
Ssentezo-Event-Id,Ssentezo-Timestamp,Ssentezo-Key-Id, andSsentezo-Signature. The signature isv1=plus lowercase hex HMAC-SHA256 oftimestamp.event_id.raw_body. - Reject a timestamp more than five minutes from your clock before you parse the body. Compare the signature with
hash_equalsover the exact bytes you received. - Store the event id. A repeat of that id, including a manual redelivery, must return success and must not fulfill or refund again.
- Network failures, timeouts, HTTP 429, and HTTP 5xx are retried up to five times. Other 4xx responses stop.
Where callbacks are set
- Collection, payout, and card requests accept
success_callbackandfailure_callback. - Collection also accepts the older
callbackfield as the success URL. - Bank transfer requests do not store callback URLs. Poll the bank status endpoint.
- Card checkout also has
redirect_urlfor the browser. That redirect is not a webhook and is not proof of payment.
Success body
{
"response": "OK",
"data": {
"transactionStatus": "SUCCEEDED",
"ssentezoWalletReference": "generated-uuid",
"externalReference": "order-1001",
"financialTransactionId": "provider-reference"
}
}
Failure body
{
"response": "ERROR",
"data": {
"transactionStatus": "FAILED",
"ssentezoWalletReference": "generated-uuid",
"externalReference": "order-1001",
"financialTransactionId": null
}
}
Handle duplicates
An unsigned callback is sent once. A signed event can be retried and redelivered, always with the same event id and body. Your endpoint can also be called more than once if you shared the same URL for success and failure. Store the external reference, and store the event id when a signature is present. Ignore a second SUCCEEDED after you have already fulfilled the order.
Sample receiver
This page cannot register a URL or POST a sample to your server. That would let the portal call arbitrary addresses. Use the payload above against a tunnel you control, then call POST /api/get_status/{externalReference} and trust that response.
These samples are the unsigned bodies. A signed delivery adds the headers above. Either way, the next step is the status API.
Ssentezo Developers