Skip to main content
Webhooks let you act on events in miniDesk without polling. When a conversation is created, a message arrives, a status changes or a conversation is assigned, we send a POST request to a URL you choose.

Events

miniDesk sends four event types. Every payload reuses the /v1 shapes, so what a trigger gives you matches what the API returns: data.conversation is the conversation as GET /v1/tickets/{id} returns it, without its messages, and data.message is one item of that conversation’s messages. Every payload has the same envelope: Ids are plain UUIDs. A payload never contains internal notes, AI drafts or reasoning, knowledge base text, attachment links, or team members’ emails.

conversation.created

Fires when a customer starts a new conversation, from any channel including the API. It is sent after triage has set category, priority and language (or failed, or found AI turned off), so you can filter on those fields. It does not fire for:
  • conversations the team starts, from the dashboard or through the API with first_message_from: "team";
  • spam, and mail sent by machines (auto-replies, mailing lists);
  • conversations brought in by the importer.
A chat held until the visitor leaves an email address fires when it is released. If a person restores a conversation our spam filter had filed, it fires then. A conversation a person filed as spam and later restores does not fire again: it was announced when it arrived.
The first message is included in conversation.created. It does not also fire message.received.

message.received

Fires when a customer writes in an existing conversation. Never fires for team replies, AI replies, internal notes, or messages sent through the API. Does not fire for conversations filed as spam, or while a chat is held for the visitor’s email address.

conversation.status_changed

Fires when the status changes, whoever changed it: a team member, the customer replying, our AI, an API key, or the system.

conversation.assigned

Fires when the assignee changes, including when a conversation becomes unassigned. change.field is "assignee". change.from and change.to are member ids, as GET /v1/members lists them, and null means nobody. The conversation object does not carry the assignee: read it from change.to.

Headers

Every webhook request carries these headers:
  • Content-Type: application/json
  • User-Agent: miniDesk-Webhooks/1
  • miniDesk-Event-Id: the event id from the payload
  • miniDesk-Event-Type: the event type
  • miniDesk-Signature: the signature of the payload (see below)

Verifying the signature

Each webhook request is signed with your webhook’s secret, the whole whsec_... string. Verify the signature to make sure the request came from miniDesk. The miniDesk-Signature header looks like this:
t is the Unix time, in seconds, when the request was sent. v1 is the HMAC-SHA256 hex digest of the string <t>.<raw body>, keyed with your secret. Verify against the raw body, exactly as it arrived. Parsing the JSON and serialising it again changes the bytes, and the signature will not match. In Express, for example, read the body with express.raw({ type: "application/json" }). Both examples below return false and never throw: on a missing or malformed header, a t that is not a number, a v1 that is not 64 hex characters, or a request older than 5 minutes. They compare in constant time, on two values of the same length.

Node.js example

Python example

Refuse requests older than 5 minutes, as the examples do: they may be replays.

Delivery and retries

We POST to your URL with a 10 second timeout. Redirects are not followed. Any 2xx response is a success. If a delivery fails, we try again after 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours. Six tries in total, over about 9 hours. After that the delivery is marked as failed. The delivery history in the dashboard keeps every try: the time, the HTTP code and how long it took. What your server answered is kept as plain text, cut to 2 KB. Response headers are not kept. When there was no HTTP answer, the history shows one of four reasons, never the raw network error:
  • Could not connect: the address did not answer, or the name no longer resolves.
  • Timed out: no answer within 10 seconds.
  • Address not allowed: the URL now points somewhere we refuse to call (see URL requirements). We check again before every try.
  • Could not sign: the webhook’s stored secret could not be opened, so nothing was sent. Delete the webhook and create it again.
A failed delivery can be sent again from the dashboard with Resend: one more try with the same event id and payload. A paused webhook cannot resend or send a test event until it is resumed.

Pausing

When a webhook has had no successful delivery for 3 days while deliveries kept failing, we pause it and email the workspace owners once. A paused webhook gets no new deliveries. Resume it from Settings → Webhooks in the dashboard. A webhook created by an API key can also be resumed by registering the same URL again with that key (see Create a webhook). An owner can also pause a webhook by hand, for example after an unexpected “webhook added” email. That pause stays until an owner resumes it. An API key can neither resume nor delete a webhook an owner paused: registering the URL again or deleting the webhook answers 409. Otherwise the key could delete the paused webhook and register a fresh, active one.

At least once, no ordering

Delivery is at least once. You may receive the same event more than once. Deduplicate on the id field. Events may arrive out of order. A quick second customer message can arrive before conversation.created while triage runs. Use created_at to order events.

Loop prevention

A webhook created by an API key never receives events caused by that same key. A workflow that creates a conversation, changes a status, or reassigns through its own key does not trigger itself. Webhooks added in the dashboard belong to no key and receive everything. Events caused by a different key do fire, and data.actor names that key.

URL requirements

  • https:// only. Any port is allowed.
  • No user name or password in the URL.
  • Public addresses only. We refuse private and internal addresses, minidesk.ai and its subdomains, and our own servers’ addresses. We check when you save the webhook and again before every try, on the address we actually connect to.
  • Up to 10 webhooks per workspace.
Webhook URLs are secrets: a Zapier catch hook or an n8n webhook URL works on its own, with no other key. We never log webhook URLs, and the emails we send owners name only the host (hooks.example.com), never the full URL.

Managing webhooks

From the dashboard

Go to Settings → Webhooks (owners only). Owners see every webhook in the workspace, whoever added it. Add a webhook by pasting a URL and choosing which events to receive. The secret is shown once, when the webhook is created. After that it is masked. Lost secret: delete the webhook and create it again.
The Add webhook window: a URL and two of the four events ticked

From the API

An API key can register webhooks with the /v1/webhooks endpoints, and list, read and delete only the ones it registered. A webhook added in the dashboard or by another key is not listed, and its id answers 404. Workspace owners get an email every time an API key adds a webhook, naming the key and the receiving host. Registering the same URL again does not email them again. Revoking an API key deletes the webhooks it created.

Create a webhook

Returns 201 Created with the webhook and its secret:
Keep the secret. It is never returned after creation. If this key already registered the exact same URL, no second webhook is made. You get the same webhook back with 200 OK: same id and delivery history, the events you sent replace the old ones, a new secret, and the webhook is active again if it was paused for failing. If an owner paused it by hand, the answer is 409 Conflict and the webhook stays paused:

List webhooks

Returns the webhooks this key registered, newest first. No secret, masked or otherwise.

Get a webhook

Returns the same object as one item of the list, without a secret. status: "paused" means nothing is being sent. Returns 404 if the webhook does not belong to this API key.

Delete a webhook

Returns:
Deleting a webhook removes it permanently. Its delivery history goes with it. If an owner paused the webhook by hand, the answer is 409 Conflict and the webhook stays. Only an owner can resume or delete it, in Settings → Webhooks:

Error responses

A refused URL returns 400 Bad Request, with one message whatever the reason:
The reason can be a URL that is not https://, carries a user name or password, points at a private address or at one of ours, or does not resolve. 409 Conflict means the workspace already has 10 webhooks, or an owner paused the webhook you are registering again or deleting. 404 Not Found means the webhook does not exist or belongs to another key.

Test events

Send a test event from Settings → Webhooks in the dashboard. The test event has type ping and carries no conversation:
The result appears in the delivery history like any other delivery.