Webhooks
Receive realtime events via HTTP POST to your endpoint
Last updated on
Webhooks receive realtime events via HTTP POST to a URL you configure. The same events are also available via SSE. Webhooks are configured per organisation - every organisation has its own webhooks.
Management
See the OpenAPI specification for the full request and response schemas.
| Method | Path | Description |
|---|---|---|
| GET | /api/orgs/:orgId/webhooks | Retrieve webhooks |
| GET | /api/orgs/:orgId/webhooks/delivery-stats | Retrieve delivery statistics |
| POST | /api/orgs/:orgId/webhooks/retry-deliveries | Retry deliveries. Body: mode (failed or force) and optionally id for a single webhook. failed: only permanently failed deliveries, immediately. force: pending and permanently failed deliveries in the background, ignoring wait times. |
| GET | /api/orgs/:orgId/webhooks/:id | Retrieve a webhook by ID |
| POST | /api/orgs/:orgId/webhooks | Create a webhook |
| PATCH | /api/orgs/:orgId/webhooks/:id | Update a webhook |
| DELETE | /api/orgs/:orgId/webhooks/:id | Delete a webhook |
Creating a webhook
POST /api/orgs/org_abc123/webhooks
Content-Type: application/json
x-api-key: tkn_xxx{
"url": "https://jouw-server.com/webhooks/tillor",
"subscribedEventKeys": ["invoice:created", "invoice:paid", "customer:updated"],
"enabled": true
}- url - HTTPS endpoint that accepts POST requests
- subscribedEventKeys - Array of event keys or
["*"]for all events. The full list is in Tillor under Profile menu → Developers, at Add webhook → Subscribed events.
A few events occur so often that you cannot choose them and they do not arrive via * either, such as the physical state of barriers, typing in the inbox, controller logs and vehicle presence. They are not in the list below. Which events you can choose also depends on your permissions.
- enabled -
trueto switch delivery on
You may only choose events for which your organisation member has the right permission (e.g. bank transactions for bankAccountTransaction:*). Wildcard * is only available to members with full access (*). With an invalid combination the API returns an error. A webhook forwards the full payload, including the linked data (such as the customer on a reservation). That is why you must also be allowed to read those domains in order to create a webhook, change its URL or events, or switch it back on.
Webhook payload
Every webhook delivery is a POST with:
Headers:
Content-Type: application/jsonX-Webhook-Event-ID- stable unique ID for this logical delivery (same value on retries)X-Webhook-Signature- HMAC-SHA256 signature of the body, formatsha256=<hex>(see Verifying the signature)
Body:
{
"event": "invoice:created",
"data": {},
"timestamp": 1735689600000,
"traceId": "4bf92f3577b34da6a3ce929d0e0e4736",
"spanId": "00f067aa0ba902b7",
"triggeredBy": {
"id": "usr_63x8jp68is",
"name": "Niels"
}
}| Field | Type | Description |
|---|---|---|
event | string | Event key (e.g. invoice:created) |
data | object | Event-specific payload |
timestamp | number | Unix timestamp (ms) when the event was emitted |
traceId | string | OpenTelemetry trace ID of the Tillor action that caused this event. Pass it on to support; it is not a secret. |
spanId | string (optional) | Span within that trace |
triggeredBy | object | null | User who performed the action (id, name). null for system actions (cron, bank sync, …) |
See Examples for full payload examples.
Verifying the signature
Every webhook has its own signing secret (64 hex characters, visible in the webhook table via Show signing secret on Profile menu → Developers). Tillor signs every POST with that secret so that you can verify that the request really comes from Tillor and that the body has not been altered.
How Tillor signs:
- Compute
HMAC-SHA256(signingSecret, raw_request_body)(lowercase hex). - Send that as
X-Webhook-Signature: sha256=<hex>.
How you verify:
- Read the raw request body (before JSON parsing - otherwise the hash will not match).
- Compute
HMAC-SHA256(signingSecret, raw_body)yourself in hex. - Compare it with the value after
sha256=in theX-Webhook-Signatureheader using a constant-time comparison.
import crypto from "node:crypto";
function verifyTillorWebhook(rawBody, headerSignature, signingSecret) {
const expected = crypto.createHmac("sha256", signingSecret).update(rawBody).digest("hex");
const received = headerSignature.replace(/^sha256=/, "");
const a = Buffer.from(expected, "hex");
const b = Buffer.from(received, "hex");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}Treat the secret like a password
You can show the signing secret in Tillor via Show signing secret in the webhook table. Store it as a secret (env var, vault) and never log it. Suspect a leak? Delete the webhook and create a new one; that gives you a new secret.
Delivery behaviour and retries
- Reply with a 2xx status to confirm success.
- If a delivery fails (non-2xx, network error or blocked target), Tillor automatically retries the delivery with increasingly long intervals. Every attempt reuses the same payload, the same signature and the same event ID (idempotent).
| Attempt | Wait since previous attempt |
|---|---|
| 1 | immediately |
| 2 | 1 second |
| 3 | 2 seconds |
| 4 | 4 seconds |
| 5 | 8 seconds |
| 6 | 5 minutes |
| 7 | 30 minutes |
| 8 | 2 hours |
| 9 | 6 hours |
| 10 | 24 hours |
- If one of the attempts succeeds (2xx) → the delivery is complete.
- If attempt 10 (after 24 h) also fails → the delivery is permanently marked as failed and not attempted further.
- If the webhook is disabled in the meantime, Tillor stops retrying.
Idempotency
Every retry reuses the same X-Webhook-Event-ID and X-Webhook-Signature. Deduplicate on your side on that header so that a successful retry after a slow 200 does not cause double processing.
Notifications for persistent failures
If deliveries for a webhook endpoint keep failing, Tillor sends at most two notifications per incident (per endpoint):
- After 5 minutes of persistent failures - email to the organisation email address (
supportEmail, otherwiseemail) and to organisation members with permission to change organisation settings, plus push (if enabled) for those members. The email states since when the deliveries have been failing. Dates in the email use the time zone from Settings > General (defaultEurope/Brussels). - On permanent failure - the same recipients receive one notification when the first event in that incident has been marked as permanently failed after all attempts.
You do not get a notification per failed event. As soon as all failed deliveries for an endpoint have been resolved (successfully delivered), a following incident starts again with the same rules.
On Profile menu → Developers, the webhook table shows a compact delivery status per endpoint (numbers waiting for retry and permanently failed). Click the ℹ icon for details, including the full error message and the payload of the most recent attempt.
- Retry failed deliveries (↻): only permanently failed deliveries for that webhook.
- Force retry (⚡): all pending and permanently failed deliveries for that webhook, regardless of scheduled wait times. Tillor starts this in the background so that large queues (e.g. thousands of events) are processed safely.
- Force retry all at the top right next to Add webhook: the same for all enabled webhooks in the organisation.
Event types
Event keys use the format entity:action or entity:subresource:action. The full list, with the permission per event and an example payload, is in the Event index. That page is generated from the code and is therefore always complete. The same keys are in Tillor under Profile menu → Developers at Add webhook → Subscribed events.
Webhooks vs SSE
| Use case | Recommendation |
|---|---|
| Server-side integration | Webhooks - Your server receives POSTs |
| Simple logging | Webhooks - Minimal setup |
| Realtime dashboard, live updates in an app | SSE - One connection, low latency. |
| Many event types, client-side filtering | SSE - Use the events query param to limit traffic. |
Related
- OpenAPI - Full API specification
- HTTP API - REST API and API keys
- SSE - Same events via Server-Sent Events
- Automations - Send your own messages from an automation