Skip to main content
Webhooks send delivery receipts to your server as messages move through their lifecycle. Webhook requests are signed with HMAC SHA-256 so you can verify they came from UgaText.

Events

Payload envelope

Every event uses the same envelope. Event-specific fields are in data. The delivery_id field matches the X-UgaText-Delivery-ID header.

SMS events

sms.failed webhooks report the reason in error_reason (alongside error_code). GET /sms/status/{id} and cross-border DLR callbacks use failure_reason instead. To handle both, read data.error_reason || data.failure_reason.

wallet.topup

Fired as soon as a Mobile Money top-up (MTN Mobile Money, Airtel Money, or M-Pesa) completes and SMS credits are added to your wallet.

Configure in the dashboard

1

Open webhook settings

In the dashboard, go to API & Webhooks and open the Webhooks tab.
2

Set your endpoint

Paste your HTTPS destination URL, set a webhook secret, and choose which events to receive.
3

Adjust advanced options

Optionally configure retry policies and payload formats, then save.
4

Test

Use the built-in webhook simulator to send test events to your endpoint.

Manage webhooks with the API

Register a webhook:
An invalid URL returns 400 INVALID_URL.

Request headers

Every webhook POST from UgaText includes these headers:
All webhook URLs on your account share one account-level signing secret (whsec_...).

Verify signatures

Always verify the signature before trusting the payload.
1

Parse the header

Split X-UgaText-Signature on , to get the timestamp t and the signature v1.
2

Build the signed payload

Join the timestamp, a literal period, and the raw, unparsed request body:
3

Compute the digest

Compute an HMAC SHA-256 of that string with your signing secret (whsec_...), encoded as lowercase hex.
4

Compare

Compare your digest with v1 using a constant-time function such as crypto.timingSafeEqual or hmac.compare_digest. To prevent replay attacks, also reject requests where t is more than 5 minutes (300 seconds) old.
Use the raw request body. Parsing and re-serializing the JSON changes the bytes and breaks verification.
If you prefer polling, call GET /sms/status/{id} to read a message’s current status.