Help
Developers

Webhooks and events

Get told when something happens: signed webhooks, an event log to catch up from, and a stream to wait on.

Three ways to hear about the same events, all carrying the same envelope:

  • Webhooks push. Subscribe an endpoint to event types and ShowPilot posts each event to it, signed, with retries.
  • The log lets you catch up. GET /v1/events?since=<id> returns events in order from where you stopped.
  • The stream lets you wait. GET /v1/events/stream is server-sent events for agents that cannot host a receiver.

Endpoints can also be added by hand in Settings → Webhooks, which shows each endpoint's recent deliveries and lets you replay one. The API and the screen manage the same subscriptions.

The envelope

{
  "id": "1042",
  "type": "meeting.booked",
  "occurredAt": "2026-09-25T14:02:11.000Z",
  "data": { "meetingId": "…", "visitorId": "…", "startAt": "2026-09-29T15:00:00.000Z", "attendee": { "email": "sam@example.com", "name": "Sam" } },
  "links": { "meeting": "/v1/meetings/…", "visitor": "/v1/visitors/…" }
}

id is monotonic within your workspace, so it doubles as the log cursor and as a deduplication key: delivery is at least once. links are API paths you can fetch with the same key.

Event types

TypeFires whendata carries
conversation.startedA chat or voice session opensconversationId, agentId, visitorId, channel, pageUrl, referrer
conversation.engagedThe visitor sends a second messageconversationId, agentId, visitorId, channel, pageUrl
conversation.endedQuiet for 30 minutes (the worker closes it)conversationId, agentId, visitorId, channel, startedAt, endedAt, durationSecs, reason
visitor.identifiedAn anonymous visitor gives an emailvisitorId, email, name, source, account, crm
lead.identifiedA visitor becomes a lead through a form or the agentleadId, visitorId, conversationId, agentId, email, name, source, verdict
intent.hotThe agent scores a visitor as hotvisitorId, conversationId, agentId, email, intentScore, account, routedTo
meeting.bookedA booking is confirmedmeetingId, visitorId, conversationId, agentId, leadId, provider, startAt, attendee
form.submittedAny form completesformId, formName, formKind, outcome, visitorId, email, answers
segment.enteredA visitor now matches a saved segmentsegmentId, segmentName, visitorId, email, displayName, account, crm
segment.leftA visitor no longer matchessame as segment.entered
offer.shownThe agent puts an offer in front of a visitorofferId, presentation, agentId, visitorId, pageUrl
offer.acceptedThey take itsame as offer.shown
offer.declinedThey say nosame as offer.shown
tour.playedA visitor starts a tourtourId, tourPublicId, sittingId, visitorId, email, source, stepCount
tour.step_viewedOne per steptourId, tourPublicId, sittingId, visitorId, email, stepIndex, screenId
tour.cta_clickedA call to action inside a tourtourId, tourPublicId, sittingId, visitorId, email, stepIndex, ctaId, url
tour.completedA visitor reaches the endtourId, tourPublicId, sittingId, visitorId, email, durationSecs, stepsViewed
experience.viewedA shared experience page opens (no visitor yet; the tours inside carry one)experienceId, experiencePublicId, name, shareHash, slug, referrer
checklist.task_completedOne task in a hub checklisttourId, sittingId, visitorId, checklistId, taskId
checklist.convertedThe checklist call to actiontourId, sittingId, visitorId, checklistId, ctaId, url
follow_up.createdThe agent promises to follow up on a questionfollowUpId, visitorId, conversationId, agentId, question
follow_up.answeredA rep answers itfollowUpId, visitorId, conversationId, question, answer, emailed

The operator's own tour plays from the dashboard never fire events. Subscribe with ["*"] to receive every type, including ones added later.

Verifying a delivery

Each POST carries X-ShowPilot-Timestamp (unix seconds), X-ShowPilot-Signature (sha256= followed by an HMAC-SHA256 of <timestamp>.<raw body> using your subscription's secret), X-ShowPilot-Event (the type) and X-ShowPilot-Delivery (the delivery id). Compute the HMAC over the raw body exactly as received, compare in constant time, and reject timestamps older than five minutes.

import { createHmac, timingSafeEqual } from 'node:crypto';

export function verify(req, rawBody, secret) {
  const ts = req.headers['x-showpilot-timestamp'];
  const expected = 'sha256=' + createHmac('sha256', secret).update(`${ts}.${rawBody}`).digest('hex');
  const given = req.headers['x-showpilot-signature'] ?? '';
  const fresh = Math.abs(Date.now() / 1000 - Number(ts)) < 300;
  return fresh && given.length === expected.length && timingSafeEqual(Buffer.from(given), Buffer.from(expected));
}

Respond with any 2xx within ten seconds. Anything else is retried with backoff over five attempts, and the delivery log shows each outcome. Twenty failures in a row pause the subscription; set its status back to active to resume, or replay a delivery to test the fix.

Setting one up

  1. POST /v1/webhooks with your url and the event types. The response is the only time the secret is shown.
  2. Trigger something, or replay a delivery, and watch GET /v1/webhooks/{id}/deliveries.
  3. For a receiver you cannot host yet, read GET /v1/events on a schedule instead; the since cursor makes it exactly as complete.

Subscriptions and the log need a key with the webhooks:manage scope. Private and local addresses are refused as endpoints.

Creating, changing or removing a subscription and replaying a delivery each take an Idempotency-Key header, so a retried request never makes a second subscription or a second replay. See Errors.

On this page