Ssentezo Developers
Sandbox and production use the same API host. The organization status, SANDBOX or LIVE, is the environment. This portal does not simulate a successful payment when a call fails.

Webhooks

A callback is not proof of payment. Receiving it, or seeing a browser return to your site, must not fulfill an order. Confirm the order with the status API.

What to do when a callback arrives

  1. Read data.externalReference and find your own pending order. If you cannot find exactly one order, stop.
  2. Call POST /api/get_status/{externalReference} with your API credentials. Ignore the callback's claim of success.
  3. Fulfill only when that status call returns SUCCEEDED, the reference matches the order, the currency is UGX, and the amount matches.
  4. Do not fulfill FAILED, PENDING, an unknown status, a mismatched amount, or a second notice for an order you already fulfilled.
  5. 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, and Ssentezo-Signature. The signature is v1= plus lowercase hex HMAC-SHA256 of timestamp.event_id.raw_body.
  • Reject a timestamp more than five minutes from your clock before you parse the body. Compare the signature with hash_equals over 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_callback and failure_callback.
  • Collection also accepts the older callback field as the success URL.
  • Bank transfer requests do not store callback URLs. Poll the bank status endpoint.
  • Card checkout also has redirect_url for 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.