Skip to content

Webhooks

Webhooks let your systems receive real-time HTTP POST notifications when queue events happen — a ticket joins, is called, completes, and so on.

  1. Open admin.jonot.io/settings/integrations.
  2. Click Add endpoint.
  3. Enter a public HTTPS URL your server listens on.
  4. Choose which events to subscribe to.
  5. Click Save.

Save your signing secret — it is shown once. To verify your endpoint is reachable, use the Send test request button on the endpoint edit page.

Event nameWhen it fires
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 envelope. The payload field shape varies by 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 Mustache-lite syntax:

  • {{ path.to.value }} — substituted and escaped for the content type: JSON-string escaped for application/json, percent-encoded for application/x-www-form-urlencoded, raw for text/plain
  • {{{ path.to.value }}} — substituted raw (no escaping)

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 palette — an always-visible row of insert buttons, one per available variable path for your selected events. Click a button to insert a {{ path }} token at the cursor.
  • Live validation — as you type, the editor checks both JSON structure (when the content type is application/json and no raw {{{ }}} tags are present) and variable paths. Diagnostics appear as inline markers in the editor and as a summary banner 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).

Validation also runs at save time on the server — the editor feedback mirrors the server rules exactly.

Failed deliveries (non-2xx or connection error) are retried up to 3 times with exponential back-off by Cloudflare Queues. After 3 failures the delivery lands in the dead-letter queue and the endpoint's consecutive failures counter increments.

Once an endpoint accumulates 20 consecutive failures it is automatically disabled. Re-enable it from the endpoint edit page; the counter resets to zero.

The Deliveries tab on each endpoint shows the last 30 days of delivery attempts: event type, HTTP status, attempt count, and timestamp. Use the Load more button to page through older records.

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 — it is shown once and cannot be recovered. If you dismiss the dialog without saving it, you must rotate again to obtain a new plaintext.
  5. Update your server to verify signatures with the new secret.

There is no overlap window. The old secret stops verifying as soon as you confirm rotation. Plan to deploy the new secret to your receiver immediately after rotating.

The UI shows Active secret: ····XXXX (the last four characters) next to the Rotate button so you can verify which secret is currently in effect — useful for confirming that your receiver and the server are in sync after a rotation.

When to rotate:

  • Suspected compromise: the secret appeared in a log file, was shared with a departing employee, or was captured in a screen recording.
  • Routine hygiene: periodically rotating limits the blast radius of an undetected exposure.
  • After any change to which services can read the secret (key management rotation).

How to update the receiver:

  1. Rotate in the admin UI and copy the new secret.
  2. Update the secret in your receiver's secret store (environment variable, secrets manager, etc.).
  3. Deploy the updated receiver.
  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. The previous plaintext is not recoverable — only an AEAD-encrypted form is stored server-side.