Webhooks

Webhooks let external systems react to events in BottleCRM in near real time. Configure an endpoint URL in Settings → Integrations → Webhooks, and BottleCRM will POST a signed JSON payload whenever the subscribed events fire.

Events

Event Fired when
lead.created A lead is created (any source)
lead.converted A lead is converted to an account/contact/opportunity
opportunity.stage_changed An opportunity moves between pipeline stages
opportunity.won An opportunity is closed as won
case.created A new support ticket is opened
case.escalated A ticket is escalated past its SLA
invoice.sent An invoice is emailed to a customer
invoice.paid An invoice is marked as paid

Payload format

{
  "id": "evt_1a2b3c…",
  "type": "lead.converted",
  "created_at": "2026-04-12T15:32:11Z",
  "org_id": "ab12-cd34-…",
  "data": {
    "lead": { "id": "…", "title": "…", "email": "…" },
    "account": { "id": "…", "name": "…" },
    "contact": { "id": "…", "email": "…" },
    "opportunity": { "id": "…", "name": "…", "amount": 24000 }
  }
}
  • id is unique per event — use it for idempotency on your side.
  • org_id lets multi-tenant consumers route the event correctly.
  • data is the same shape as the corresponding API resource(s) at the moment the event fired.

Verifying signatures

Every request includes an X-BottleCRM-Signature header containing an HMAC-SHA256 of the raw request body, keyed by the webhook's signing secret (shown once when you create the webhook):

X-BottleCRM-Signature: sha256=8f8b7c…

A Python verifier:

import hmac, hashlib

def verify(body_bytes: bytes, header: str, secret: str) -> bool:
    expected = "sha256=" + hmac.new(
        secret.encode(), body_bytes, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, header)

A Node.js verifier:

import crypto from "node:crypto";

export function verify(body, header, secret) {
  const expected = "sha256=" + crypto
    .createHmac("sha256", secret)
    .update(body)
    .digest("hex");
  return crypto.timingSafeEqual(
    Buffer.from(expected), Buffer.from(header)
  );
}

Delivery semantics

  • At-least-once — BottleCRM may retry a delivery on 5xx or timeout. De-duplicate on the id field.
  • Retries — exponential backoff at 1m, 5m, 30m, 2h, 6h. After the final failure the event is parked and surfaced in the webhook's Recent deliveries panel for manual re-send.
  • Timeout — your endpoint must respond within 10 seconds. Do heavy work in a background job.
  • Order — events are dispatched in the order they fire but parallel deliveries may arrive out of order. Use timestamps if order matters.

Testing locally

The webhook UI has a Send test event button that delivers a synthetic payload for any subscribed type. For development against a local backend, use a tunnel (ngrok, Cloudflare Tunnel) to expose your handler to the running CRM.