Webhooks
Webhooks are HTTP callbacks that send real-time transaction notifications to your server. When a payment reaches a final outcome, WaafiPay posts the transaction details to your registered endpoint, so you do not need to poll.
Current Support: Webhooks are sent for Hosted Payment Page (HPP) payments, direct mobile-wallet API purchases, refunds, and purchase reversals. Deliveries that do not get a 2xx response are retried automatically (see Delivery and Retries).
Best Practice: Verify the signature, ignore repeated deliveries using X-Webhook-Event-Id, respond 200 OK quickly, and process the event afterwards.
Note: This page was updated in October 2026 to match current behaviour. If you integrated against an earlier version of this page, read What changed on this page first.
Quick Start
How It Works
- Register a webhook URL → 2. Receive test ping → 3. Transaction reaches a final outcome → 4. Receive webhook → 5. Verify signature → 6. Check
event+payment.status→ 7. Take action → 8. Return 200 OK
Understanding the Payload
Every webhook has two key fields:
event: What happened (authorization,declined,refund,void, orwebhook.test)payment.status: The transaction outcome (APPROVED,DECLINED,FAILED,CANCELLED)
1. Register a Webhook
You can register a default webhook URL to receive all events. Optionally, you can register a separate URL for authorization or refund events using the eventType parameter. A merchant can have at most two webhooks, and only one per event type (the default webhook counts as one).
The URL must be a public HTTPS address with a domain name.
In all API requests, you must include the HPP credentials: merchantUid, storeId, hppKey.
Request:
Request Parameters
| Parameter | Data Type | Presence | Description |
|---|---|---|---|
schemaVersion | String | Constant | API schema version (e.g., "1.0"). |
requestId | UUID String | Required | Unique request identifier (e.g., UUID). |
timestamp | String | Required | Date and time of the request. |
channelName | String | Constant | The channel through which the request was made. |
serviceName | String | Constant | The name of the service being called (e.g., WEBHOOK_REGISTER). |
merchantUid | String | Required | Unique identifier provided by WaafiPay upon merchant account setup. |
storeId | String | Required | API store/user identifier for the merchant. |
hppKey | String | Required | API key for request authentication. |
partnerUid | String | Optional | WAAFI Merchant Number (PartnerUID). |
description | String | Optional | Description of your webhook usage. |
url | String | Required | HTTPS URL the webhooks are posted to. Maximum 255 characters. |
eventType | String | Optional | authorization or refund to send only that event to this URL. Leave empty for the default webhook, which receives every other event. |
Response:
Note: Store your secret securely—do not commit it to source control. It is returned only when the webhook is created, and again only if you later change the URL. This secret is used to verify the authenticity of incoming webhook requests.
Registration fails if the test ping (see Test Your Webhook) does not get an HTTP 200 from your URL, or if a webhook already exists for the same event type.
Registering Event-Specific Webhooks
Each event is sent to the webhook registered for that event name. If there is none, it is sent to the default webhook. If you register only a refund webhook and no default webhook, authorization, declined and void events are not delivered.
Example: Register default webhook (all events)
Example: Register refund-specific webhook
2. Get All Webhooks
Retrieve a list of all existing webhooks. The secret is never returned by this call.
POST Request
Response
3. Update Webhook
You can update the url and description of an existing webhook.
When the URL changes, the new URL receives a test ping and a new secret is generated and returned in params.secret. The old secret stops working immediately.
POST Request
4. Delete Webhook
Remove an existing webhook. Its events are then sent to your default webhook, if you have one; otherwise they are no longer delivered.
POST Request
5. Test Your Webhook
When you register a webhook, or update its URL, WaafiPay sends a test request to validate that your endpoint is reachable and responding correctly. Registration or update fails if the ping does not succeed.
Test Webhook Payload
What to Expect
- User-Agent:
WPNotifyService-Test - Content-Type:
application/json - Timeout: 1 second. Respond within 1 second.
- Expected Response: exactly
HTTP 200. Redirects count as a failure.
Test Event Details
| Field | Description | Example |
|---|---|---|
event | Special test event type | webhook.test |
message | Descriptive message indicating this is a test | WaafiPay webhook validation ping |
merchant_uid | Your merchant UID | M400394 |
Note: Test webhooks (
webhook.test) are not signed and carry none of theX-Webhook-*headers. They are sent only during webhook registration and URL updates. Your webhook handler should recognize this event type and respond with200 OKwithout requiring signature verification.
Example Implementation
Receiving a Webhook on Your Server
Once registered, your webhook URL receives transaction notifications. The payload has a fixed shape:
- The
eventfield tells you what happened (authorization,declined,refund,void) - The
payment.statusfield tells you the transaction outcome (APPROVED,DECLINED,FAILED,CANCELLED)
Note: Optional fields are omitted from the JSON when they have no value rather than sent as
null. Do not fail if an optional field is missing. Unknown fields may be added in the future; ignore what you do not use.
Authorization Transaction Example
Declined Transaction Example (customer cancelled on the payment page)
Refund Transaction Example
Void Transaction Example (purchase reversed)
Webhook Fields Reference
| Field | Description | Example | Presence |
|---|---|---|---|
event | What happened | authorization, declined, refund, void | Always |
merchant_id | Merchant ID (number) | 10165 | Always |
merchant_uid | Merchant UID (string) | M400394 | Always |
user_id | WaafiPay user ID of the account that owns the transaction, as a string | "400394" | Optional |
request_id | Your requestId from the API call that triggered the event | b0c1... | Optional; refunds only |
customer_identity | Masked card number or customer wallet number | 252612345678, 411111******1111 | Optional |
cardholder_name | Customer or cardholder name | John Doe | Optional |
payment.transaction_id | WaafiPay transaction ID, as a string. For refund and void it is the ID of the refund or reversal. "0" when no transaction was created, for example a cancelled payment page or a declined refund. | "1303630" | Always |
payment.original_transaction_id | For refund and void: the transaction that was refunded or reversed | "1303630" | refund, void |
payment.order_id | WaafiPay HPP order ID (number as a string). This is not your reference; see reference_id. | "6482913" | Optional |
payment.transfer_code | Payment provider reference for the transaction | ISR0011303630 | Optional |
payment.amount | Transaction amount (number). For refund it is the refunded amount. | 100.50 | Always |
payment.original_amount | For refund: the amount of the original transaction | 100.50 | refund |
payment.is_partial | For refund: true when the refund is for less than the original amount | true | refund |
payment.currency | ISO currency code | USD, DJF | Always |
payment.payment_method | Payment method used | MWALLET_ACCOUNT, MWALLET_BANKACCOUNT, CREDIT_CARD, DEBIT_CARD | Optional |
payment.fees | Fees charged on the transaction (number). Omitted when zero. | 2.50 | Optional |
payment.status | Transaction outcome | APPROVED, DECLINED, FAILED, CANCELLED | Always |
payment.reference_id | Your reference ID (referenceId you sent when creating the payment or refund) | REF-2024-4567 | Optional |
payment.channel | Channel the payment was made on | WEB (HPP), API | Optional |
payment.description | Transaction description | Payment for order... | Optional |
payment.date | Transaction time, YYYY-MM-DD HH:MM:SS, East Africa Time (UTC+3), no time zone suffix | 2025-08-13 15:04:05 | Always |
payment.token_reference | Rarely present. Can be ignored. | 12345 | Rare |
Understanding Events and Status
Events
| Event | Description | When It's Sent | HMAC Signed |
|---|---|---|---|
webhook.test | Webhook validation test | When you register a webhook or change its URL | No |
authorization | Result of a purchase | When a customer completes a payment via HPP (card or mobile wallet), or when a direct API mobile-wallet purchase is approved | Yes |
declined | A purchase did not complete | When a direct API mobile-wallet purchase is declined by the payment provider (DECLINED), or when the customer cancels on the hosted payment page (CANCELLED) | Yes |
refund | Result of a refund | When a full or partial refund is processed (APPROVED) or declined (DECLINED) | Yes |
void | A purchase was reversed | When an approved purchase is reversed (CANCELLED) | Yes |
Approved payments and processed refunds always produce a webhook. Other outcomes do not always produce one: a decline or a cancellation on the payment page may not send a webhook, and a closed browser, an expired payment link or a timed-out payment page never does. If you have not received an
authorizationevent for an order, query its status withHPP_GETTRANINFO(see HPP API). New event names may be added in the future. Return200 OKfor any event you do not recognise.
Status Values (Transaction Outcomes)
The payment.status field tells you what happened with the transaction:
| Status | Meaning | Events | What To Do |
|---|---|---|---|
APPROVED | Transaction succeeded | authorization, refund | Fulfil order / record the refund |
DECLINED | Declined by the payment provider or bank; or refund declined | authorization, declined, refund | Cancel order and notify customer / log refund failure |
FAILED | Transaction failed due to a technical error | authorization | Cancel order; log and contact support if it repeats |
CANCELLED | Customer cancelled on the payment page (declined), or an approved purchase was reversed (void) | declined, void | Cancel order / release goods and record the reversal |
Spelling and case. The status is
CANCELLEDwith two Ls. Compare status values case-insensitively. Treat anything other thanAPPROVEDas not approved.
Handling Webhooks
Always check both
eventandpayment.statusto determine the correct action, and key your own records onpayment.reference_id(your reference) orpayment.transaction_id, not onpayment.order_id.
Delivery and Retries
- Success is any
2xxresponse. Any other response, a connection failure or a timeout counts as a failed attempt. - Timeouts: 5 seconds to connect, 10 seconds to start responding, 30 seconds in total.
- Retries: after a failed attempt we retry about 5 minutes later, then with increasing delays of up to 1 hour, up to 8 retries over about 2.5 to 5 hours. Retry timing is approximate. Every retry carries the same
X-Webhook-Event-Idwith a newX-Webhook-TimestampandX-Webhook-Signature. After the last retry, delivery stops; contact support to have an event re-sent. - Events are not guaranteed to arrive in order. For example, a
refundmay arrive before a retriedauthorization.
Security: HMAC Signature Verification
All webhooks (except webhook.test) include an HMAC-SHA256 signature for verification. You must verify this signature to prevent fake webhook attacks.
How It Works
Signing String Format: {timestamp}.{event_id}.{raw_body_bytes}
- We compute HMAC-SHA256 of the signing string using your secret key
- We send the signature in the
X-Webhook-Signatureheader - You recompute the signature and compare it
Headers We Send
| Header | Purpose |
|---|---|
User-Agent: WPNotifyService | Identifies the source of the webhook request |
Content-Type: application/json | Indicates the media type of the resource |
X-Webhook-Timestamp | Unix timestamp (seconds) of this delivery attempt. Use it to reject old requests. |
X-Webhook-Event-Id | Unique per event and the same on every retry. Use it to ignore repeated deliveries. |
X-Webhook-Signature-Alg | Always HMAC-SHA256 |
X-Webhook-Signature | Lowercase hex digest of HMAC-SHA256 over the signing string |
X-Trace-ID | Optional delivery identifier. Include it when you report a delivery problem to support. |
Verification Steps
- Capture raw body before JSON parsing
- Build signing string:
{timestamp}.{event_id}.{raw_body_bytes} - Compute HMAC-SHA256 using your secret key
- Compare signatures using constant-time comparison
- Validate timestamp (reject if >5 minutes old; retries always carry a new timestamp)
- Check event ID (if already processed, return
200 OKwithout processing again) - Return 200 OK quickly (process the event afterwards if needed)
Critical: Always use the raw HTTP body bytes, not parsed JSON. Any whitespace changes will break verification.
Example Webhooks
Example 1: Authorization Webhook
Headers
Body
Example 2: Refund Webhook
Headers
Body
Implementation Guide
Complete Example
Here's a production-ready webhook handler:
Best Practices
- Use HTTPS: Webhook URL must use HTTPS on a domain name
- Store secret securely: Use environment variables, never commit to code
- Return 200 quickly: Respond within a few seconds and process the event afterwards
- Log everything: Keep webhook logs, including
X-Webhook-Event-IdandX-Trace-ID - Prevent duplicate processing: Use event IDs; retries reuse the same ID
- Handle test events: Respond to
webhook.testwithin 1 second without signature verification - Ignore what you do not recognise: Ignore unknown events and fields, and do not fail when an optional field is missing
- Do not rely on webhooks alone: Query
HPP_GETTRANINFOfor orders that never received an event
Common Issues
| Issue | Solution |
|---|---|
| Signature verification fails | Capture raw HTTP body before parsing JSON |
| Webhook registration fails | Your URL must answer the webhook.test ping with HTTP 200 within 1 second, over HTTPS, without a redirect |
| Webhooks not received | Ensure URL is publicly accessible; check that a default webhook exists for events without a dedicated URL |
| Duplicate processing | Track event IDs using X-Webhook-Event-Id |
| Timeout errors | Return 200 OK immediately and process the event afterwards |
| Replay attacks | Validate timestamp (reject if >5 minutes old) |
| No webhook for an abandoned payment | Expected; query HPP_GETTRANINFO |
What changed on this page
The previous version of this page described some values that WaafiPay never sent. The behaviour has not changed for the items below; the documentation was corrected.
| Topic | Previously documented | Actual behaviour |
|---|---|---|
| Cancelled status | CANCELED | CANCELLED (two Ls). WaafiPay has always sent CANCELLED. |
EXPIRED and TIMEOUT statuses | Listed as webhook statuses | Never sent. Expired or timed-out payments produce no webhook; query HPP_GETTRANINFO. |
| Events | authorization, refund only | declined and void are also sent (see Events). Unknown events must be acknowledged with 200 OK. |
| Retries | "Failed deliveries are not automatically retried" | Failed deliveries are retried up to 8 times over several hours with the same X-Webhook-Event-Id. |
| Test ping timeout | 5 seconds | 1 second |
payment.order_id | Shown as your order reference | WaafiPay HPP order ID. Your reference is payment.reference_id. |
| Refund fields | Not documented | original_transaction_id, original_amount, is_partial |
| Update webhook | Only url/description | Changing the URL also rotates the secret, which is returned in the response |
