Webhooks
Receive signed payment results, verify them, and handle retries.
Configure an endpoint
Open Developers → Webhooks in the merchant dashboard. Enter a public HTTPS URL on port 443, then verify your current password. The URL must be no more than 2048 bytes. URLs cannot contain credentials, a query string, or a fragment.
Save the signing secret when it appears. It is different from your API key. Your receiver must have the secret before processing events.
Private destinations, unsafe DNS results, and redirects are rejected by the sender.
Events
| Event type | Meaning |
|---|---|
collection.succeeded | Collection succeeded |
collection.failed | Collection failed |
disbursement.succeeded | Payout succeeded |
disbursement.failed | Payout failed |
Only terminal results produce payment events. The envelope contains id, type, created_at, and data, which is the payment object.
{
"id": "507f1f77bcf86cd799439011:succeeded",
"type": "collection.succeeded",
"created_at": "2026-09-05T12:00:10Z",
"data": {
"id": "507f1f77bcf86cd799439011",
"kind": "collection",
"method": "stk",
"status": "succeeded",
"amount_minor": 10000,
"currency": "KES",
"fee_minor": 1000,
"provider_fee_minor": 0,
"phone": "254712345678",
"reference": "ORDER-000123",
"shortcode": "600000",
"receipt": "EXAMPLE123",
"created_at": "2026-09-05T12:00:00Z",
"updated_at": "2026-09-05T12:00:10Z"
}
}Amounts, fees, shortcode, and receipt above are examples.
Signature headers
| Header | Value |
|---|---|
X-Deripay-Event-ID | Stable event identifier |
X-Deripay-Timestamp | Unix timestamp in seconds, generated for that delivery attempt |
X-Deripay-Signature | v1= followed by the hexadecimal HMAC-SHA256 digest |
The signed bytes are timestamp + "." + rawBody. Read the body as bytes before parsing JSON. Re-serializing JSON changes the signature input.
Verify on your server
The following helper uses Node.js built-ins. Reject signatures with timestamps more than five minutes from your server clock.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyWebhook(rawBody, timestamp, signature, secrets) {
if (!Buffer.isBuffer(rawBody)) return false;
if (!/^\d{1,12}$/.test(timestamp ?? "")) return false;
if (!/^v1=[a-f0-9]{64}$/.test(signature ?? "")) return false;
const seconds = Number(timestamp);
if (Math.abs(Date.now() / 1000 - seconds) > 300) return false;
const received = Buffer.from(signature.slice(3), "hex");
return secrets.filter(Boolean).some((secret) => {
const expected = createHmac("sha256", secret)
.update(timestamp + ".")
.update(rawBody)
.digest();
return timingSafeEqual(expected, received);
});
}After verification, parse the body. Verify the envelope's id matches X-Deripay-Event-ID. Store and deduplicate that signed body ID, not an unverified header. Validate the event type and payment data before using them.
Acknowledge durably
Persist the event, or durably enqueue it, before returning a 2xx response. Use a unique constraint on the event ID so duplicates do not fulfil an order twice.
The sender sends an HTTP POST with Content-Type: application/json. It waits up to 8 seconds for response headers and has a 12-second overall request timeout. Acknowledge promptly after durable storage; process business actions asynchronously.
The sender treats non-2xx responses and timeouts as failed attempts. It uses automatic retries, up to 12 attempts per delivery cycle. Administrative replay can start another cycle. Do not assume delivery order or exactly-once transport.
Retries are scheduled after 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, and 1024 seconds following failed attempts. Worker scheduling and outages can delay actual delivery. An HTTP Retry-After returned by your receiver does not change this schedule.
| Delivery state | Meaning |
|---|---|
pending | Waiting for an attempt or retry |
processing | A worker has claimed the delivery |
delivered | Receiver returned 2xx |
dead | Automatic attempts exhausted; administrative review is required |
disabled | No endpoint or signing secret was configured when the event was recorded |
These are delivery states, separate from the payment status. A failed webhook does not mean the payment failed.
Rotate or disable
Updating an existing URL without requesting rotation retains its signing secret. First-time configuration or explicit rotation generates a secret shown once. Generating a new signing secret applies to future events. Already queued events retain their original encrypted signing secret and endpoint URL. Keep the previous secret available for verification until those deliveries finish.
Clearing the endpoint disables new notifications. It does not cancel existing queued deliveries. Events recorded while no endpoint was configured appear as Not configured in delivery history and are not automatically backfilled when you add an endpoint.
Troubleshoot delivery
Check Developers → Webhooks → Delivery history for the event ID, state, and attempt count. Inspect receiver logs for that event ID without logging keys or signing secrets. A final failed delivery needs administrative review; polling the payment remains available.