Callbacks
Receive real-time transaction status updates via webhooks, including optional signed callbacks
Callbacks
When a transaction is completed, Bridge sends a POST request to the callback_url you provided in the original payment request. This eliminates the need to poll our API for status updates.
Callback URL Requirements
Your callback endpoint must:
- Be publicly accessible
- Accept POST requests with a JSON body
- Respond with HTTP
200
Sample Callback Payload
{
"collection_trans_id": null,
"trans_ref": "2220260609162313-03947541",
"payment_option": null,
"trans_id": "0982989097",
"trans_status": "000",
"customer_number": "233244000001",
"nw": "MTN",
"payment_mode": "MOM",
"amount": "0.21",
"message": "SUCCESSFUL"
}Callback Fields
| Field | Description |
|---|---|
collection_trans_id | Bridge's internal transaction ID |
trans_ref | Your original transaction ID (the transaction_id you submitted) |
payment_option | Payment option — not in use, can be ignored |
trans_id | The network's transaction ID |
trans_status | Transaction status code |
customer_number | Customer's phone number |
nw | Network of the customer's phone number (e.g. MTN, VOD, AIR) |
payment_mode | Payment mode used (e.g. MOM for Mobile Money) |
amount | Transaction amount |
message | Human-readable status description |
Verifying callback signatures
Bridge can sign outbound callbacks (payment results and other async events) so your server can confirm the POST came from us and that the JSON body was not altered in transit.
When signature headers are present
| Traffic | Signed? |
|---|---|
| Live/production callbacks | Only after TheBridge enables callback signatures for your service and an active secret_key exists. Until then, live callbacks are unsigned — contact TheBridge to enable signing. |
| Sandbox payment callbacks | Always when an active secret_key exists. Use sandbox payment traffic to implement verification before going live. |
| Other sandbox callbacks (e.g. auto-debit forward) | Same rule as live (follows the enrollment flag). |
If signing is expected but no secret is configured, we still POST the callback without signature headers.
Headers
| Header | Description |
|---|---|
X-TheBridge-Timestamp | Unix time in seconds (UTC) when the signature was computed. |
X-TheBridge-Signature | v1= followed by lowercase hex HMAC-SHA256. |
Secret key
Use the same secret_key (API password) you use for Basic authentication (client_key / secret_key from your service keys). We never send the secret on the callback request. If you rotate or reset your keys, the signing secret changes immediately — update your verifier when you rotate.
Signed payload
The HMAC input is a single string:
{timestamp}.{raw_json_body}{timestamp}is the exact value ofX-TheBridge-Timestamp(ASCII digits).{raw_json_body}is the raw HTTP request body as received (byte-for-byte). Do not parse JSON and re-serialize before verifying; key order and spacing must match what we sent.
Algorithm: HMAC-SHA256, output as lowercase hexadecimal, prefixed with v1= in the header.
Replay protection
Reject callbacks whose X-TheBridge-Timestamp is too old or too far in the future. We recommend a 5 minute tolerance window on your server clock.
Compare the received signature to the expected value in constant time. If verification fails, do not update the transaction.
Example implementation
const crypto = require("crypto");
function verifyTheBridgeCallback(rawBody, timestamp, signatureHeader, secretKey, maxSkewSec = 300) {
if (!timestamp || !signatureHeader) return false;
const now = Math.floor(Date.now() / 1000);
const ts = Number(timestamp);
if (!Number.isFinite(ts) || Math.abs(now - ts) > maxSkewSec) return false;
const expected =
"v1=" +
crypto.createHmac("sha256", secretKey).update(`${timestamp}.${rawBody}`).digest("hex");
const received = Buffer.from(signatureHeader);
const expectedBuf = Buffer.from(expected);
if (received.length !== expectedBuf.length) return false;
return crypto.timingSafeEqual(received, expectedBuf);
}import hmac
import hashlib
import time
def verify_thebridge_callback(
raw_body: bytes,
timestamp: str | None,
signature_header: str | None,
secret_key: str,
max_skew_sec: int = 300,
) -> bool:
if not timestamp or not signature_header:
return False
try:
ts = int(timestamp)
except ValueError:
return False
now = int(time.time())
if abs(now - ts) > max_skew_sec:
return False
expected = (
"v1="
+ hmac.new(
secret_key.encode(),
f"{timestamp}.".encode() + raw_body,
hashlib.sha256,
).hexdigest()
)
return hmac.compare_digest(signature_header, expected)Handling the Callback
When signature headers are present, verify the callback before you update the transaction. Then use trans_status to determine the outcome and update your system accordingly.
trans_status | Meaning | Recommended Action |
|---|---|---|
000 | Successful | Mark the transaction as successful and fulfil the order |
001 | Failed | Mark the transaction as failed and notify the customer |
002 | Pending | Transaction is still processing — wait for a follow-up callback |
003 | Cancelled | Mark the transaction as cancelled |
Use trans_ref to match the callback to the original transaction in your system.
For a full list of status codes, see Response Codes.
Missed Callbacks
If your system does not receive a callback (e.g. due to downtime or a network issue), you can query the transaction status directly using the Get Transaction Status endpoint. Pass the original transaction_id you submitted, and the response will include a status field that you should handle the same way as trans_status in the callback.