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
| Event | When |
|---|---|
invoice.created | A document was created (in the app, through the API, by a recurring invoice, by an import). |
invoice.updated | A document changed. |
invoice.deleted | A document was deleted. |
invoice.restored | A deleted document was restored. |
invoice.sent | A document was e-mailed. |
invoice.reminder_sent | A payment reminder was sent. |
invoice.viewed | The customer opened the document (link or e-mail). |
invoice.payment_added | A payment was recorded. |
invoice.payment_deleted | A payment was removed. |
invoice.paid | The document is paid in full. |
ping | Test 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']);
}