API reference
Outgoing webhooks
Outgoing webhooks let Blengi push events to your endpoint when
something interesting happens. Configure them per workspace in the
webhook card on Settings › Connections › Integrations
(/settings/integrations).
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 these headers:
| Header | Value |
|---|---|
Content-Type | application/json |
X-Pitchbar-Signature | t={timestamp},v1={hmac} — Stripe-style timestamped signature |
X-Pitchbar-Webhook-Id | A UUID unique to this delivery. Use it to dedupe at-most-once on your side. |
X-Pitchbar-Event | Event name (e.g. lead.captured) so your router doesn't have to parse the body before dispatching. |
Rotating the secret
If you suspect the secret has leaked, rotate it from
/settings/integrations → the webhook card →
Rotate secret. The new value is shown once
in a modal — copy it immediately into your verifier. The old
secret stops working the moment the rotation completes; in-flight
deliveries still use the old secret until they finish. A
integration.webhook_secret_rotated entry lands in the
audit log.
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. Every delivery carries its own
X-Pitchbar-Webhook-Id; the lead'sidin the body identifies the lead across deliveries (a visitor who submits the form again produces a second delivery for the same lead). - 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",
"lead": {
"id": "0199…",
"email": "alex@example.com",
"phone": "+1…",
"name": "Alex",
"fields": { "company": "Acme" },
"status": "new"
},
"agent_id": "0199…",
"score": 72,
"score_bucket": "high",
"trajectory": [
{ "url": "https://shop.example/pricing", "title": "Pricing", "referrer": "https://www.google.com/", "viewed_at": "2026-05-07T11:58:00+00:00" }
]
}
- lead — the lead as saved.
fieldsholds what the visitor filled into the form beyond name, email and phone. The keys are always present; a value the visitor did not give isnull. - score, score_bucket — the conversation's lead score and its bucket, recomputed just before the delivery (
0andlowwithout a score). See Lead scoring & trajectory. - trajectory — the visitor's last 10 page views, newest first.
There is no timestamp or conversation id in the body; the
t= part of X-Pitchbar-Signature is the send
time.
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 live preview 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.