Skip to content

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.

  1. Open admin.jonot.io/settings/integrations.
  2. Click Add endpoint.
  3. Enter a public HTTPS URL that accepts requests on your server.
  4. Choose which events to subscribe to.
  5. 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 nameWhen Jonot sends it
ticket.joinedA customer joins a queue
ticket.calledA staff member calls a ticket to the service desk
ticket.completedA ticket is marked complete
ticket.cancelledA customer or staff member cancels a ticket
ticket.skippedA ticket is skipped (a recallable deferral)
ticket.no_showA called or skipped ticket is confirmed as a no-show
queue.status_changedA queue’s status changes (ACTIVE, PAUSED, or CLOSED)

Every delivery is an HTTP POST with:

Content-Type: application/json
X-Jonot-Event: ticket.called
X-Jonot-Delivery-Id: <uuid>
X-Jonot-Timestamp: 2024-06-01T12:00:00.000Z
X-Jonot-Signature: v1,<base64-hmac-sha256>
X-Jonot-Payload-Version: v1

The 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"
}
}

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);
}

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 for application/json, percent encoding for application/x-www-form-urlencoded, and no escaping for text/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 }}"
}

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/json and 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.

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.

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.

LimitValue
Endpoints per organisation5
Custom headers per endpoint10
Header value length1 024 bytes
Payload template size16 KB
Delivery timeout10 s
Delivery history retention30 days
Max delivery rate per organisation120 / 60 s
  1. Open the endpoint edit page.
  2. Click Rotate signing secret.
  3. Confirm the rotation in the dialog.
  4. 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.
  5. 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.

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:

  1. Rotate in the admin UI and copy the new secret.
  2. Update the secret used by the receiving system. Store it in an environment variable, secrets manager, or another secure secret store.
  3. Deploy the updated receiving system.
  4. 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.