Developers

Webhooks

Signed HTTPS notifications when invoices are issued, cleared, rejected or paid and when customers change: events, headers, signatures, retries and the events list.

How it works

Register an endpoint in Settings → Developers → Webhooks, or with POST /webhook-endpoints (scope webhooks:manage, and the person behind the key must manage the company's settings). Each event the endpoint subscribes to is sent to it as an HTTPS POST with a JSON body, usually within a minute. An endpoint belongs to one company; a sandbox company's endpoints receive only its test events (livemode: false).

An endpoint subscribes only to the events the key or app that registers it may read: invoice and note events need invoices:read, customer events customers:read. Through the API, * is stored as exactly those types, and any other type is refused with 422. An endpoint an app registers belongs to that app's connection: when the company disconnects the app it is switched off for good (disabled_reason: connection_revoked), and it never receives an event the connection's current scopes cannot read. An app sees and changes only the endpoints it registered.

Event When
invoice.created An invoice draft is created, through any channel.
invoice.issued A draft is issued.
invoice.zatca_status_changed The ZATCA status changes, for example from pending to cleared or rejected. data.previous_zatca_status holds the status before.
invoice.payment_recorded A payment is recorded, voided or deleted. data.payment.status says which.
invoice.paid, invoice.overdue The invoice becomes paid in full, or overdue.
invoice.cancelled A draft is cancelled.
credit_note.issued, debit_note.issued A note is issued.
credit_note.zatca_status_changed, debit_note.zatca_status_changed A note's ZATCA status changes.
customer.created, customer.updated A customer is added or changed.
ping You send a test event (POST /webhook-endpoints/{id}/test).

Documents brought in by an import from another system, and opening balances, are history and produce no events.

Headers

  • WF-Event-Id: the event id (evt_…), the same on every retry and redelivery. Deduplicate on it.
  • WF-Event-Type: the event type.
  • WF-Delivery-Id: the delivery id (whd_…), for support and redelivery.
  • WF-Api-Version: the payload format, 2026-11-01, fixed for the endpoint when it is created.
  • WF-Signature: t=<unix time>,v1=<signature>, described below.

Payload

The body is {"id": "evt_…", "type": "invoice.issued", "api_version": "2026-11-01", "created_at": "…", "livemode": true, "company_id": 42, "connection_id": null, "data": {"object": {…}}}. data.object is the invoice, note or customer exactly as GET returns it, as it stood when the change was saved. The schema is under webhooks in the OpenAPI document.

Signatures

Each endpoint has a secret (whsec_…), shown once when the endpoint is created and when the secret is rotated. v1 is the hex HMAC-SHA256 of t + "." + raw body, keyed with the whole secret. Refuse a t more than 300 seconds from your clock and compare in constant time; in PHP: hash_equals(hash_hmac('sha256', $t.'.'.$rawBody, $secret), $v1). For 24 hours after a rotation a second v1 signed with the previous secret is sent too; accept either.

Delivery and retries

  • Answer with any 2xx within 8 seconds. A redirect, a timeout or any other status is a failure.
  • Failures are retried after 1, 5 and 30 minutes, then 2, 6, 12 and 24 hours (about 45 hours in all), then given up. You can redeliver any delivery by hand.
  • At least once and in no guaranteed order: deduplicate on the event id, and read the object again when you need its current state.
  • An endpoint that fails 100 times in a row with no success for 72 hours is switched off and its owner is emailed. Switch it on again from the dashboard or with PATCH ("status": "active").
  • Endpoints must be public https addresses on port 443 or 8443, with a domain name (not an IP address) that resolves only to public addresses. This is checked when you register one and again before every delivery, and redirects are never followed.
  • The first 512 bytes of your answer are kept with the delivery, so do not echo secrets back.

Polling instead

GET /events (scope invoices:read; customer events also need customers:read) lists the same events newest first, with the same data. Events are recorded while the company has an active endpoint and are kept for 30 days.

Ready to build?

API access is part of the Business and Enterprise plans. Create a key in Settings → API keys and make your first call in minutes.