# Webhooks

When something happens to your company's data, Wathiqaty sends a signed HTTPS `POST` to your endpoint. Register endpoints with [Webhook endpoints](/reference/webhook-endpoints.md) 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](/guides/conventions.md#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`.

```json
{
  "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)

```json
{
  "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.

```json
{
  "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).

```json
{
  "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.

```json
{
  "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.

```json
{
  "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)

```json
{
  "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.

```json
{
  "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).

```json
{
  "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.

```json
{
  "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.

```json
{
  "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.

```json
{
  "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.

```json
{
  "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)

```json
{
  "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)

```json
{
  "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.

```json
{
  "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.

```json
{
  "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)

```json
{
  "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.

```json
{
  "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.

```json
{
  "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.

```json
{
  "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.

```json
{
  "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).

```json
{
  "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"
  }
}
```

## 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.

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.

**Node.js**

```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**

```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#**

```csharp
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 binding
```

## 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](/reference/webhook-endpoints.md#rotate-webhook-secret); both secrets sign for 24 hours.
