API reference
Outgoing webhooks
Outgoing webhooks let Pitchbar push events to your endpoint when
something interesting happens. Configure them per workspace under
/app/integrations/webhooks.
lead.captured. The
delivery is single-attempt (no retries) with HMAC-SHA256
signing. Conversation-level events
(conversation.started, conversation.message,
conversation.routed) are deferred and not yet emitted.
Configuration
Each webhook subscription has:
- URL โ your endpoint. HTTPS strongly recommended.
- Events โ currently only
lead.captured. - Signing secret โ auto-generated. Used to HMAC the body.
- Active โ toggle.
Headers
The dispatcher sends two headers:
| Header | Value |
|---|---|
Content-Type | application/json |
X-Pitchbar-Signature | t={timestamp},v1={hmac} โ Stripe-style timestamped signature |
Signature verification
The signature is an HMAC-SHA256 of "{timestamp}.{body}"
using the subscription's signing secret. To verify:
// Node
const crypto = require('crypto');
function verify(rawBody, signatureHeader, secret) {
const parts = Object.fromEntries(
signatureHeader.split(',').map(p => p.split('='))
);
const ts = parts.t;
const sig = parts.v1;
const expected = crypto
.createHmac('sha256', secret)
.update(`${ts}.${rawBody}`)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(sig)
);
}
Always use a constant-time comparison
(timingSafeEqual in Node, hash_equals in PHP)
to avoid timing attacks. Reject the delivery if t is
older than ~5 minutes โ replay protection lives on your side.
Delivery semantics
Each delivery is a single HTTP POST with a 5-second timeout. There is no built-in retry: a non-2xx response or timeout drops the event. If your endpoint is briefly down you'll lose that event. Recommended pattern:
- Reply 2xx fast. Buffer to your own queue and process asynchronously.
- Idempotency. Use the event's
occurred_at+datacontents to dedupe โ there's no per-delivery ID yet. - Reconciliation. For business-critical data, periodically pull from the admin lead list rather than relying solely on webhooks.
Event payloads
lead.captured
{
"event": "lead.captured",
"occurred_at": "2026-05-07T12:00:00Z",
"data": {
"lead_id": "01HXY...",
"agent_id": "01HXY...",
"conversation_id": "01HXZ...",
"name": "Alex",
"email": "[email protected]",
"phone": "+1...",
"fields": { "company": "Acme" }
}
}
The exact field set depends on what the visitor filled into the
inline form and any custom fields you've defined on the agent. Keys
are stable; missing values are null rather than absent.
Stripe webhooks (incoming)
These are separate โ Stripe sends to /billing/webhook
and Cashier verifies the standard Stripe-Signature header
using STRIPE_WEBHOOK_SECRET. They drive subscription
state. You don't configure these from the integrations page; they're
a platform-admin concern.
Testing locally
Point a webhook at https://webhook.site or a tunneled
local URL (ngrok). Submit a lead via the playground or the live
widget; the webhook fires within a second.
Roadmap
The webhook surface will expand to include conversation-level events,
a per-delivery ID, and at-least-once retry semantics. Until then,
poll the admin endpoints for state you care about beyond
lead.captured.