v1 · draft
View .md

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.

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 fieldMeaning
idEvent id. The same on every retry and redelivery: deduplicate on it.
typeEvent type, e.g. policy.issued.
createdAtWhen the event happened.
environmentsandbox or production.
apiVersionv1. The shape of data follows this version.
dataThe 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.

  1. Parse t and every v1 from the header.
  2. Reject the request if t is more than 5 minutes from now (replay protection). Keep your clock in sync (NTP).
  3. 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.
  4. Compare in constant time. Accept if any v1 matches.
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)

Responding and retries#

  • Answer 2xx within 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 2xx to 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.