Webhooks
Configure signed webhook delivery for challan search and payment-link events.
What webhooks are for
Webhooks let your integration receive lifecycle updates without polling every workflow step. They are particularly useful for:
- challan search completion or failure monitoring
- payment-link creation notifications
- payment success, failure, and expiry handling
Configure your webhook
From the organization API settings area, your admin can:
- Save a webhook URL.
- Activate or deactivate delivery.
- Rotate the webhook secret.
- Send a test event.
For the testing replica, the same workflow is exposed through staging-only API routes documented in the Testing guide.
Store the webhook secret securely
When the webhook secret is rotated, update your receiver before relying on new deliveries. Use the latest secret to verify every signed payload.
Webhook endpoint requirements
Staging webhook URLs must be public HTTPS endpoints. Localhost, private or link-local addresses, and redirecting callback URLs are rejected by the staging API.
Testing lifecycle behavior
In the staging replica, a normal payment-link flow emits
payment_link.created and then payment_link.paid almost immediately. Use
the testing-only reserved RC numbers if you want deterministic
payment_link.failed or payment_link.expired webhook branches instead.
Delivery headers
| Field | Type | Required | Description |
|---|---|---|---|
| content-type | string | Required | Always sent as application/json. |
| x-cms-event | string | Required | Event type, such as payment_link.paid or challan_search.completed. |
| x-cms-timestamp | string | Required | Unix timestamp used in signature verification. |
| x-cms-signature | string | Required | HMAC SHA-256 signature in the format sha256=HEX_DIGEST. |
Payload envelope
Every webhook body uses the same top-level shape:
{"id": "a8e97bbf-0ba5-4ea7-b7cb-11b29efdf641","type": "payment_link.paid","createdAt": "2026-05-13T11:42:08.000Z","data": { "...": "event-specific fields"}}Verify the signature
Use the raw request body exactly as received and compute the signature over:
${x-cms-timestamp}.${rawRequestBody}Example in Node.js:
import crypto from 'node:crypto';
function verifyWebhook({
timestamp,
rawBody,
signatureHeader,
secret,
}: {
timestamp: string;
rawBody: string;
signatureHeader: string;
secret: string;
}) {
const expected = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
const received = signatureHeader.replace(/^sha256=/, '');
return crypto.timingSafeEqual(
Buffer.from(expected, 'hex'),
Buffer.from(received, 'hex'),
);
}Supported event types
| Status | Meaning | Notes |
|---|---|---|
| webhook.test | Test event queued from the CMS settings screen. | Use this to verify connectivity and signature handling. |
| challan_search.completed | A challan search completed successfully. | Includes rcNumber, challanCount, and newChallansCount. |
| challan_search.failed | A challan search failed. | Includes rcNumber, statusCode, and errorMessage. |
| payment_link.created | A payment link was created. | Includes paymentLinkId, vehicleNumber, amount summary, and expiresAt. |
| payment_link.paid | A payment link was paid successfully. | Includes paymentLinkId, transactionId, paymentMode, and amount. |
| payment_link.failed | A payment attempt failed. | Includes paymentLinkId, orderId, and errorMessage. |
| payment_link.expired | A payment link expired before payment. | Includes paymentLinkId and expiredAt metadata. |
Example event payloads
{"id": "8803f57d-6814-41e5-a3da-97f349acf698","type": "webhook.test","createdAt": "2026-05-13T09:02:21.000Z","data": { "orgId": "bf4cc07b-d395-456a-8aac-e419d9ef10ff", "sentAt": "2026-05-13T09:02:21.000Z"}}{"id": "418b94b6-74e8-4d82-a474-773e3aeae0c6","type": "challan_search.completed","createdAt": "2026-05-13T09:10:11.000Z","data": { "rcNumber": "DL01AB1234", "apiKeyId": "4e4984c0-16c2-4b33-b831-fcbf5485f95d", "challanCount": 2, "newChallansCount": 2}}{"id": "e398c607-d52d-4828-9755-9351fef99871","type": "payment_link.created","createdAt": "2026-05-13T10:20:00.000Z","data": { "paymentLinkId": "17de2557-58d6-4f8d-aa2b-fb60074c6a6d", "vehicleNumber": "DL01AB1234", "status": "ACTIVE", "challanCount": 2, "amount": { "baseAmount": 1500, "legalFee": 100, "gst": 18, "convenienceFee": 0, "totalAmount": 1618 }, "expiresAt": "2026-05-15T14:20:00.000Z"}}{"id": "b58bc6f3-8384-4db9-aa7b-40c418ccb4b1","type": "payment_link.paid","createdAt": "2026-05-13T11:42:08.000Z","data": { "paymentLinkId": "17de2557-58d6-4f8d-aa2b-fb60074c6a6d", "status": "PAID", "orderId": "ORD-20260513-0011", "transactionId": "TXN-845012193", "paymentMode": "UPI", "amount": 1618}}Delivery expectations
- Treat any non-2xx response as a failed delivery attempt.
- Do not rely on redirects during webhook delivery; the staging sender will not follow them.
- Validate the signature before trusting the payload.
- Store webhook event IDs if you want an idempotency layer in your own system.
- In testing, expect final payment events to arrive very quickly after
payment_link.created.
