Webhooks
Webhooks send an HTTP POST request to your system when a queue event happens. Events include a ticket joining a queue, being called, or being completed.
Getting started
Section titled “Getting started”- Open admin.jonot.io/settings/integrations.
- Click Add endpoint.
- Enter a public HTTPS URL that accepts requests on your server.
- Choose which events to subscribe to.
- Click Save.
Copy and store your signing secret when it appears. Jonot shows it only once. To check that Jonot can reach your endpoint, use the Send test request button on the endpoint edit page.
Event types
Section titled “Event types”| Event name | When Jonot sends it |
|---|---|
ticket.joined | A customer joins a queue |
ticket.called | A staff member calls a ticket to the service desk |
ticket.completed | A ticket is marked complete |
ticket.cancelled | A customer or staff member cancels a ticket |
ticket.skipped | A ticket is skipped (a recallable deferral) |
ticket.no_show | A called or skipped ticket is confirmed as a no-show |
queue.status_changed | A queue’s status changes (ACTIVE, PAUSED, or CLOSED) |
Payload format
Section titled “Payload format”Every delivery is an HTTP POST with:
Content-Type: application/jsonX-Jonot-Event: ticket.calledX-Jonot-Delivery-Id: <uuid>X-Jonot-Timestamp: 2024-06-01T12:00:00.000ZX-Jonot-Signature: v1,<base64-hmac-sha256>X-Jonot-Payload-Version: v1The default body is a JSON object. The contents of the payload field depend on the event:
ticket.joined
{ "deliveryId": "<uuid>", "event": "ticket.joined", "timestamp": "2024-06-01T12:00:00.000Z", "org": { "id": "org_…", "name": "My Org" }, "payload": { "ticket": { "id": "tkt_…", "queueId": "q_…" }, "queue": { "id": "q_…", "name": "Main Queue" }, "location": { "id": "loc_…", "name": "Downtown" } }}ticket.called / ticket.completed / ticket.skipped / ticket.no_show
{ "deliveryId": "<uuid>", "event": "ticket.called", "timestamp": "2024-06-01T12:00:00.000Z", "org": { "id": "org_…", "name": "My Org" }, "payload": { "ticket": { "id": "tkt_…", "number": 42, "status": "CALLED", "queueId": "q_…", "createdAt": "2024-06-01T11:58:00.000Z", "updatedAt": "2024-06-01T12:00:00.000Z" } }}ticket.cancelled
{ "deliveryId": "<uuid>", "event": "ticket.cancelled", "timestamp": "2024-06-01T12:00:00.000Z", "org": { "id": "org_…", "name": "My Org" }, "payload": { "ticketId": "tkt_…", "queueId": "q_…" }}queue.status_changed
{ "deliveryId": "<uuid>", "event": "queue.status_changed", "timestamp": "2024-06-01T12:00:00.000Z", "org": { "id": "org_…", "name": "My Org" }, "payload": { "queueId": "q_…", "status": "PAUSED" }}Verifying the signature
Section titled “Verifying the signature”Jonot signs every delivery with HMAC-SHA256 over the string:
<deliveryId>.<timestamp>.<body>To verify in Node.js (≥18):
import { createHmac, timingSafeEqual } from "node:crypto";
/** * Returns true when the signature header is valid and the timestamp is * within 5 minutes of now. Throws for malformed input. */function verifySignature(secret, deliveryId, timestamp, body, header) { // Replay-attack guard: reject deliveries older than 5 minutes. const ageMs = Date.now() - new Date(timestamp).getTime(); if (Math.abs(ageMs) > 5 * 60 * 1000) return false;
const expected = "v1," + createHmac("sha256", secret) .update(`${deliveryId}.${timestamp}.${body}`) .digest("base64");
// timingSafeEqual prevents timing-oracle attacks. // Buffers must be the same length — if lengths differ the signature is // invalid, but we still compare a dummy value to keep constant time. const expectedBuf = Buffer.from(expected); const headerBuf = Buffer.from(header); if (expectedBuf.length !== headerBuf.length) return false; return timingSafeEqual(expectedBuf, headerBuf);}Payload templates
Section titled “Payload templates”You can replace the default JSON body with a custom template. Templates use this Mustache-lite syntax:
{{ path.to.value }}— Jonot inserts the value and escapes it for the content type. It uses JSON string escaping forapplication/json, percent encoding forapplication/x-www-form-urlencoded, and no escaping fortext/plain.{{{ path.to.value }}}— Jonot inserts the value without escaping it.
Example template for application/json (subscribed to ticket.joined):
{ "type": "{{ event }}", "ticketId": "{{ payload.ticket.id }}", "queueName": "{{ payload.queue.name }}"}Template editor
Section titled “Template editor”The payload template field is a full code editor with:
- Syntax highlighting and bracket matching for JSON templates.
- Variable list — a row of buttons for the variable paths available to your selected events. Click a button to insert a
{{ path }}token at the cursor. - Live validation — as you type, the editor checks variable paths. It also checks JSON structure when the content type is
application/jsonand the template has no raw{{{ }}}tags. The editor marks problems in the code and lists them below it:- Error — a path that is unknown in all of your selected events.
- Warning — a path that exists only for some of your selected events (it will be empty for deliveries of the other events).
The server runs the same validation rules when you save.
Delivery retries
Section titled “Delivery retries”Cloudflare Queues retries a failed delivery up to 3 times, with a longer delay before each retry. A delivery fails when the endpoint returns a status outside the 2xx range or has a connection error. If the initial attempt and all 3 retries fail, the delivery moves to the dead-letter queue and the endpoint’s consecutive failures count increases.
Jonot automatically disables an endpoint after 20 consecutive failures. You can re-enable it from the endpoint edit page. This resets the count to zero.
Delivery history
Section titled “Delivery history”The Deliveries tab for each endpoint shows delivery attempts from the last 30 days. Each record includes the event type, HTTP status, attempt count, and timestamp. Use the Load more button to view older records within that period.
Limits
Section titled “Limits”| Limit | Value |
|---|---|
| Endpoints per organisation | 5 |
| Custom headers per endpoint | 10 |
| Header value length | 1 024 bytes |
| Payload template size | 16 KB |
| Delivery timeout | 10 s |
| Delivery history retention | 30 days |
| Max delivery rate per organisation | 120 / 60 s |
Rotating the signing secret
Section titled “Rotating the signing secret”- Open the endpoint edit page.
- Click Rotate signing secret.
- Confirm the rotation in the dialog.
- Copy the new secret immediately. Jonot shows it once and cannot recover it. If you close the dialog without storing it, rotate the secret again to get a new value.
- Update your server to verify signatures with the new secret.
The old and new secrets do not work at the same time. The old secret stops verifying requests as soon as you confirm the rotation. Update the system that receives your webhooks immediately after rotating.
The UI shows Active secret: ····XXXX next to the Rotate button. The last four characters help you confirm which secret is active after a rotation.
Signing-secret hygiene
Section titled “Signing-secret hygiene”When to rotate:
- Suspected exposure: the secret appeared in a log file, was shared with a departing employee, or was captured in a screen recording.
- Regular rotation: changing the secret periodically limits how long an exposed secret can be used.
- Access changes: rotate after changing which services can read the secret.
How to update the receiver:
- Rotate in the admin UI and copy the new secret.
- Update the secret used by the receiving system. Store it in an environment variable, secrets manager, or another secure secret store.
- Deploy the updated receiving system.
- Confirm the next delivery succeeds in the Deliveries tab.
If you lost the new secret before saving it:
Rotate again. Each rotation generates a new random secret. Jonot cannot recover the previous readable value because the server stores only an encrypted form.