Webhooks
Subscribe an HTTPS endpoint to invoice and expense events, verify the HMAC-SHA256 signature, and handle retries.
Webhooks push a JSON message to your HTTPS endpoint whenever an invoice or expense changes in your business, whether the change came from the API, the app, a recurring invoice or a payment. They are included in the same plan as the API.
Subscribe
There is no API for subscriptions. In the app open Settings, Integrations, Webhooks, add your endpoint URL and tick the events you want. The app generates a signing secret for the subscription, prefixed whsec_. Reveal it and store it with your endpoint's configuration.
Endpoint rules:
- Must be
https://. - Up to 2048 characters.
Events
invoice.createdinvoice.createdEVENTinvoice.updatedinvoice.updatedEVENTinvoice.deletedinvoice.deletedEVENTexpense.createdexpense.createdEVENTexpense.updatedexpense.updatedEVENTexpense.deletedexpense.deletedWhat each event carries and when it is sent:
| Event | data | When |
|---|---|---|
invoice.created | full invoice, same shape as GET /invoices/{id} | a non-draft invoice is created |
invoice.updated | full invoice | any change, including payment status changes and payments received through the app |
invoice.deleted | { "id": "<uuid>" } | a non-draft invoice is deleted |
expense.created | full expense, same shape as GET /expenses/{id} | an expense is created |
expense.updated | full expense | figures or file changed |
expense.deleted | { "id": "<uuid>" } | an expense is deleted through the API or the app |
Draft invoices never produce events. invoice.created is sent as soon as the invoice is saved, before its PDF is rendered; a download request made in the handler waits for the PDF.
Delivery
Each delivery is an HTTP POST with:
Content-Type: application/json
Signature: <hex HMAC-SHA256 of the raw body, keyed with your whsec_ secret>Body:
{
"event": "invoice.updated",
"data": {
"id": "6f1e9d2a-0b3c-4e5f-8a9b-1c2d3e4f5a6b",
"invoice_type": "vat_invoice",
"series": "SF",
"invoice_number": "007",
"payment_status": "paid",
"…": "…"
}
}Your endpoint must answer with any 2xx status within 10 seconds. Anything else, including a timeout, counts as a failure.
Retries
A failed delivery is retried twice more: after about 10 seconds, then after about 100 seconds. After three failures the message is dropped. Deliveries can therefore arrive more than once and, for a busy resource, out of order. Make your handler idempotent: use data.id plus data.updated_at to detect duplicates, and fetch the current state with GET /invoices/{id} when the order matters.
There is no event identifier or timestamp header, and there is no delivery log in the app.
Verify the signature
Compute HMAC-SHA256 over the raw request body exactly as received, using the subscription secret, and compare it in constant time with the Signature header.
$secret = getenv('FSASKAITA_WEBHOOK_SECRET'); // whsec_…
$payload = file_get_contents('php://input');
$expected = hash_hmac('sha256', $payload, $secret);
$signature = $_SERVER['HTTP_SIGNATURE'] ?? '';
if (! hash_equals($expected, $signature)) {
http_response_code(401);
exit;
}
$event = json_decode($payload, true);
// handle $event['event'] and $event['data']
http_response_code(200);Read the body as raw bytes before any JSON parsing; re-serialising the parsed object may change whitespace or key order and break the comparison.
Respond fast
Acknowledge with 2xx first and process afterwards. Slow handlers hit the 10 second timeout, which triggers retries and duplicate work.
Rotate a secret
Delete the subscription and create a new one; the new subscription gets a new secret. Update your endpoint configuration before deleting the old subscription if you cannot tolerate a gap.