Webhooky

Webhook je adresa ve vašem programu, kterou abFaktura zavolá, když se změní doklad. Nemusíte se pak neustále ptát na změny.

Nastavení

Webhooky nastavíte v Nastavení › API klíče › Webhooky nebo přes API (oprávnění webhooks:write):

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

Odpověď obsahuje secret pro ověření podpisu. Ukáže se jen jednou; nový získáte přes POST /webhooks/{id}/rotate-secret. Firma může mít nejvýše 10 webhooků. Adresa musí být veřejná a začínat https://.

Události

UdálostKdy přijde
invoice.createdDoklad byl vytvořen (v aplikaci, přes API, opakovanou fakturou, importem).
invoice.updatedDoklad se změnil.
invoice.deletedDoklad byl smazán.
invoice.restoredSmazaný doklad byl obnoven.
invoice.sentDoklad byl odeslán e-mailem.
invoice.reminder_sentOdešla upomínka.
invoice.viewedOdběratel otevřel doklad (odkaz nebo e-mail).
invoice.payment_addedK dokladu přibyla úhrada.
invoice.payment_deletedÚhrada byla zrušena.
invoice.paidDoklad je celý zaplacený.
pingZkušební událost z POST /webhooks/{id}/ping.

Místo výčtu můžete poslat ["*"] pro všechny události.

Volání

abFaktura pošle POST s JSON tělem a hlavičkami:

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": "Odběratel 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/…"
  }
}

Webhook nese jen souhrn; celý doklad si načtěte z adresy v data.api. Odpovězte do 15 sekund kódem 2xx. Jinou odpověď zkusíme znovu po 1, 5 a 30 minutách, po 2, 6, 12 a 24 hodinách. Pak se doručení vzdáme; poslat ho znovu můžete přes POST /webhooks/deliveries/{id}/redeliver. Stejná událost může výjimečně přijít dvakrát, poznáte ji podle id.

Ověření podpisu

Hlavička X-Abfaktura-Signature obsahuje čas odeslání t (unixové sekundy) a v1, což je HMAC-SHA256 řetězce t + "." + tělo s vaším tajemstvím, zapsaný hexadecimálně. Porovnávejte surové tělo požadavku a odmítněte volání starší než 5 minut.

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']);
}