BotDesk

Receiving webhooks

Subscribe a URL and receive signed, real-time events from your bots — new conversations, messages and escalations.

BotDesk can push signed HTTP events to your own endpoint as things happen in a conversation — a new conversation starts, a message is persisted, or the bot escalates to a human. This turns the public API from pull-only into a real integration surface: instead of polling, you receive events as they occur.

Outbound webhooks are available on the Premium plan.

Authentication

All management endpoints authenticate with your public API key in the X-API-Key header (the same key used by the rest of the public API). Each endpoint is scoped to your tenant — you can only ever see or modify your own subscriptions.

Managing subscriptions

Base path: /v1/public/v1/webhooks

Method Path Purpose
POST /webhooks Create a subscription. Returns the signing secret once.
GET /webhooks List your subscriptions.
PATCH /webhooks/{id} Update url, event_types, or is_active.
DELETE /webhooks/{id} Delete a subscription.
POST /webhooks/{id}/rotate-secret Generate a new signing secret. Returns it once.
GET /webhooks/{id}/deliveries Inspect delivery history (status, response code, attempts).

Creating a subscription

POST /v1/public/v1/webhooks
X-API-Key: <your-api-key>
Content-Type: application/json

{
  "url": "https://your-app.example.com/botdesk/webhooks",
  "event_types": ["conversation.created", "message.created", "escalation.requested"]
}

Response (201 Created):

{
  "id": "…",
  "url": "https://your-app.example.com/botdesk/webhooks",
  "event_types": ["conversation.created", "message.created", "escalation.requested"],
  "is_active": true,
  "created_at": "…",
  "updated_at": "…",
  "secret": "whsec_…"
}
  • url must be HTTPS and must resolve to a public address. URLs pointing at loopback, link-local, private, or otherwise reserved ranges are rejected with 422 (SSRF protection), both at creation and on any later URL change.
  • Only the three event types below are accepted; an unknown value returns 422.
  • secret is returned exactly once, here and on rotate-secret. Store it securely — it is used to verify signatures and is never shown again.

Events

Every delivery is a JSON POST with a versioned envelope:

{
  "event": "message.created",
  "event_id": "b3f1c2a4-…",
  "version": "v1",
  "created_at": "2026-07-29T12:34:56.789012+00:00",
  "data": { }
}
Field Meaning
event The event type (one of the three below).
event_id Stable id for this logical event. Use it to dedupe (see below).
version Envelope version, currently v1.
created_at ISO-8601 UTC timestamp of when the event was rendered.
data Event-specific payload.

conversation.created

Fires when a new conversation is created on any channel (widget, WhatsApp, Telegram, Instagram).

{ "conversation_id": "…", "channel": "whatsapp", "bot_id": "…" }

message.created

Fires whenever a message is persisted in a conversation, for both the inbound user turn and the outbound bot turn.

{ "conversation_id": "…", "message_id": "…", "role": "user", "content": "…" }

role is user or assistant. Note: for the widget channel the assistant message is persisted first as an empty placeholder and its text is finalized a moment later; the event carries the placeholder body at persist time. Reconcile on message_id if you need the finalized reply.

escalation.requested

Fires when the bot escalates a conversation to a human (explicit or implicit trigger). contact is included when contact information was captured.

{ "conversation_id": "…", "contact": { "email": "customer@example.com" } }

Verifying signatures

Every delivery carries these headers:

Header Value
X-BotDesk-Signature sha256=<hex> — HMAC-SHA256 of the signed message.
X-BotDesk-Timestamp Unix timestamp (seconds) used in the signed message.
X-BotDesk-Event The event type, mirroring event in the body.
X-BotDesk-Delivery Unique id for this delivery attempt.

The signed message is the timestamp, a literal ., and the raw request body:

signed_message = f"{X-BotDesk-Timestamp}." + <raw request body bytes>
expected       = "sha256=" + HMAC_SHA256(secret, signed_message).hexdigest()

Compare expected against X-BotDesk-Signature using a constant-time comparison. Sign against the raw bytes you received — do not re-serialize the JSON first, or whitespace/key-ordering differences will break the check.

Reject deliveries whose X-BotDesk-Timestamp is outside your tolerance (e.g. ±5 minutes) to bound replay.

Python example:

import hashlib
import hmac

def verify(secret: str, headers: dict, raw_body: bytes) -> bool:
    timestamp = headers["X-BotDesk-Timestamp"]
    message = f"{timestamp}.".encode() + raw_body
    expected = "sha256=" + hmac.new(secret.encode(), message, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, headers["X-BotDesk-Signature"])

Delivery, retries, and deduplication

  • Success is any 2xx response. Respond quickly (within ~10 seconds) and do slow work asynchronously — BotDesk uses a short timeout and does not follow redirects.
  • Retries use a fixed backoff schedule after the first attempt: 0s, 1m, 5m, 30m, 2h, 6h. After 6 failed attempts the delivery is marked dead and is not retried again. Inspect state via GET /webhooks/{id}/deliveries.
  • Deduplicate on event_id. The same logical event carries one stable event_id across every retry, and across every subscription it fans out to. Treat delivery as at-least-once: an event may arrive more than once, so make your handler idempotent keyed on event_id.
  • Ordering is not guaranteed. Use created_at / message_id if you need to order events on your side.

Rotating and disabling

  • POST /webhooks/{id}/rotate-secret issues a new secret and invalidates the old one. Update your verifier before (or immediately after) rotating.
  • Set is_active: false via PATCH to pause deliveries without deleting the subscription; set it back to true to resume.