Webhooks
Receive invoice and certificate events at your HTTPS endpoint. Every delivery is persisted, retried automatically on failure, and visible in the delivery log.
Overview
- Configure a single webhook URL per tenant (HTTPS only).
- We send a
POSTwith a JSON body for each event. - Every request includes
X-ZATCA-Event(event type) andX-ZATCA-Delivery-ID(unique delivery id for idempotency). - Optionally set a signing secret; we then send
X-ZATCA-Signature: sha256=<hmac_hex>(HMAC-SHA256 of the raw body). - Retries: Failed deliveries (non-2xx or timeout) are retried automatically up to 5 times with exponential backoff (1m, 5m, 15m, 1h). After that, events are marked permanently failed and can be retried manually via the API or dashboard.
Event types
| Event | When |
|---|---|
| invoice.reported | Simplified invoice (or credit/debit note) reported to ZATCA. |
| invoice.cleared | Standard invoice (or credit/debit note) cleared by ZATCA. |
| webhook.test | Triggered by "Send test event" (dashboard or API). |
| cert.expiring | EGS unit certificate expiring within 30/14/7/1 days or expired. |
Payloads always include event plus event-specific fields (e.g. invoice_id, uuid, status, document_type, invoice_type).
Verifying the signature
If you set a webhook secret, verify the X-ZATCA-Signature header using the raw request body (UTF-8) and your secret. Use constant-time comparison to avoid timing attacks.
// Node.js example
const crypto = require('crypto');
function verifySignature(rawBody, signatureHeader, secret) {
const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
return crypto.timingSafeEqual(Buffer.from(signatureHeader), Buffer.from(expected));
}Get webhook configuration
Returns the current webhook URL and whether a signing secret is set. Secret value is never returned.
Code examples
const res = await fetch('https://api.esnadapi.com/v1/webhooks/config', {
headers: { Authorization: `Bearer ${apiKey}` },
});
const config = await res.json();Success response (200)
{
"webhook_url": "https://api.esnadapi.com/webhooks/zatca",
"webhook_secret_set": true
}Update webhook URL and/or secret
Set or update the webhook URL (HTTPS only) and optional secret for X-ZATCA-Signature. Pass null to clear.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| webhook_url | string | null | No | HTTPS URL; max 500 chars |
| webhook_secret | string | null | No | Secret for signing; max 64 chars |
Code examples
await fetch('https://api.esnadapi.com/v1/webhooks/config', {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify({
webhook_url: 'https://api.esnadapi.com/webhooks/zatca',
webhook_secret: 'your-signing-secret',
}),
});Success response (200)
{
"ok": true
}Trigger test webhook delivery
Sends a test payload to your configured webhook URL (synchronous). Returns an error if webhook_url is not set. The delivery appears in the events log.
Code examples
const res = await fetch('https://api.esnadapi.com/v1/webhooks/test', {
method: 'POST',
headers: { Authorization: `Bearer ${apiKey}` },
});
const result = await res.json();Success response (200)
{
"sent": true,
"message": "Test webhook fired",
"delivery_id": "delivery-uuid"
}Error responses
- 200
{ "error": { "code": "WEBHOOK_NOT_CONFIGURED", "message": "Set webhook_url on your tenant to receive test events", "retryable": false } }
List webhook delivery events
Returns recent webhook deliveries with status, attempt count, and full attempt history. Use query params to filter by status or event type. Cursor-based pagination.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
| limit | number | No | Default: 50 |
| cursor | string | No | Pagination cursor (ISO date of last item) |
| status | string | No | Filter: delivered | pending | failed | permanently_failed |
| event_type | string | No | Filter: e.g. invoice.reported, invoice.cleared |
Code examples
const res = await fetch(`${BASE}/v1/webhooks/events?limit=50&status=failed`, {
headers: { Authorization: `Bearer ${apiKey}` },
});
const { data } = await res.json();Success response (200)
{
"data": [
{
"id": "delivery-uuid",
"event_type": "invoice.reported",
"invoice_id": "inv-uuid",
"status": "delivered",
"attempts": 1,
"response_code": 200,
"created_at": "2025-01-15T10:05:00.000Z",
"last_attempt_at": "2025-01-15T10:05:01.000Z",
"next_retry_at": null,
"attempt_history": [
{
"attempt_number": 1,
"attempted_at": "2025-01-15T10:05:01.000Z",
"response_code": 200,
"success": true,
"error_message": null
}
]
}
],
"next_cursor": null
}Retry a failed webhook delivery
Schedules one more delivery attempt for a failed or permanently_failed event. The attempt runs shortly after the request. Returns ok: true if the subsequent attempt succeeded.
Code examples
await fetch(`${BASE}/v1/webhooks/events/${deliveryId}/retry`, {
method: 'POST',
headers: { Authorization: `Bearer ${apiKey}` },
});Success response (200)
{
"ok": true
}