API reference
Widget API
The Widget API is the public HTTP surface the bundled JavaScript talks to. You normally don't call it yourself โ the widget loader does โ but the contract is documented here so you can build custom clients, audit traffic, or simulate the widget for testing.
All endpoints are under /api/v1/widget. Authentication is
a signed JWT issued by /init. CORS is permissive on
POST for cross-origin embeds.
POST POST /v1/widget/init
Boots the widget for a visitor. No auth โ but the request's
Origin header must match the agent's
allowed_origins (see Allowed origins).
Request
POST /api/v1/widget/init
Origin: https://your-site.com
Content-Type: application/json
{
"agent_id": "01HXY...",
"page_url": "https://your-site.com/pricing",
"anon_id": "anon_abc123" // optional; persists visitor across reloads
}
Response (200)
{
"data": {
"conversation_id": "01HXZ...",
"visitor_id": "01HXY...",
"anonymous_id": "anon_abc123",
"jwt": "eyJhbGciOiJIUzI1NiI...",
"expires_at": "2026-05-07T13:00:00Z",
"agent": {
"id": "01HXY...",
"name": "Aria",
"persona": { "name": "Aria", "tone": "friendly" },
"theme": { "primary": "#111827", ... },
"starter_prompts": [ "..." ],
"language_default": "en"
},
"branding": { "show": true, "label": "...", "url": "...", "logo_url": "...", "display_mode": "logo_only" },
"behavior_rules": [ ... ],
"messages": [ ... ], // last 30 messages of the resumed conversation
"reverb": { "app_key": "...", "host": "...", "port": 8080, "scheme": "wss" }
}
}
Error responses
| Status | Code | Cause |
|---|---|---|
| 404 | agent_not_found | Agent doesn't exist or isn't published. |
| 403 | origin_forbidden | Origin not in allowed_origins. |
| 429 | plan_limit_reached | Workspace exceeded its monthly conversation quota. |
| 429 | (throttled) | Per-IP rate limit hit (60 rpm by default). |
POST POST /v1/widget/messages/stream
The streaming endpoint. SSE response. Auth: Authorization: Bearer
<jwt>. Use this for the visitor experience โ every other
method is sync and slower.
Request
POST /api/v1/widget/messages/stream
Authorization: Bearer eyJhbGciOiJIUzI1NiI...
Content-Type: application/json
{
"message": "What's your refund policy?",
"page_url": "https://your-site.com/pricing",
"page_context": { ... } // optional; structured data extracted from the current page
}
Response (Server-Sent Events)
HTTP/1.1 200 OK
content-type: text/event-stream
data: {"event":"token","token":"Our "}
data: {"event":"token","token":"refund "}
data: {"event":"token","token":"policy is 30 days "}
data: {"event":"citations","citations":[{"id":1,"url":"https://your-site.com/refunds"}]}
data: {"event":"done","conversation_id":"01HXZ..."}
Token events come fastest in the first few hundred ms โ that's the
1-second-to-first-token target on the hot path. citations
event arrives once after streaming completes; done closes
the stream.
POST POST /v1/widget/messages
Sync version of /messages/stream. Returns the full response
in one JSON payload. Slower (visitor waits for the full response) but
easier to integrate with non-browser clients.
Response
{
"data": {
"message_id": "01HXZ...",
"conversation_id": "01HXZ...",
"content": "Our refund policy is 30 days...",
"citations": [{"id": 1, "url": "..."}],
"low_confidence": false
}
}
POST POST /v1/widget/leads
Submit captured contact info. Auth: same JWT as messages.
POST /api/v1/widget/leads
Authorization: Bearer eyJhbGciOiJIUzI1NiI...
{
"name": "Alex",
"email": "[email protected]",
"phone": "+1...",
"fields": { "company": "Acme" } // any agent-defined custom fields
}
Dedupes on (agent_id, email): repeat submissions update the existing lead instead of creating a new one. Rate-limited at 5 requests per JWT per window โ abuse-resistant.
POST POST /v1/widget/events
Lightweight client-side analytics. The widget calls this with telemetry events (launcher opened, CTA clicked, dismissed, scroll trigger fired). Auth: JWT. Rate-limited.
{
"event": "cta.click",
"rule_id": "01HXY...",
"metadata": { ... }
}
JWT format
HS256, signed with WIDGET_JWT_SECRET. Claims:
{
"iss": "pitchbar",
"iat": 1714900000,
"exp": 1714903600, // 60 minutes
"agent_id": "01HXY...",
"visitor_id": "01HXY...",
"conversation_id": "01HXZ..."
}
Tokens are scoped to a single conversation. Re-init to get a fresh
token for a new conversation. Verifying happens in
WidgetJwt::verify() โ invalid signatures, expired tokens,
or tampered claims all return 401.
Rate limits
| Endpoint | Limit | Key |
|---|---|---|
/init | 60 rpm | per IP + agent_id (throttle:widget-init) |
/messages, /messages/stream, /events, /conversation/*, DELETE /me | 30 rpm | per JWT (throttle:widget-session) |
/leads | 5 rpm | per JWT (throttle:widget-leads) |
All return 429 with a Retry-After header on limit.