Webhooks

A webhook is an address in your program that abFaktura calls when a document changes, so you do not have to keep asking.

Setting up

Set webhooks up in Settings › API keys › Webhooks or through the API (permission webhooks:write):

POST /api/v1/webhooks
{ "url": "https://your-program.example/abfaktura", "events": ["invoice.created", "invoice.paid"] }

The answer holds the secret for checking the signature. It is shown once; get a new one by POST /webhooks/{id}/rotate-secret. A company may have at most 10 webhooks. The address must be public and start with https://.

Events

EventWhen
invoice.createdA document was created (in the app, through the API, by a recurring invoice, by an import).
invoice.updatedA document changed.
invoice.deletedA document was deleted.
invoice.restoredA deleted document was restored.
invoice.sentA document was e-mailed.
invoice.reminder_sentA payment reminder was sent.
invoice.viewedThe customer opened the document (link or e-mail).
invoice.payment_addedA payment was recorded.
invoice.payment_deletedA payment was removed.
invoice.paidThe document is paid in full.
pingTest event from POST /webhooks/{id}/ping.

Send ["*"] instead of a list for all events.

The call

abFaktura sends a POST with a JSON body and these headers:

POST /abfaktura HTTP/1.1
Content-Type: application/json
X-Abfaktura-Event: invoice.paid
X-Abfaktura-Delivery: 7b0c5c1e-…
X-Abfaktura-Signature: t=1792915200,v1=5f2b…

{
  "id": "7b0c5c1e-…",
  "type": "invoice.paid",
  "createdAt": "2026-10-26T08:30:00Z",
  "accountId": "…",
  "data": {
    "invoiceId": "…",
    "documentNumber": "2026-0042",
    "documentType": "issued",
    "kind": "invoice",
    "partner": { "name": "Customer s.r.o.", "companyNumber": "12345678", "taxNumber": "CZ12345678" },
    "currency": "CZK",
    "total": 30250,
    "amountDue": 0,
    "dateOfIssue": "2026-10-20T00:00:00Z",
    "dueDate": "2026-11-03T00:00:00Z",
    "deleted": false,
    "api": "/api/v1/invoices/…"
  }
}

A webhook carries a summary only; load the whole document from data.api. Answer within 15 seconds with 2xx. Any other answer is tried again after 1, 5 and 30 minutes and after 2, 6, 12 and 24 hours. Then the delivery gives up; send it again by POST /webhooks/deliveries/{id}/redeliver. Rarely the same event comes twice; tell it by its id.

Checking the signature

The X-Abfaktura-Signature header holds the sending time t (Unix seconds) and v1, the hex HMAC-SHA256 of t + "." + body with your secret. Compare against the raw request body and refuse calls older than 5 minutes.

Node.js

import crypto from "node:crypto";

export function verify(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const age = Math.abs(Date.now() / 1000 - Number(parts.t));
  const expected = crypto.createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
  return age < 300 && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}

C#

using System.Security.Cryptography;
using System.Text;

static bool Verify(string rawBody, string header, string secret)
{
    var parts = header.Split(',').Select(p => p.Split('=', 2)).ToDictionary(p => p[0], p => p[1]);
    var age = Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - long.Parse(parts["t"]));
    var mac = HMACSHA256.HashData(Encoding.UTF8.GetBytes(secret), Encoding.UTF8.GetBytes($"{parts["t"]}.{rawBody}"));
    return age < 300 && CryptographicOperations.FixedTimeEquals(
        Encoding.ASCII.GetBytes(Convert.ToHexString(mac).ToLowerInvariant()), Encoding.ASCII.GetBytes(parts["v1"]));
}

PHP

function verify(string $rawBody, string $header, string $secret): bool {
    parse_str(str_replace(',', '&', $header), $parts);
    $expected = hash_hmac('sha256', $parts['t'] . '.' . $rawBody, $secret);
    return abs(time() - (int)$parts['t']) < 300 && hash_equals($expected, $parts['v1']);
}