Connect
API & webhooks
Quincer AI is API-first. Everything you can do in the dashboard — take over a live chat, update a lead, read the activity log — you can do via REST. This page is the map. For request/response previews against your real workspace, open Developer APIs inside the dashboard.
Authentication
All API requests use a workspace developer key. Generate one under Dashboard → Integrations → API keys, choose scopes, and pass it as a Bearer token:
Authorization: Bearer cw_dev_<your-key>
Two kinds of credentials exist: widget keys (cw_live_) are
embedded in the page script and only identify a widget to the public chat endpoint.
Developer keys (cw_dev_) are scoped to an organization, carry
explicit scopes like leads:read or conversations:takeover, and
must stay server-side.
Base URL
https://chat.quincer.com
Embedding Quincer in a mobile app? Use
@quincer/react-native instead of calling these REST endpoints
directly. The SDK wraps /api/chat, the voice-relay, the
widget-key conversation endpoints, and the live agent-takeover SSE stream
— with native UI, native voice, and AsyncStorage persistence. See the
React Native SDK guide.
Conversations
Use these to let an outside AI (OpenClaw, a custom agent, or your own backend) discover, take over, reply on, and hand back live conversations. The takeover/reply/handback trio is how the OpenClaw integration works under the hood.
| Method | Path | Scope | Purpose |
|---|---|---|---|
GET | /api/v1/conversations | conversations:read | List conversations. Filters: widget_id, status (single or comma-separated), channel, engaged (true|all), updated_since. Cursor-paginated. |
GET | /api/v1/conversations/:id | conversations:read | Fetch full transcript + metadata. |
POST | /api/v1/conversations/:id/takeover | conversations:takeover | Claim a conversation — AI stops responding. |
POST | /api/v1/conversations/:id/reply | conversations:reply | Post a human-agent message (auto-translated to visitor's language). |
POST | /api/v1/conversations/:id/handback | conversations:handback | Release the takeover — AI resumes. |
POST | /api/v1/conversations/:id/handoff | conversations:write | Switch the active persona for this conversation. Body: { "persona_id": "..." | null } (null clears the override). |
POST | /api/v1/conversations/:id/reply-template | conversations:write | Send a pre-approved WhatsApp template outside the 24-h window. Requires the conversation to be taken over first (same precondition as /reply). Body: { "template_id", "variables": [], "speaker_name"? }. |
GET | /api/v1/conversations/:id/whatsapp-window | conversations:read | WhatsApp 24-h customer-service window state. WhatsApp channel only. |
Leads
| Method | Path | Scope | Purpose |
|---|---|---|---|
GET | /api/v1/leads | leads:read | List leads. Filters: widget_id, stage, temperature, assigned_to, updated_since. Cursor-paginated — see Pagination below. |
POST | /api/v1/leads | leads:write | Create a lead outside the chat flow (e.g. CRM ingestion, marketing-form capture). Requires widget_id and at least one of email, name, phone. |
GET | /api/v1/leads/:id | leads:read | Full lead record (BANT, tags, assigned task history). |
POST | /api/v1/leads/:id | leads:write | Update fields: stage, temperature, tags, score, assignment, contact info. PATCH on the same path is also accepted (legacy alias). |
DELETE | /api/v1/leads/:id | leads:delete | Permanently delete the lead and its associated rows. |
GET | /api/v1/leads/:id/emails | leads:read | List drafted & sent follow-up emails for this lead. |
POST | /api/v1/leads/:id/emails/generate | leads:write | Generate an AI follow-up draft on demand. Reuses the latest draft if one already exists. |
PATCH | /api/v1/leads/:id/emails/:email_id | leads:write | Inline-edit a draft's subject/body. Draft status only. |
POST | /api/v1/leads/:id/emails/:email_id | leads:write | Send a draft. Body: { "action": "approve" | "send" }. |
POST | /api/v1/leads/:id/emails/:email_id/redraft | leads:write | AI rewrites the existing draft per natural-language instructions. Body: { "instructions": "..." }. |
Tasks
| Method | Path | Scope | Purpose |
|---|---|---|---|
GET | /api/v1/tasks | tasks:read | Agent task log. Filters: widget_id, lead_id, status, type, updated_since. Mounted at /api/v1/activities too as a legacy alias. |
POST | /api/v1/tasks | tasks:write | Schedule a new task. Requires widget_id, type, title. |
GET | /api/v1/tasks/:id | tasks:read | Full task record with execution + result data. Mounted at /api/v1/activities/:id too as a legacy alias. |
POST | /api/v1/tasks/:id | tasks:write | Update status, priority, assignment, or schedule. PATCH also accepted. |
DELETE | /api/v1/tasks/:id | tasks:delete | Cancel/delete the task. Useful for clearing stuck tasks that no longer have a runner. |
Knowledge base
| Method | Path | Scope | Purpose |
|---|---|---|---|
GET | /api/v1/knowledge | knowledge:read | List knowledge items. Filters: widget_id, persona_id, source_type, query (or q) to search title/content/URL. Cursor-paginated. |
POST | /api/v1/knowledge | knowledge:write | Create a knowledge item. Body: { "title", "content", "widget_id"?, "persona_id"?, "personas"?: [] }. Embedding is regenerated automatically. |
GET | /api/v1/knowledge/:id | knowledge:read | Single item. |
POST | /api/v1/knowledge/:id | knowledge:write | Update title/content/persona binding. PATCH also accepted. |
DELETE | /api/v1/knowledge/:id | knowledge:delete | Cascade-deletes chunks and embeddings. |
Personas
| Method | Path | Scope | Purpose |
|---|---|---|---|
GET | /api/v1/personas | personas:read | List personas. Filter: widget_id. |
POST | /api/v1/personas | personas:write | Create a persona. Body: { "name", "systemPrompt", "widget_id"?, "bubble_color"?, "url_patterns"?, "is_default"? }. |
GET | /api/v1/personas/:id | personas:read | Single persona. |
POST | /api/v1/personas/:id | personas:write | Update fields: name, prompt, urlPatterns, isDefault, voice, agentMode, Slack/Telegram routing, Meta asset bindings, enabledToolIds, email-inbox settings. |
DELETE | /api/v1/personas/:id | personas:delete | Delete. Refuses if this is the widget's last persona. |
Sales playbook
| Method | Path | Scope | Purpose |
|---|---|---|---|
GET | /api/v1/playbook | playbook:read | List entries. Filters: widget_id, category. |
POST | /api/v1/playbook | playbook:write | Create. category must be one of value_prop, roi_data, objection_handler, competitor_comparison, pricing, case_study. |
GET | /api/v1/playbook/:id | playbook:read | Single entry. |
POST | /api/v1/playbook/:id | playbook:write | Update. PATCH also accepted. |
DELETE | /api/v1/playbook/:id | playbook:delete | Delete. |
POST | /api/v1/playbook/generate | playbook:write | Upload a document (PDF / Markdown / text, max 10MB) via multipart/form-data to auto-generate entries (mode=generate) or import as-is (mode=import). Requires file, widgetId. |
Models
| Method | Path | Scope | Purpose |
|---|---|---|---|
GET | /api/v1/models | widgets:read | List the AI models available to the workspace (from its configured provider keys). Useful for picking a widget's aiModel. Cached 10 minutes. |
Widgets
| Method | Path | Scope | Purpose |
|---|---|---|---|
GET | /api/v1/widgets | widgets:read | List widgets in the workspace. |
POST | /api/v1/widgets | widgets:write | Create a new widget (one brand / deploy target). Requires brand; websiteUrl records the site it's deployed on. Seeds default personas. By default also mints a public embed key (cw_live_*) and returns apiKey + embed (scriptUrl + ready-to-paste snippet), so one call yields a deployable brand. Pass withApiKey: false to skip. |
GET | /api/v1/widgets/:id | widgets:read | Full widget config: branding, AI model, agent mode, voice, languages, attendees. |
POST | /api/v1/widgets/:id | widgets:write | Update any configurable field. Plan-gated knobs (proactive mode, multi-domain, voice, attachments) are enforced. PATCH also accepted. |
DELETE | /api/v1/widgets/:id | widgets:write | Delete. Refuses if this is the workspace's only widget. |
GET | /api/v1/widgets/:id/keys | widgets:read | List the widget's active public embed keys, each with an embed snippet. Re-fetch a key returned at create time. |
POST | /api/v1/widgets/:id/keys | widgets:write | Mint a new public embed key for the widget (rotation). Returns the key + embed snippet. |
POST | /api/v1/widgets/:id/import-storefront | knowledge:write | Import a public Shopify storefront's catalog (its /products.json) into this widget's knowledge base — no app install needed. Body: { url? } (defaults to the widget's websiteUrl). Products are also available live to the generative-UI product panel. |
Integrations
| Method | Path | Scope | Purpose |
|---|---|---|---|
GET | /api/v1/integrations | integrations:read | List supported integrations and their connection state for this workspace (optionally a specific widget_id). |
GET | /api/v1/integrations/:provider | integrations:read | Connection details for one provider. Returns { "connected": false } if not connected. |
Connect/disconnect flows still require an interactive OAuth roundtrip and live under the dashboard at Dashboard → Integrations. This endpoint is read-only by design — programmatically minting OAuth grants from a dev key is a footgun.
Webhooks
Programmatic webhook subscription management. Previously dashboard-only; now a developer key
with webhooks:manage can provision a subscription end-to-end.
| Method | Path | Scope | Purpose |
|---|---|---|---|
GET | /api/v1/webhooks | webhooks:manage | List subscriptions in this workspace. |
POST | /api/v1/webhooks | webhooks:manage | Create. Body: { "url", "events": [] }. The signing secret is returned once on creation — store it; subsequent GETs do not include it. |
GET | /api/v1/webhooks/:id | webhooks:manage | Single subscription. |
POST | /api/v1/webhooks/:id | webhooks:manage | Update url, events, active state, or reset_fail_count. PATCH also accepted. |
DELETE | /api/v1/webhooks/:id | webhooks:manage | Delete the subscription. |
Voice
| Method | Path | Scope | Purpose |
|---|---|---|---|
GET | /api/v1/voice/sessions | voice:read | List voice sessions. Filters: widget_id, status. Cursor-paginated by started_at. |
GET | /api/v1/voice/sessions/:id | voice:read | Single voice session metadata. |
GET | /api/v1/voice/sessions/:id/transcript | voice:read | Ordered transcript lines (speaker: visitor | ai | agent). |
POST | /api/v1/voice/sessions/:id/end | voice:write | Terminate a live voice session and tear down the SFU room. |
GET | /api/v1/voice/settings | voice:read | Read org-level voice defaults (silence timeout, max call duration, provider, xAI model). |
PUT | /api/v1/voice/settings | voice:write | Update org-level voice defaults. Takes effect on the next session. |
POST | /api/v1/voice/callback/request | voice:write | File a callback request against the voice queue. Body: { "session_id"?, "name"?, "phone"?, "email"?, "note"? } — at least one of phone or email is required. |
Voice handoff (agent claim) remains seat-licensed and is initiated from the dashboard
Live Conversations UI — it requires a User row with liveHandoffLicensed,
not a service key, so it has no /api/v1 equivalent.
All update operations use POST on the resource path
(POST /api/v1/leads/:id). PATCH on the same path stays
accepted for one major version so existing integrations don’t break, but
POST is the canonical method going forward.
Resource type discriminator
Every resource response carries a read-only object field naming the
resource type ("lead", "conversation", "task",
"message"). List responses carry object: "list" at the
envelope and per-item object fields inside data[]. Use it to
deserialize polymorphic payloads (e.g., webhooks, mixed-resource responses).
{
"object": "lead",
"id": "clx...",
"stage": "qualified",
"temperature": "warm"
}
Pagination
List endpoints return a data array plus a has_more boolean and
a url field naming the endpoint. Use cursor pagination via
starting_after=<resource_id> — the API returns the next page of
items created before that cursor. limit is bounded at 200 (default 50).
GET /api/v1/leads?limit=20&starting_after=clx_abc123
{
"object": "list",
"data": [ { "object": "lead", "id": "clx_xyz789", ... }, ... ],
"has_more": true,
"url": "/api/v1/leads",
// Legacy fields surfaced alongside the new envelope for one major
// version. New consumers should read `data` + `has_more`.
"leads": [ /* same items as data[] */ ],
"total": 142,
"limit": 20,
"offset": 0
}
Naming conventions
Query parameters, request body fields, and response field names use
snake_case. Existing camelCase aliases on list filters
(widgetId, assignedTo, leadId, updatedSince) and on
PATCH bodies (jobTitle, assignedTo) continue to work for one
major version — new code should use snake_case.
Rate limits
Developer keys are rate-limited per key: 100 reads/min and 30 writes/min. Every authenticated response carries the current state in headers so well-behaved clients can self-throttle:
Quincer-RateLimit-Limit: 100
Quincer-RateLimit-Remaining: 87
Quincer-RateLimit-Reset: 1714435260
Over the limit returns 429 Too Many Requests with both the standard
Retry-After header (seconds until reset) and the
Quincer-RateLimit-* headers. The error body indicates the remaining count.
HTTP/1.1 429 Too Many Requests
Retry-After: 42
Quincer-RateLimit-Limit: 100
Quincer-RateLimit-Remaining: 0
Quincer-RateLimit-Reset: 1714435260
{
"error": "Rate limit exceeded. 0 read requests remaining. Retry after 42 seconds."
}
Webhooks
Subscribe to events under Dashboard → Settings → Webhooks. Quincer AI signs every
payload with HMAC-SHA256 using the secret you set. Requests include
Quincer-Signature (sha256=...), Quincer-Event, and
Quincer-Timestamp. The legacy X-Quincer-* variants are also sent
for one major version (RFC 6648 deprecated the X- prefix — existing
subscribers reading X-Quincer-* keep working unchanged).
Events
| Event | Fires when |
|---|---|
conversation.message_received | Visitor sends any message. Payload includes status (active or human_active) so subscribers know whether an external agent is driving. |
conversation.takeover | A human or external agent claimed a conversation (dashboard takeover, Slack/Telegram approve, or POST /api/v1/conversations/:id/takeover). AI stops responding until handback fires. Payload: conversationId, widgetId, actorKind (internal_user or external_agent), actorUserId, actorName, optional externalLabel, optional surface (dashboard | slack | telegram | api). |
conversation.handback | Takeover released — AI resumes. Same payload shape as conversation.takeover. |
lead.created | A new lead record is captured. |
lead.updated | Any lead field changes. |
lead.stage_changed | Lead moves between pipeline stages. Payload includes both previous_stage (canonical) and previousStage (legacy alias). |
task.created | An agent task (follow-up, qualification, CRM sync, etc.) is scheduled. |
task.completed | A task finishes successfully. |
email.sent | A lead email goes out. |
email.draft_created | An AI-drafted email is ready for review. Email resources are managed via the dashboard — the payload is fully self-contained for downstream notification use. |
Verifying signatures
const signature = req.headers["quincer-signature"];
// (or "x-quincer-signature" if you set up your webhook before April 2026)
const expected = "sha256=" + crypto
.createHmac("sha256", process.env.QUINCER_WEBHOOK_SECRET)
.update(req.rawBody)
.digest("hex");
if (signature !== expected) {
return res.status(401).send("invalid signature");
}
OpenClaw integration
Quincer AI ships a first-class OpenClaw skill so any OpenClaw agent can take over Quincer AI conversations, query leads, and log activity. Setup takes about a minute:
- In Quincer AI, open Dashboard → Integrations → OpenClaw and click Connect OpenClaw. This creates a dedicated "OpenClaw" team member in your workspace and issues an API token bound to it.
- Download SKILL.md and drop it in
~/.openclaw/workspace/skills/quincer/. - Add the token to
~/.openclaw/openclaw.jsonunderskills.entries.quincer.env.QUINCER_API_KEY. - Restart the OpenClaw gateway. The
quincertool is now available.
OpenClaw actions (takeovers, replies) appear in your Quincer AI inbox under the OpenClaw team member, with their own avatar and audit trail — not as a generic "external agent."