# Webhooks · DameDoklad docs

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

Source: https://damedoklad.cz/en/docs/webhooks/

# Webhooks

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

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](/en/pricing/).

## Create an endpoint

```bash
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.

```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

| Event | When it happens |
| --- | --- |
| `invoice.issued` | An invoice was issued. |
| `invoice.storno` | An invoice was reversed. |
| `invoice.payment_added` | A payment was added to an invoice. |
| `invoice.payment_removed` | A payment was removed from an invoice. |
| `received_invoice.recorded` | A received invoice was recorded. |
| `received_invoice.storno` | A received invoice was reversed. |
| `received_invoice.settled` | A received invoice was settled. |
| `credit_note.issued` | A credit note was issued. |
| `credit_note.storno` | A credit note was reversed. |
| `credit_note.payment_added` | A payment was added to a credit note. |
| `proforma.issued` | A proforma was issued. |
| `proforma.payment_added` | A payment was added to a proforma. |
| `delivery_note.issued` | A delivery note was issued. |
| `member.invited` | Someone was invited to the company. |
| `member.joined` | Someone accepted an invitation. |
| `member.role_changed` | A member's role changed. |
| `member.removed` | A member was removed. |
| `company.updated` | Company details changed. |
| `inbox.document_received` | A document arrived in the To do inbox. |
| `inbox.document_finished` | A document in the inbox was finished. |

## Managing them through the API

You manage webhooks with these operations:

-   [Ask the shop to send a notification here when an order is created or changes, so an order arrives within a minute and not at the next 15-minute read. Turn it on with webhooksEnabled](/en/docs/api-reference/connections/#registerShopWebhooks) `POST /v1/shop-connections/{id}/register-webhooks`
-   [Send a failed delivery again](/en/docs/api-reference/platform/#retryWebhookDelivery) `POST /v1/webhook-deliveries/{id}/retry`
-   [List the event types a webhook can subscribe to. Payloads are in the \`webhooks\` section of /openapi.json](/en/docs/api-reference/platform/#listWebhookEvents) `GET /v1/webhook-events`
-   [List webhook endpoints](/en/docs/api-reference/platform/#listWebhookEndpoints) `GET /v1/webhooks`
-   [Create a webhook endpoint. The signing secret is returned once.](/en/docs/api-reference/platform/#createWebhookEndpoint) `POST /v1/webhooks`
-   [Get a webhook endpoint](/en/docs/api-reference/platform/#getWebhookEndpoint) `GET /v1/webhooks/{id}`
-   [Update a webhook endpoint: URL, events, or pause it](/en/docs/api-reference/platform/#updateWebhookEndpoint) `PATCH /v1/webhooks/{id}`
-   [Delete a webhook endpoint and its delivery log](/en/docs/api-reference/platform/#deleteWebhookEndpoint) `DELETE /v1/webhooks/{id}`
-   [List delivery attempts of a webhook endpoint, newest first](/en/docs/api-reference/platform/#listWebhookDeliveries) `GET /v1/webhooks/{id}/deliveries`
-   [Replace the signing secret. The new secret is returned once; the old one stops working immediately.](/en/docs/api-reference/platform/#rotateWebhookSecret) `POST /v1/webhooks/{id}/rotate-secret`
-   [Send a signed test event to this endpoint now and return the result. Test events are not retried.](/en/docs/api-reference/platform/#testWebhookEndpoint) `POST /v1/webhooks/{id}/test`
