Help centerAdminDecision maker

Outbound webhooks: push events to your systems

Signed HTTPS POSTs for new leads and pipeline stage changes. Full event catalog, payloads, signatures, and a Ciclo-style setup recipe.

Outbound webhooks let Concierge POST signed JSON events to your own HTTPS endpoint (for example an external sales panel, spreadsheet automation, or internal CRM). They are configured per workspace under API Access → Webhooks. Delivery is best-effort with retries and a delivery log in the console. Lead capture and chat continue while delivery runs in the background.

When to use webhooks

  • Mirror new leads into a panel or Google Sheet in near real time
  • Sync CRM board stage changes (funnel phase) to an external system
  • Alert your own backend when a visitor requests a human
  • Optional: track outbound message delivery status on campaigns

Configure in the console

  1. Open API Access → Webhooks

    You need Admin or Developer access (manage permissions). The page also lists signing secret, test ping, and recent deliveries.

  2. Paste an HTTPS endpoint

    Only public HTTPS URLs are accepted. Private networks, non-HTTPS schemes, and redirects are refused (SSRF guard). Port 443 is the normal case.

  3. Choose events carefully

    Defaults for a new config are lead.captured + handoff.requested. For external funnels, prefer lead.captured + crm.card_moved and leave the rest unchecked.

  4. Enable, save, and send a test

    Saving mints a signing secret (czwh_…) if you did not have one. Use Send test event. It sends type ping and ignores the event checklist so you can verify connectivity before go-live.

  5. Confirm Recent deliveries

    A success row with HTTP 2xx means your endpoint accepted the body. Fix 4xx contract errors before enabling production traffic.

  • Endpoint returns 2xx quickly (under ~10 seconds per attempt)
  • You verify X-Concierge-Signature on every request
  • You ignore unknown event types so catalog growth stays safe
  • You store event id for idempotency (retries may re-deliver)

What most teams should subscribe to

  • lead.captured: assistant captured contact (widget/channel). Upsert a contact/row.
  • crm.card_moved: board card changed stage (pipeline phase). Sync funnel status. This is the phase event for the board; the flat Leads list uses capture events instead of a separate lead.status_changed.

Subscribe later only if needed

  • handoff.requested: visitor asked for a person
  • handoff.claimed / returned / closed / requeued: desk lifecycle
  • crm.session_promoted: conversation first became a board lead
  • crm.workspace_changed: coarse board structure changes
  • dispatch.accepted / sent / delivered / read / failed / expired: outbound message receipts

HTTP request shape

Every delivery is a single JSON POST. Headers carry the event type, event id, and signature. The body is a stable envelope; only data changes by event type.

Request headers
POST /your-hook
Content-Type: application/json
X-Concierge-Event: lead.captured
X-Concierge-Id: evt_1a2b3c4d5e6f
X-Concierge-Signature: t=1721051696,v1=<hmac_hex>
Envelope (every event)
{
  "id": "evt_1a2b3c4d5e6f",
  "type": "lead.captured",
  "createdAt": "2026-07-15T12:34:56.789Z",
  "workspaceId": "acme-1a2b",
  "data": { }
}
  • id: unique event id (use for dedupe if a retry arrives)
  • type: same string as X-Concierge-Event
  • createdAt: ISO timestamp when Concierge built the envelope
  • workspaceId: tenant / workspace id
  • data: event-specific object (below)

Verify the signature

Signatures use HMAC-SHA256 over the string `${unix_timestamp}.${rawBody}` with your workspace secret. The header format is t=<seconds>,v1=<hex>. Compare v1 in constant time. Reject if t is too far from your clock (e.g. more than five minutes) to limit replay.

Node.js verify sketch
import { createHmac, timingSafeEqual } from "node:crypto";

function verify(rawBody, header, secret) {
  const parts = Object.fromEntries(
    header.split(",").map((p) => {
      const [k, v] = p.split("=");
      return [k, v];
    }),
  );
  const expected = createHmac("sha256", secret)
    .update(`${parts.t}.${rawBody}`)
    .digest("hex");
  return timingSafeEqual(
    Buffer.from(parts.v1, "utf8"),
    Buffer.from(expected, "utf8"),
  );
}

Payloads by event

lead.captured

Fired when the assistant captures contact details. source is typically widget or a messaging channel. Fields may be null when the visitor only gave one channel.

data for lead.captured
{
  "lead": {
    "id": "lead_9f8e7d",
    "name": "Jane Doe",
    "email": "jane@example.com",
    "phone": null,
    "otherContact": null,
    "reason": "Wants a demo",
    "preferredChannel": null,
    "source": "widget"
  }
}

crm.card_moved (pipeline phase)

Fired when a card on the CRM board moves to another stage. Map stageId to your funnel labels using the stages defined in the Board for that workspace. actor may be null when the move is system-driven.

data for crm.card_moved
{
  "card": {
    "id": "card_abc123",
    "stageId": "stage_negotiation",
    "revision": 12
  },
  "actor": { "id": "user_…", "name": "Daniel" }
}

handoff.requested

data for handoff.requested
{
  "handoff": {
    "id": "ho_…",
    "reason": "Visitor asked for a person",
    "conversationTitle": "Widget chat"
  }
}

Handoff lifecycle (claimed / returned / closed / requeued)

data for handoff lifecycle events
{
  "handoff": {
    "id": "ho_…",
    "conversationId": "conv_…"
  },
  "actor": { "id": "user_…", "name": "Erika" }
}

crm.session_promoted

data for crm.session_promoted
{
  "session": { "id": "sess_…" },
  "leadId": "lead_…"
}

crm.workspace_changed

Coarse signal that board structure or shared CRM state changed (action + workspaceSequence). Prefer card_moved for phase sync; use this only if you rebuild caches.

dispatch.* (outbound delivery)

data for dispatch.*
{
  "dispatch": {
    "providerMessageId": "wamid.…",
    "channelId": "ch_…",
    "conversationId": "conv_…",
    "recipient": "+55…",
    "status": "delivered"
  }
}

On failure, an extra error string may be present. Status values: accepted, sent, delivered, read, failed, expired. Only genuine forward progress emits a new event.

ping (test only)

data for ping
{
  "message": "This is a test event from Concierge. Your webhook endpoint is reachable."
}

Delivery, retries, and reliability

  • Up to 3 attempts with backoff (about 2s then 8s between tries)
  • About 10 seconds timeout per attempt
  • 4xx responses (except 408/429) stop early; fix your contract
  • Permanent guard errors (bad URL, blocked host, non-HTTPS) skip retries
  • Successful product actions complete independently of your webhook
  • Recent deliveries in the console show status, HTTP code, attempts, and error

Security checklist

  • Use HTTPS only for the receiver
  • Verify signature on every request before trusting data
  • Reject stale timestamps
  • Rotate the secret from the console if it leaks; update your receiver the same day
  • Keep full secrets and full PII payloads out of shared logs

Recipe: external sales panel (e.g. Ciclo-style)

  1. Enable webhooks and set your HTTPS URL
  2. Subscribe only to lead.captured and crm.card_moved
  3. Save → Send test event → confirm 2xx
  4. On lead.captured: upsert contact by email, phone, or lead.id
  5. On crm.card_moved: update phase using card.stageId; store card.id as foreign key if you track board cards
  6. Ignore every other type until you need it