Developer docsv1
Guides

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

What each event carries and when it is sent:

EventdataWhen
invoice.createdfull invoice, same shape as GET /invoices/{id}a non-draft invoice is created
invoice.updatedfull invoiceany change, including payment status changes and payments received through the app
invoice.deleted{ "id": "<uuid>" }a non-draft invoice is deleted
expense.createdfull expense, same shape as GET /expenses/{id}an expense is created
expense.updatedfull expensefigures 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.

F-sąskaita / DevelopersWhat’s new

On this page