8. Webhooks
8.1 Providers
Only the following provider names are accepted in the path (case-insensitive):
| Path segment | Kind | Accepted | Signature header | Algorithm and encoding |
|---|---|---|---|---|
flutterwave | Payments | Yes | flutterwave-signature | HMAC-SHA256 over the raw body, base64 |
paystack | Payments | Yes | x-paystack-signature | HMAC-SHA512 over the raw body, hex |
fincra | Payments | Yes | signature | HMAC-SHA512, hex |
squad | Payments | Yes | x-squad-signature | HMAC-SHA512, hex |
termii | SMS status | Verified, then always 503 | x-termii-signature | HMAC-SHA512, hex |
twilio | SMS status | Verified, then always 503 | x-twilio-signature | HMAC-SHA1 of URL, parameters and content type, base64 (constant-time compare) |
Any other provider name returns 404 NOT_FOUND.
Signature comparison is constant-time (constantTimeEqual). A missing signature fails verification.
SMS status callbacks (termii, twilio) are verified and then rejected with 503 FEATURE_UNAVAILABLE because a durable tenant-bound dispatch record is not implemented. Invalid signatures on those providers still return 401.
8.2 Processing flow for payment webhooks
- Route reads the raw body (max 1 MiB) and sets
x-dffms-webhook-urlto the request URL (used by Twilio verification). - Provider adapter verifies the signature. Failure: 401
INVALID_WEBHOOK. - The adapter extracts the provider
eventIdandreference. Missing reference: 401. - The transaction is looked up by
reference. Zero or several matches: 401INVALID_WEBHOOK("unknown or ambiguous"). - An inbox row is inserted with unique key
(tenant, provider, providerEventId); a conflict meansduplicate: trueand no further work. - An outbox job
payment.webhook.receivedis written. - The response returns 200 immediately.
after()then processes up to 5 outbox jobs for that tenant. - The worker re-verifies the payment with the provider API (
verifyPayment,verifyPayoutorverifyRefund), checks amount and currency against the stored transaction, and only then posts ledger entries. Provider redirects or webhook bodies alone never move money.
8.3 Webhook responses
{
"data": { "accepted": true, "duplicate": false, "eventId": "<provider-event-id>" },
"error": null,
"requestId": "<request-id>"
}