Skip to Content

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.

MethodPathDescription
GET/api/orgs/:orgId/webhooksRetrieve webhooks
GET/api/orgs/:orgId/webhooks/delivery-statsRetrieve delivery statistics
POST/api/orgs/:orgId/webhooks/retry-deliveriesRetry 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/:idRetrieve a webhook by ID
POST/api/orgs/:orgId/webhooksCreate a webhook
PATCH/api/orgs/:orgId/webhooks/:idUpdate a webhook
DELETE/api/orgs/:orgId/webhooks/:idDelete 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 - true to 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/json
  • X-Webhook-Event-ID - stable unique ID for this logical delivery (same value on retries)
  • X-Webhook-Signature - HMAC-SHA256 signature of the body, format sha256=<hex> (see Verifying the signature)

Body:

{
  "event": "invoice:created",
  "data": {},
  "timestamp": 1735689600000,
  "traceId": "4bf92f3577b34da6a3ce929d0e0e4736",
  "spanId": "00f067aa0ba902b7",
  "triggeredBy": {
    "id": "usr_63x8jp68is",
    "name": "Niels"
  }
}
FieldTypeDescription
eventstringEvent key (e.g. invoice:created)
dataobjectEvent-specific payload
timestampnumberUnix timestamp (ms) when the event was emitted
traceIdstringOpenTelemetry trace ID of the Tillor action that caused this event. Pass it on to support; it is not a secret.
spanIdstring (optional)Span within that trace
triggeredByobject | nullUser 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:

  1. Compute HMAC-SHA256(signingSecret, raw_request_body) (lowercase hex).
  2. Send that as X-Webhook-Signature: sha256=<hex>.

How you verify:

  1. Read the raw request body (before JSON parsing - otherwise the hash will not match).
  2. Compute HMAC-SHA256(signingSecret, raw_body) yourself in hex.
  3. Compare it with the value after sha256= in the X-Webhook-Signature header 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).
AttemptWait since previous attempt
1immediately
21 second
32 seconds
44 seconds
58 seconds
65 minutes
730 minutes
82 hours
96 hours
1024 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):

  1. After 5 minutes of persistent failures - email to the organisation email address (supportEmail, otherwise email) 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 (default Europe/Brussels).
  2. 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 caseRecommendation
Server-side integrationWebhooks - Your server receives POSTs
Simple loggingWebhooks - Minimal setup
Realtime dashboard, live updates in an appSSE - One connection, low latency.
Many event types, client-side filteringSSE - 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
Was this page helpful?

On this page