Core concepts
Webhooks
When something happens to your company's data, Wathiqaty sends a signed HTTPS POST to your endpoint. Register endpoints with Webhook endpoints and choose the event types each one receives.
Webhooks tell you that something changed. Delivery is at-least-once and not ordered. Deduplicate by event id, and keep a reconciliation job on the change feed as a safety net.
Event types#
action needed marks events that wait for your decision. Click an event to see its body.
ping
Test from POST /partner/webhook-endpoints/{id}/test.
{
"id": "evt_7f6c1b0e-3c2d-4c84-9a0e-2f3b1c5d7e90",
"type": "ping",
"createdAt": "2026-11-05T09:15:02.481Z",
"environment": "sandbox",
"apiVersion": "v1",
"data": {}
}policy.issued
A payment was captured and the policy issued. Book it. (action needed)
{
"id": "evt_7f6c1b0e-3c2d-4c84-9a0e-2f3b1c5d7e90",
"type": "policy.issued",
"createdAt": "2026-11-05T09:15:02.481Z",
"environment": "sandbox",
"apiVersion": "v1",
"data": {
"id": "6f1c2d3e-4b5a-4c69-8d7e-1f2a3b4c5d6e",
"number": "POL-00000042",
"externalReference": "MI/2026/MED/004213",
"packageId": "5e2b8c41-9d3a-4f67-b1c0-7a8e2d4f6b13",
"packageExternalReference": "PRD-GOLD-2026",
"packageName": {
"en": "Gold Care",
"ar": "جولد كير"
},
"insuranceType": "individual_medical",
"status": "active",
"coverageStartsAt": "2026-11-05T09:15:00.000Z",
"coverageEndsAt": "2027-11-05T09:15:00.000Z",
"cancelledAt": null,
"sumInsured": "250000.00",
"premium": {
"net": "8333.33",
"taxes": [
{
"name": "VAT",
"rate": "14.00",
"amount": "1166.67"
}
],
"total": "9500.00",
"currency": "EGP"
},
"commission": {
"rate": "10.00",
"amount": "833.33"
},
"customer": {
"name": "Mona Adel",
"email": "mona@example.com",
"phone": "01012345678",
"nationalId": "29001011234567",
"birthDate": "1990-01-01",
"gender": "female"
},
"kyc": {
"provider": "lumin",
"verifiedId": "lm_7c1d9e2f",
"verifiedAt": "2026-11-05T09:02:11.000Z"
},
"vehicle": null,
"cardNumber": "MC-2026-42",
"cancellationScale": {
"effectiveFrom": "2026-10-01",
"tiers": [
{
"fromDay": 0,
"toDay": 14,
"refundPercent": "90.00",
"basis": "flat"
},
{
"fromDay": 15,
"toDay": 365,
"refundPercent": "80.00",
"basis": "pro_rata"
}
]
},
"createdAt": "2026-11-05T09:15:05.000Z",
"updatedAt": "2026-11-05T09:16:40.000Z"
}
}policy.updated
Your reference was set, or the customer signed the e-contract.
{
"id": "evt_7f6c1b0e-3c2d-4c84-9a0e-2f3b1c5d7e90",
"type": "policy.updated",
"createdAt": "2026-11-05T09:15:02.481Z",
"environment": "sandbox",
"apiVersion": "v1",
"data": {
"id": "6f1c2d3e-4b5a-4c69-8d7e-1f2a3b4c5d6e",
"number": "POL-00000042",
"externalReference": "MI/2026/MED/004213",
"packageId": "5e2b8c41-9d3a-4f67-b1c0-7a8e2d4f6b13",
"packageExternalReference": "PRD-GOLD-2026",
"packageName": {
"en": "Gold Care",
"ar": "جولد كير"
},
"insuranceType": "individual_medical",
"status": "active",
"coverageStartsAt": "2026-11-05T09:15:00.000Z",
"coverageEndsAt": "2027-11-05T09:15:00.000Z",
"cancelledAt": null,
"sumInsured": "250000.00",
"premium": {
"net": "8333.33",
"taxes": [
{
"name": "VAT",
"rate": "14.00",
"amount": "1166.67"
}
],
"total": "9500.00",
"currency": "EGP"
},
"commission": {
"rate": "10.00",
"amount": "833.33"
},
"customer": {
"name": "Mona Adel",
"email": "mona@example.com",
"phone": "01012345678",
"nationalId": "29001011234567",
"birthDate": "1990-01-01",
"gender": "female"
},
"kyc": {
"provider": "lumin",
"verifiedId": "lm_7c1d9e2f",
"verifiedAt": "2026-11-05T09:02:11.000Z"
},
"vehicle": null,
"cardNumber": "MC-2026-42",
"cancellationScale": {
"effectiveFrom": "2026-10-01",
"tiers": [
{
"fromDay": 0,
"toDay": 14,
"refundPercent": "90.00",
"basis": "flat"
},
{
"fromDay": 15,
"toDay": 365,
"refundPercent": "80.00",
"basis": "pro_rata"
}
]
},
"createdAt": "2026-11-05T09:15:05.000Z",
"updatedAt": "2026-11-05T09:16:40.000Z"
}
}policy.signature_pending
The customer hasn't signed the e-contract yet (reminder sent).
{
"id": "evt_7f6c1b0e-3c2d-4c84-9a0e-2f3b1c5d7e90",
"type": "policy.signature_pending",
"createdAt": "2026-11-05T09:15:02.481Z",
"environment": "sandbox",
"apiVersion": "v1",
"data": {
"policyId": "6f1c2d3e-4b5a-4c69-8d7e-1f2a3b4c5d6e",
"remindedAt": "2026-11-06T09:00:00.000Z"
}
}policy.expired
The cover ended.
{
"id": "evt_7f6c1b0e-3c2d-4c84-9a0e-2f3b1c5d7e90",
"type": "policy.expired",
"createdAt": "2026-11-05T09:15:02.481Z",
"environment": "sandbox",
"apiVersion": "v1",
"data": {
"policyId": "6f1c2d3e-4b5a-4c69-8d7e-1f2a3b4c5d6e",
"coverageEndsAt": "2027-11-05T09:15:00.000Z"
}
}policy.cancelled
The policy was cancelled, for any reason.
{
"id": "evt_7f6c1b0e-3c2d-4c84-9a0e-2f3b1c5d7e90",
"type": "policy.cancelled",
"createdAt": "2026-11-05T09:15:02.481Z",
"environment": "sandbox",
"apiVersion": "v1",
"data": {
"policyId": "6f1c2d3e-4b5a-4c69-8d7e-1f2a3b4c5d6e",
"cancelledAt": "2026-11-22T10:00:00.000Z",
"reason": "cancellation_completed",
"cancellationId": "b7d4e1a9-2c3f-4a58-9e6b-0d1c2b3a4f5e"
}
}cancellation.requested
The customer asked to cancel. Decide. (action needed)
{
"id": "evt_7f6c1b0e-3c2d-4c84-9a0e-2f3b1c5d7e90",
"type": "cancellation.requested",
"createdAt": "2026-11-05T09:15:02.481Z",
"environment": "sandbox",
"apiVersion": "v1",
"data": {
"id": "b7d4e1a9-2c3f-4a58-9e6b-0d1c2b3a4f5e",
"policyId": "6f1c2d3e-4b5a-4c69-8d7e-1f2a3b4c5d6e",
"status": "requested",
"initiatedBy": "customer",
"reason": "Moved to my employer's group cover",
"requestedAt": "2026-11-20T11:00:00.000Z",
"refundQuote": {
"daysElapsed": 15,
"tier": {
"fromDay": 15,
"toDay": 365,
"refundPercent": "80.00",
"basis": "pro_rata"
},
"refundAmount": "6202.74",
"taxesRetained": "1166.67",
"commissionReversal": "620.27"
},
"decidedAt": null,
"completedAt": null,
"updatedAt": "2026-11-20T11:00:00.000Z"
}
}cancellation.withdrawn
The customer withdrew the request.
{
"id": "evt_7f6c1b0e-3c2d-4c84-9a0e-2f3b1c5d7e90",
"type": "cancellation.withdrawn",
"createdAt": "2026-11-05T09:15:02.481Z",
"environment": "sandbox",
"apiVersion": "v1",
"data": {
"id": "b7d4e1a9-2c3f-4a58-9e6b-0d1c2b3a4f5e",
"policyId": "6f1c2d3e-4b5a-4c69-8d7e-1f2a3b4c5d6e",
"status": "withdrawn",
"initiatedBy": "customer",
"reason": "Moved to my employer's group cover",
"requestedAt": "2026-11-20T11:00:00.000Z",
"refundQuote": {
"daysElapsed": 15,
"tier": {
"fromDay": 15,
"toDay": 365,
"refundPercent": "80.00",
"basis": "pro_rata"
},
"refundAmount": "6202.74",
"taxesRetained": "1166.67",
"commissionReversal": "620.27"
},
"decidedAt": null,
"completedAt": null,
"updatedAt": "2026-11-20T11:00:00.000Z"
}
}cancellation.approved
Approved (by you, your staff, or automatically).
{
"id": "evt_7f6c1b0e-3c2d-4c84-9a0e-2f3b1c5d7e90",
"type": "cancellation.approved",
"createdAt": "2026-11-05T09:15:02.481Z",
"environment": "sandbox",
"apiVersion": "v1",
"data": {
"id": "b7d4e1a9-2c3f-4a58-9e6b-0d1c2b3a4f5e",
"policyId": "6f1c2d3e-4b5a-4c69-8d7e-1f2a3b4c5d6e",
"status": "refund_pending",
"initiatedBy": "customer",
"reason": "Moved to my employer's group cover",
"requestedAt": "2026-11-20T11:00:00.000Z",
"refundQuote": {
"daysElapsed": 15,
"tier": {
"fromDay": 15,
"toDay": 365,
"refundPercent": "80.00",
"basis": "pro_rata"
},
"refundAmount": "6202.74",
"taxesRetained": "1166.67",
"commissionReversal": "620.27"
},
"decidedAt": null,
"completedAt": null,
"updatedAt": "2026-11-20T11:00:00.000Z"
}
}cancellation.rejected
Rejected.
{
"id": "evt_7f6c1b0e-3c2d-4c84-9a0e-2f3b1c5d7e90",
"type": "cancellation.rejected",
"createdAt": "2026-11-05T09:15:02.481Z",
"environment": "sandbox",
"apiVersion": "v1",
"data": {
"id": "b7d4e1a9-2c3f-4a58-9e6b-0d1c2b3a4f5e",
"policyId": "6f1c2d3e-4b5a-4c69-8d7e-1f2a3b4c5d6e",
"status": "rejected",
"initiatedBy": "customer",
"reason": "Moved to my employer's group cover",
"requestedAt": "2026-11-20T11:00:00.000Z",
"refundQuote": {
"daysElapsed": 15,
"tier": {
"fromDay": 15,
"toDay": 365,
"refundPercent": "80.00",
"basis": "pro_rata"
},
"refundAmount": "6202.74",
"taxesRetained": "1166.67",
"commissionReversal": "620.27"
},
"decidedAt": null,
"completedAt": null,
"updatedAt": "2026-11-20T11:00:00.000Z"
}
}cancellation.completed
The refund is done; the policy is cancelled.
{
"id": "evt_7f6c1b0e-3c2d-4c84-9a0e-2f3b1c5d7e90",
"type": "cancellation.completed",
"createdAt": "2026-11-05T09:15:02.481Z",
"environment": "sandbox",
"apiVersion": "v1",
"data": {
"id": "b7d4e1a9-2c3f-4a58-9e6b-0d1c2b3a4f5e",
"policyId": "6f1c2d3e-4b5a-4c69-8d7e-1f2a3b4c5d6e",
"status": "completed",
"initiatedBy": "customer",
"reason": "Moved to my employer's group cover",
"requestedAt": "2026-11-20T11:00:00.000Z",
"refundQuote": {
"daysElapsed": 15,
"tier": {
"fromDay": 15,
"toDay": 365,
"refundPercent": "80.00",
"basis": "pro_rata"
},
"refundAmount": "6202.74",
"taxesRetained": "1166.67",
"commissionReversal": "620.27"
},
"decidedAt": null,
"completedAt": null,
"updatedAt": "2026-11-20T11:00:00.000Z"
}
}payment.refunded
A refund reached the customer.
{
"id": "evt_7f6c1b0e-3c2d-4c84-9a0e-2f3b1c5d7e90",
"type": "payment.refunded",
"createdAt": "2026-11-05T09:15:02.481Z",
"environment": "sandbox",
"apiVersion": "v1",
"data": {
"paymentId": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"policyId": "6f1c2d3e-4b5a-4c69-8d7e-1f2a3b4c5d6e",
"amount": "6202.74",
"refundedAt": "2026-11-22T10:00:00.000Z"
}
}payment.reversed
A captured payment was voided or reversed at the gateway; its policy is cancelled.
{
"id": "evt_7f6c1b0e-3c2d-4c84-9a0e-2f3b1c5d7e90",
"type": "payment.reversed",
"createdAt": "2026-11-05T09:15:02.481Z",
"environment": "sandbox",
"apiVersion": "v1",
"data": {
"paymentId": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"policyId": "6f1c2d3e-4b5a-4c69-8d7e-1f2a3b4c5d6e"
}
}claim.submitted
A customer submitted a claim. Review it. (action needed)
{
"id": "evt_7f6c1b0e-3c2d-4c84-9a0e-2f3b1c5d7e90",
"type": "claim.submitted",
"createdAt": "2026-11-05T09:15:02.481Z",
"environment": "sandbox",
"apiVersion": "v1",
"data": {
"id": "c9e8d7f6-1a2b-4c3d-8e4f-5a6b7c8d9e0f",
"number": "CLM-00000107",
"externalReference": null,
"policyId": "6f1c2d3e-4b5a-4c69-8d7e-1f2a3b4c5d6e",
"categoryId": "4c5d6e7f-8a9b-4c0d-9e1f-2a3b4c5d6e7f",
"status": "submitted",
"incidentDate": "2026-12-01",
"claimedAmount": "4200.00",
"approvedAmount": null,
"items": [
{
"lineId": "l1",
"description": "Outpatient consultation",
"claimedAmount": "700.00"
},
{
"lineId": "l2",
"description": "MRI scan",
"claimedAmount": "3500.00"
}
],
"documents": [
{
"id": "d1e2f3a4-b5c6-4d7e-8f9a-0b1c2d3e4f5a",
"requirementKey": "medical_report",
"status": "pending",
"fileName": "report.pdf",
"url": "https://files.wathiqaty.com/claims/report.pdf"
}
],
"documentRequests": [],
"policyCoverage": {
"sumInsured": "250000.00",
"consumed": "0.00",
"remaining": "250000.00"
},
"submittedAt": "2026-12-02T08:30:00.000Z",
"updatedAt": "2026-12-02T08:30:00.000Z"
}
}claim.resubmitted
The customer answered your document request. (action needed)
{
"id": "evt_7f6c1b0e-3c2d-4c84-9a0e-2f3b1c5d7e90",
"type": "claim.resubmitted",
"createdAt": "2026-11-05T09:15:02.481Z",
"environment": "sandbox",
"apiVersion": "v1",
"data": {
"id": "c9e8d7f6-1a2b-4c3d-8e4f-5a6b7c8d9e0f",
"number": "CLM-00000107",
"externalReference": null,
"policyId": "6f1c2d3e-4b5a-4c69-8d7e-1f2a3b4c5d6e",
"categoryId": "4c5d6e7f-8a9b-4c0d-9e1f-2a3b4c5d6e7f",
"status": "under_review",
"incidentDate": "2026-12-01",
"claimedAmount": "4200.00",
"approvedAmount": null,
"items": [
{
"lineId": "l1",
"description": "Outpatient consultation",
"claimedAmount": "700.00"
},
{
"lineId": "l2",
"description": "MRI scan",
"claimedAmount": "3500.00"
}
],
"documents": [
{
"id": "d1e2f3a4-b5c6-4d7e-8f9a-0b1c2d3e4f5a",
"requirementKey": "medical_report",
"status": "pending",
"fileName": "report.pdf",
"url": "https://files.wathiqaty.com/claims/report.pdf"
}
],
"documentRequests": [],
"policyCoverage": {
"sumInsured": "250000.00",
"consumed": "0.00",
"remaining": "250000.00"
},
"submittedAt": "2026-12-02T08:30:00.000Z",
"updatedAt": "2026-12-02T08:30:00.000Z"
}
}claim.status_changed
Any other claim move.
{
"id": "evt_7f6c1b0e-3c2d-4c84-9a0e-2f3b1c5d7e90",
"type": "claim.status_changed",
"createdAt": "2026-11-05T09:15:02.481Z",
"environment": "sandbox",
"apiVersion": "v1",
"data": {
"claimId": "c9e8d7f6-1a2b-4c3d-8e4f-5a6b7c8d9e0f",
"from": "under_review",
"to": "approved",
"actor": "insurer_staff"
}
}claim.message_received
The customer wrote on a claim.
{
"id": "evt_7f6c1b0e-3c2d-4c84-9a0e-2f3b1c5d7e90",
"type": "claim.message_received",
"createdAt": "2026-11-05T09:15:02.481Z",
"environment": "sandbox",
"apiVersion": "v1",
"data": {
"claimId": "c9e8d7f6-1a2b-4c3d-8e4f-5a6b7c8d9e0f",
"messageId": "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e"
}
}settlement.requested
Wathiqaty requested commission payment. Pay and upload proof. (action needed)
{
"id": "evt_7f6c1b0e-3c2d-4c84-9a0e-2f3b1c5d7e90",
"type": "settlement.requested",
"createdAt": "2026-11-05T09:15:02.481Z",
"environment": "sandbox",
"apiVersion": "v1",
"data": {
"id": "e4f5a6b7-c8d9-4e0f-9a1b-2c3d4e5f6a7b",
"status": "requested",
"periodFrom": "2026-11-01",
"periodTo": "2026-11-30",
"amount": "184500.00",
"platformBankAccount": {
"bank": "National Bank of Egypt",
"iban": "EG380019000500000000263180002"
},
"proof": null,
"requestedAt": "2026-12-01T07:00:00.000Z",
"updatedAt": "2026-12-01T07:00:00.000Z"
}
}settlement.confirmed
Wathiqaty confirmed your payment.
{
"id": "evt_7f6c1b0e-3c2d-4c84-9a0e-2f3b1c5d7e90",
"type": "settlement.confirmed",
"createdAt": "2026-11-05T09:15:02.481Z",
"environment": "sandbox",
"apiVersion": "v1",
"data": {
"id": "e4f5a6b7-c8d9-4e0f-9a1b-2c3d4e5f6a7b",
"status": "confirmed",
"periodFrom": "2026-11-01",
"periodTo": "2026-11-30",
"amount": "184500.00",
"platformBankAccount": {
"bank": "National Bank of Egypt",
"iban": "EG380019000500000000263180002"
},
"proof": null,
"requestedAt": "2026-12-01T07:00:00.000Z",
"updatedAt": "2026-12-01T07:00:00.000Z"
}
}settlement.rejected
Your proof was rejected (rejectionReason). Submit again.
{
"id": "evt_7f6c1b0e-3c2d-4c84-9a0e-2f3b1c5d7e90",
"type": "settlement.rejected",
"createdAt": "2026-11-05T09:15:02.481Z",
"environment": "sandbox",
"apiVersion": "v1",
"data": {
"id": "e4f5a6b7-c8d9-4e0f-9a1b-2c3d4e5f6a7b",
"status": "rejected",
"periodFrom": "2026-11-01",
"periodTo": "2026-11-30",
"amount": "184500.00",
"platformBankAccount": {
"bank": "National Bank of Egypt",
"iban": "EG380019000500000000263180002"
},
"proof": null,
"requestedAt": "2026-12-01T07:00:00.000Z",
"updatedAt": "2026-12-01T07:00:00.000Z",
"rejectionReason": "Amount doesn't match"
}
}settlement.cancelled
Wathiqaty cancelled the request.
{
"id": "evt_7f6c1b0e-3c2d-4c84-9a0e-2f3b1c5d7e90",
"type": "settlement.cancelled",
"createdAt": "2026-11-05T09:15:02.481Z",
"environment": "sandbox",
"apiVersion": "v1",
"data": {
"id": "e4f5a6b7-c8d9-4e0f-9a1b-2c3d4e5f6a7b",
"status": "cancelled",
"periodFrom": "2026-11-01",
"periodTo": "2026-11-30",
"amount": "184500.00",
"platformBankAccount": {
"bank": "National Bank of Egypt",
"iban": "EG380019000500000000263180002"
},
"proof": null,
"requestedAt": "2026-12-01T07:00:00.000Z",
"updatedAt": "2026-12-01T07:00:00.000Z"
}
}package.updated
A package changed, in the portal or through the API.
{
"id": "evt_7f6c1b0e-3c2d-4c84-9a0e-2f3b1c5d7e90",
"type": "package.updated",
"createdAt": "2026-11-05T09:15:02.481Z",
"environment": "sandbox",
"apiVersion": "v1",
"data": {
"packageId": "5e2b8c41-9d3a-4f67-b1c0-7a8e2d4f6b13",
"status": "active",
"version": 8
}
}bordereau.ready
An async bordereau export is ready (link valid 24 h).
{
"id": "evt_7f6c1b0e-3c2d-4c84-9a0e-2f3b1c5d7e90",
"type": "bordereau.ready",
"createdAt": "2026-11-05T09:15:02.481Z",
"environment": "sandbox",
"apiVersion": "v1",
"data": {
"exportId": "9c0d1e2f-3a4b-4c5d-8e6f-7a8b9c0d1e2f",
"url": "https://files.wathiqaty.com/exports/premium-2026-q3.csv",
"expiresAt": "2026-11-06T09:15:00.000Z"
}
}Body
{
"id": "evt_7f6c1b0e-3c2d-4c84-9a0e-2f3b1c5d7e90",
"type": "policy.issued",
"createdAt": "2026-11-05T09:15:02.481Z",
"environment": "sandbox",
"apiVersion": "v1",
"data": {
"id": "6f1c2d3e-4b5a-4c69-8d7e-1f2a3b4c5d6e",
"number": "POL-00000042",
"externalReference": "MI/2026/MED/004213",
"packageId": "5e2b8c41-9d3a-4f67-b1c0-7a8e2d4f6b13",
"packageExternalReference": "PRD-GOLD-2026",
"packageName": {
"en": "Gold Care",
"ar": "جولد كير"
},
"insuranceType": "individual_medical",
"status": "active",
"coverageStartsAt": "2026-11-05T09:15:00.000Z",
"coverageEndsAt": "2027-11-05T09:15:00.000Z",
"cancelledAt": null,
"sumInsured": "250000.00",
"premium": {
"net": "8333.33",
"taxes": [
{
"name": "VAT",
"rate": "14.00",
"amount": "1166.67"
}
],
"total": "9500.00",
"currency": "EGP"
},
"commission": {
"rate": "10.00",
"amount": "833.33"
},
"customer": {
"name": "Mona Adel",
"email": "mona@example.com",
"phone": "01012345678",
"nationalId": "29001011234567",
"birthDate": "1990-01-01",
"gender": "female"
},
"kyc": {
"provider": "lumin",
"verifiedId": "lm_7c1d9e2f",
"verifiedAt": "2026-11-05T09:02:11.000Z"
},
"vehicle": null,
"cardNumber": "MC-2026-42",
"cancellationScale": {
"effectiveFrom": "2026-10-01",
"tiers": [
{
"fromDay": 0,
"toDay": 14,
"refundPercent": "90.00",
"basis": "flat"
},
{
"fromDay": 15,
"toDay": 365,
"refundPercent": "80.00",
"basis": "pro_rata"
}
]
},
"createdAt": "2026-11-05T09:15:05.000Z",
"updatedAt": "2026-11-05T09:16:40.000Z"
}
}The request#
Headers
POST /wathiqaty/webhooks HTTP/1.1 Host: core.insurer.example Content-Type: application/json User-Agent: Wathiqaty-Webhooks/1 Wathiqaty-Event: policy.issued Wathiqaty-Delivery: 0d0a7c9b-8e6f-4a5b-9c3d-2e1f0a9b8c7d Wathiqaty-Signature: t=1793876102,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
| Body field | Meaning |
|---|---|
id | Event id. The same on every retry and redelivery: deduplicate on it. |
type | Event type, e.g. policy.issued. |
createdAt | When the event happened. |
environment | sandbox or production. |
apiVersion | v1. The shape of data follows this version. |
data | The resource as the API returns it, or a small object for lifecycle events. |
Verifying signatures#
Wathiqaty-Signature holds a Unix timestamp t and one or two v1 signatures (two while a secret is being rotated). Each v1 is the hex HMAC-SHA256 of {t}.{raw body} with your endpoint's signing secret.
- Parse
tand everyv1from the header. - Reject the request if
tis more than 5 minutes from now (replay protection). Keep your clock in sync (NTP). - Compute the HMAC over the raw bytes of the body, as received. Parsing and re-serialising the JSON changes the bytes and breaks the signature.
- Compare in constant time. Accept if any
v1matches.
Node.js
import { createHmac, timingSafeEqual } from "node:crypto"
// rawBody: the request body as received (Buffer), before JSON parsing
export function verify(rawBody, header, secret, now = Date.now() / 1000) {
const parts = header.split(",").map((p) => p.split("="))
const t = Number(parts.find(([k]) => k === "t")?.[1])
if (!Number.isFinite(t) || Math.abs(now - t) > 300) return false
const expected = createHmac("sha256", secret).update(`${t}.`).update(rawBody).digest()
return parts
.filter(([k]) => k === "v1")
.some(([, sig]) => {
const got = Buffer.from(sig, "hex")
return got.length === expected.length && timingSafeEqual(got, expected)
})
}
// Express: app.post("/wathiqaty/webhooks", express.raw({ type: "application/json" }), handler)Python
import hashlib, hmac, time
def verify(raw_body: bytes, header: str, secret: str) -> bool:
parts = [p.split("=", 1) for p in header.split(",")]
t = int(next(v for k, v in parts if k == "t"))
if abs(time.time() - t) > 300:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(v, expected) for k, v in parts if k == "v1")
# Flask: verify(request.get_data(), request.headers["Wathiqaty-Signature"], SECRET)C#
using System.Security.Cryptography;
using System.Text;
static bool Verify(byte[] rawBody, string header, string secret)
{
var parts = header.Split(',').Select(p => p.Split('=', 2)).ToList();
var t = long.Parse(parts.First(p => p[0] == "t")[1]);
if (Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - t) > 300) return false;
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var expected = hmac.ComputeHash(Encoding.UTF8.GetBytes($"{t}.").Concat(rawBody).ToArray());
return parts.Where(p => p[0] == "v1").Any(p =>
CryptographicOperations.FixedTimeEquals(Convert.FromHexString(p[1]), expected));
}
// ASP.NET Core: read Request.Body into a byte[] before model bindingimport { createHmac, timingSafeEqual } from "node:crypto"
// rawBody: the request body as received (Buffer), before JSON parsing
export function verify(rawBody, header, secret, now = Date.now() / 1000) {
const parts = header.split(",").map((p) => p.split("="))
const t = Number(parts.find(([k]) => k === "t")?.[1])
if (!Number.isFinite(t) || Math.abs(now - t) > 300) return false
const expected = createHmac("sha256", secret).update(`${t}.`).update(rawBody).digest()
return parts
.filter(([k]) => k === "v1")
.some(([, sig]) => {
const got = Buffer.from(sig, "hex")
return got.length === expected.length && timingSafeEqual(got, expected)
})
}
// Express: app.post("/wathiqaty/webhooks", express.raw({ type: "application/json" }), handler)Responding and retries#
- Answer
2xxwithin 10 seconds, then process the event asynchronously (queue it). The response body is ignored. - Anything else (non-2xx, timeout, TLS or DNS error) is retried with exponential backoff and jitter: about 1 min, 5 min, 30 min, 2 h, 6 h, then every 12 h, for up to 3 days.
- Redirects are not followed: register the final URL.
- After 3 days of failures the endpoint is disabled and your technical contact is emailed. Re-enable it and redeliver the missed events from its delivery list.
- Answer
2xxto event types you don't handle; new types can be added within v1.
Endpoint requirements#
- HTTPS with a valid certificate from a public CA, on a public hostname. Private, loopback and link-local addresses are refused.
- Deliveries come from fixed IP addresses, published by your account manager, so you can allow-list them.
- Secrets: each endpoint has its own. Rotate with Rotate the signing secret; both secrets sign for 24 hours.