Skip to content

Guide

Signed accounting events in your own systems: endpoints, payload, signature, retries and the delivery log.

Updated 30 Oct 2026

A webhook is an address in your system that DameDoklad calls when something happens: for example, when you issue an invoice or a payment comes in. That way you don’t have to keep asking the API what’s new.

You manage webhooks in Settings > Webhooks or through the API. Creating one takes a plan with webhooks; see pricing.

Create an endpoint

Terminal
curl https://api.damedoklad.cz/v1/webhooks \
  -H "Authorization: Bearer $DAMEDOKLAD_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://example.com/hooks/damedoklad", "events": ["invoice.issued", "received_invoice.*"] }'

The response contains the signing secret, which starts with whsec_. You see it only once. If you lose it, replace it with POST /v1/webhooks/{id}/rotate-secret; the old secret stops working immediately.

In events, list event names, a prefix such as invoice.*, or * for everything. The address must be public: DameDoklad refuses private and local addresses, and checks again before each delivery.

What arrives

Each delivery is a POST with a JSON body:

JSON
{
  "id": "evt_6f1c2a9b04d3e8f17a5c0b21",
  "type": "invoice.issued",
  "createdAt": "2026-10-30T09:12:44.120Z",
  "workspaceId": "wsp_…",
  "data": { "row": { "id": "inv_…", "number": "FV2026001", "status": "issued" }, "lines": [] }
}
Field Meaning
id The event ID. It stays the same when a delivery is retried, so use it to spot repeats.
type The event name.
createdAt When the event happened.
workspaceId The company.
data What the event says about the object. Its fields depend on the event, so load the current state through the API when you need more.

These headers come with it:

Header Meaning
X-DameDoklad-Event The event name.
X-DameDoklad-Delivery The ID of this delivery, as in the delivery log.
X-DameDoklad-Signature The signature, see below.
User-Agent DameDoklad-Webhooks/1.0

Check the signature

The signature header looks like t=1793351564,v1=5f8a…. t is the send time in Unix seconds and v1 is a hexadecimal HMAC SHA-256 of the text {t}.{raw body}, keyed with your secret. Compare it over the raw bytes of the body, before parsing the JSON.

Node.js
import { createHmac, timingSafeEqual } from 'node:crypto';

export function verify(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(',').map((part) => part.split('=')));
  const expected = createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex');
  const same = expected.length === parts.v1.length && timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
  return same && fresh;
}
Python
import hashlib, hmac, time

def verify(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(part.split("=", 1) for part in header.split(","))
    signed = parts["t"].encode() + b"." + raw_body
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts["v1"]) and abs(time.time() - int(parts["t"])) < 300

The five-minute window is our recommendation against replay. DameDoklad signs each attempt with a fresh t.

Retries

Any 2xx answer within 15 seconds counts as delivered. Anything else is retried, up to eight attempts in total:

After attempt Next attempt in
1 1 minute
2 5 minutes
3 30 minutes
4 2 hours
5 6 hours
6 12 hours
7 24 hours

After the eighth failure the delivery stays in the log as failed, and you can send it again with POST /v1/webhook-deliveries/{id}/retry. See the log with GET /v1/webhooks/{id}/deliveries.

POST /v1/webhooks/{id}/test sends a signed webhook.test event straight away and returns the result. Test events are not retried.

Receiving events

  • Respond quickly. Store the event and process it afterwards.
  • Expect repeats. The same event can arrive more than once, so keep track of id.
  • Don’t rely on order. When in doubt, load the object’s current state through the API.
  • An event is sent only after its transaction commits, so a rolled-back invoice never announces itself.

Events

EventWhen it happens
invoice.issuedAn invoice was issued.
invoice.stornoAn invoice was reversed.
invoice.payment_addedA payment was added to an invoice.
invoice.payment_removedA payment was removed from an invoice.
received_invoice.recordedA received invoice was recorded.
received_invoice.stornoA received invoice was reversed.
received_invoice.settledA received invoice was settled.
credit_note.issuedA credit note was issued.
credit_note.stornoA credit note was reversed.
credit_note.payment_addedA payment was added to a credit note.
proforma.issuedA proforma was issued.
proforma.payment_addedA payment was added to a proforma.
delivery_note.issuedA delivery note was issued.
member.invitedSomeone was invited to the company.
member.joinedSomeone accepted an invitation.
member.role_changedA member's role changed.
member.removedA member was removed.
company.updatedCompany details changed.
inbox.document_receivedA document arrived in the To do inbox.
inbox.document_finishedA document in the inbox was finished.

Managing them through the API

You manage webhooks with these operations: