ZChat REST API
Developers · API reference
Connect your own code or CRM to ZChat, or use the HTTP and webhook steps in Zapier or Make (ZChat has no app in their directories): read, search and answer conversations, chat with your AI agent (with streaming), manage AI instructions, tickets and knowledge sources, sync contacts and subscribe to events. The same keys connect Claude, ChatGPT or Cursor through the ZChat MCP server. Authenticate with a workspace API key; every plan includes the API.
Before you start
- Difficulty: Developer
- Time: 10 minutes to a first call
- You need: A ZChat workspace (the free plan includes the API)
- You need: An admin or owner account to create a key
- You need: A server to call from: keys must never be put in a browser, an app or a URL
Steps
Create an API key
In ZChat, open Settings → API keys and choose + Create key. Name it after the integration, pick its permissions (Read only, Read & write, or individual scopes), tick Can send messages to customers only if the integration replies to customers, and optionally an expiry. The full key is shown once: copy it into your integration's secret settings. ZChat stores only a hash of it, so a lost key cannot be recovered - revoke it and create another.
Make your first call
Send the key as a Bearer token. GET /api/v1/me returns the workspace, the key's scopes and its rate limit. Keys in the query string are refused.
curl https://api.zchat.com/api/v1/me \ -H "Authorization: Bearer zck_live_YOUR_KEY"
Read conversations
Lists are newest first. Pass limit (1-100) and the nextCursor of the previous page as cursor. Filter by status (open, pending_human, resolved), channel, contactId, assignedAgentId, tagId or updatedSince. GET /api/v1/conversations/{id} adds the messages.
curl "https://api.zchat.com/api/v1/conversations?status=pending_human&limit=50" \ -H "Authorization: Bearer zck_live_YOUR_KEY"
Reply as your integration
Needs the messages:send permission ("Can send messages to customers"). The reply reaches the visitor on the conversation's channel and is shown as written by the API key. Like any agent reply it hands the conversation to your team, so the AI stops answering it. Send an Idempotency-Key header to make retries safe.
curl -X POST https://api.zchat.com/api/v1/conversations/1234/messages \ -H "Authorization: Bearer zck_live_YOUR_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 6f1c2b4e-reply-1" \ -d '{"body": "Your order shipped today."}'Chat with your AI agent
POST /api/v1/chat sends a message as an API visitor and returns the AI's answer: the same agent, knowledge, procedures, guardrails and AI allowance as the widget. The conversation shows up in the inbox with channel api; send conversationId to continue it. If the AI hands over to a person, message is null and handedOff is true. Needs chat:write.
curl -X POST https://api.zchat.com/api/v1/chat \ -H "Authorization: Bearer zck_live_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{"message": "Do you ship to Canada?", "contact": {"email": "[email protected]"}}'Stream the answer
Add ?stream=true to get Server-Sent Events: event conversation (the ids), event delta with {"delta": "..."} as the model writes, event message with the same object as the JSON response, then event done. Deltas arrive when the AI provider streams and the agent has no guardrail rules; otherwise you get the final message only.
curl -N -X POST "https://api.zchat.com/api/v1/chat?stream=true" \ -H "Authorization: Bearer zck_live_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{"message": "Do you ship to Canada?"}'Stream from JavaScript (server side)
EventSource cannot send an Authorization header, so read the stream with fetch on your server and forward the text to your page.
const res = await fetch("https://api.zchat.com/api/v1/chat?stream=true", { method: "POST", headers: { Authorization: `Bearer ${process.env.ZCHAT_KEY}`, "Content-Type": "application/json" }, body: JSON.stringify({ message: "Do you ship to Canada?", conversationId }) }); const reader = res.body.pipeThrough(new TextDecoderStream()).getReader(); let buffer = ""; for (;;) { const { value, done } = await reader.read(); if (done) break; buffer += value; let end; while ((end = buffer.indexOf("\n\n")) >= 0) { const frame = buffer.slice(0, end); buffer = buffer.slice(end + 2); const event = frame.match(/^event: (.*)$/m)?.[1]; const data = frame.match(/^data: (.*)$/m)?.[1]; if (event === "delta") process.stdout.write(JSON.parse(data).delta); if (event === "message") conversationId = JSON.parse(data).conversationId; } }Update the AI's instructions
PATCH /api/v1/ai-settings changes the instructions, model or on/off. When Settings → AI → Require publish is on (or a draft is pending), the change goes to the draft and customers keep the live version until POST /api/v1/ai-settings/publish. Per-site agents (GET /api/v1/ai-agents) have their own extra instructions.
curl -X PATCH https://api.zchat.com/api/v1/ai-settings \ -H "Authorization: Bearer zck_live_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{"instructions": "You are Acme support. Be brief."}' curl -X POST https://api.zchat.com/api/v1/ai-settings/publish \ -H "Authorization: Bearer zck_live_YOUR_KEY" \ -H "Content-Type: application/json" -d '{"note": "Shorter answers"}'Keep the knowledge base in sync
PUT an article under your own id: the first call creates it, later calls update it in place, and the AI's index is rebuilt automatically. Curation done in the dashboard (enabled, category, status) survives updates.
curl -X PUT https://api.zchat.com/api/v1/knowledge/articles/external/shipping-times \ -H "Authorization: Bearer zck_live_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{"title": "Shipping times", "body": "Orders ship within 2 business days.", "tags": "shipping"}'Subscribe to events (REST hooks)
POST /api/v1/webhooks with a public https URL and the events you want; DELETE it to unsubscribe. This is the subscribe/unsubscribe pattern Zapier and Make use. ZChat generates a signing secret if you send none and returns it once.
curl -X POST https://api.zchat.com/api/v1/webhooks \ -H "Authorization: Bearer zck_live_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{"url": "https://example.com/zchat-events", "events": ["conversation.created", "conversation.resolved"]}'Verify each delivery
Every delivery carries X-ZChat-Event (the event name), X-ZChat-Timestamp and X-ZChat-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>" keyed with the secret>. It also carries X-ZChat-Secret, the signing secret itself, for receivers built before signatures existed. Each of these four headers is sent a second time under an X-Dchat- name (X-Dchat-Event, X-Dchat-Timestamp, X-Dchat-Signature, X-Dchat-Secret) with the same value, for receivers built before the ZChat rename. Verify the signature rather than comparing X-ZChat-Secret, keep request headers out of your logs, reject old timestamps and compare in constant time.
import crypto from "node:crypto"; function verify(rawBody, header, secret) { const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("="))); if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false; const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex"); return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1)); }
Every endpoint
GET /api/v1/me: Any key. The workspace, the key's scopes and its rate limit.GET /api/v1/openapi.json: No key needed. The OpenAPI 3.1 description of this API, for code generators and API clients.GET /api/v1/conversations: conversations:read. List and search: status, channel, contact, agent, tag, text (q), start date, cursor paging.GET /api/v1/conversations/{id}: conversations:read. One conversation with its latest 500 messages.POST /api/v1/conversations/{id}/messages: conversations:write and messages:send. Reply to the customer, attributed to the API key.POST /api/v1/conversations/{id}/assign: conversations:write. Assign to an agent ({"agentId": "..."}).POST /api/v1/conversations/{id}/close: conversations:write. Resolve, with an optional outcome and note.POST /api/v1/conversations/{id}/reopen: conversations:write. Set back to open.POST /api/v1/conversations/{id}/tags: conversations:write. Apply an existing tag by tagId or name.DELETE /api/v1/conversations/{id}/tags/{tagId}: conversations:write. Remove a tag.GET /api/v1/tags: conversations:read. The workspace's tags.PATCH /api/v1/conversations/{id}: conversations:write. Set priority, a custom status or custom field values. Send only what you change.GET /api/v1/custom-statuses: conversations:read. Your custom statuses and the built-in state each one refines.GET /api/v1/custom-fields: conversations:read. Your conversation and ticket fields: key, type, options and whether they are required to resolve.GET /api/v1/agents: conversations:read. Agents you can assign to.GET /api/v1/conversations/{id}/transcript: conversations:read. The transcript as json (default), text or markdown, without internal notes.POST /api/v1/conversations/{id}/status: conversations:write. Set open, pending_human (hand to a person) or resolved.POST /api/v1/conversations/{id}/feedback: conversations:write. Record the customer's rating: positive, neutral or negative, with an optional comment.POST /api/v1/chat: chat:write. Talk to the AI agent as an API visitor; ?stream=true for Server-Sent Events. Uses the AI allowance like the widget.GET /api/v1/ai-settings: agents:read. Live AI settings (instructions, model, enabled) and any unpublished draft.PATCH /api/v1/ai-settings: agents:write. Change instructions, model or enabled; saved to the draft while Require publish is on.POST /api/v1/ai-settings/publish: agents:write. Make the draft live as a new version.DELETE /api/v1/ai-settings/draft: agents:write. Throw the draft away.GET /api/v1/ai-agents: agents:read. The AI agents, one per site; the id is the agentId for chat and tickets.GET /api/v1/ai-agents/{id}: agents:read. One agent.PATCH /api/v1/ai-agents/{id}: agents:write. Change an agent's name, extra instructions or knowledge scope (live at once).GET /api/v1/ai-tests: agents:read. Simulated customer tests and this month's run allowance.POST /api/v1/ai-tests/{id}/run: agents:write. Run a simulated test and get the graded result.GET /api/v1/knowledge/sources: knowledge:read. Websites, pages, Q&A, API articles and connected apps the AI learns from.POST /api/v1/knowledge/sources: knowledge:write. Add a url (one page, or crawl: true for the site), a text document or a Q&A pair.POST /api/v1/knowledge/sources/{key}/sync: knowledge:write. Re-sync a website or connected source.GET /api/v1/knowledge/search: knowledge:read. Keyword search over enabled articles (no AI allowance used).GET /api/v1/tickets: tickets:read. Helpdesk tickets, newest first, optionally by status.GET /api/v1/tickets/{id}: tickets:read. One ticket with its replies.POST /api/v1/tickets: tickets:write. Open a ticket for a customer. Accepts Idempotency-Key.POST /api/v1/tickets/{id}/replies: tickets:write and messages:send. Reply; the customer gets it by email.POST /api/v1/tickets/{id}/status: tickets:write. Close or reopen.GET /api/v1/reports/summary: reports:read. Headline numbers: conversations, AI deflection, CSAT, first response time.GET /api/v1/contacts: contacts:read. List or search (q, email, externalId).GET /api/v1/contacts/{id}: contacts:read. One contact with its tags.POST /api/v1/contacts: contacts:write. Create (name, email, phone, externalId). Accepts Idempotency-Key.PATCH /api/v1/contacts/{id}: contacts:write. Update only the fields you send.DELETE /api/v1/contacts/{id}: contacts:write and contacts:erase. GDPR erasure: the person and every conversation, message and file about them. Permanent.GET /api/v1/contacts/{id}/events: contacts:read. The contact's event timeline.POST /api/v1/contacts/{id}/events: contacts:write. Record an event or note (name plus a JSON payload) agents see on the timeline.GET /api/v1/contact-attributes: contacts:read. The custom contact attributes (key, label, type, AI may see, sensitive) and your plan's limit.POST /api/v1/contact-attributes: contacts:write. Define an attribute: text, number, boolean, date or select. The AI sees it only if you turn on aiVisible, and never if it is sensitive.PATCH /api/v1/contact-attributes/{key}: contacts:write. Change an attribute's label, options or flags. Key and type stay fixed.DELETE /api/v1/contact-attributes/{key}: contacts:write. Delete an attribute and every contact's value for it.GET /api/v1/knowledge/articles: knowledge:read. List articles (without bodies).GET /api/v1/knowledge/articles/{id}: knowledge:read. One article with its body.PUT /api/v1/knowledge/articles/external/{externalId}: knowledge:write. Create or update by your own id.DELETE /api/v1/knowledge/articles/{id}: knowledge:write. Delete an article.POST /api/v1/knowledge/reindex: knowledge:write. Rebuild the AI's search index.GET /api/v1/webhooks: webhooks:read. Every webhook of the workspace (secrets never shown).POST /api/v1/webhooks: webhooks:write. Subscribe a URL to events. Accepts Idempotency-Key.DELETE /api/v1/webhooks/{id}: webhooks:write. Unsubscribe.GET /api/v1/webhooks/events: Any key. The event types: conversation.created, .visitor_replied, .agent_replied, .handoff_requested, .resolved, .sla_breached.
Good to know
- Base URL: https://api.zchat.com. HTTPS only. JSON request and response bodies, camelCase names, ISO-8601 UTC timestamps.
- Scopes: conversations, contacts, knowledge, webhooks, agents and tickets each have :read and :write (:write includes :read); chat:write talks to the AI; reports:read reads reports. Two scopes are never in a preset and must be ticked: messages:send (anything that reaches a customer: conversation and ticket replies) and contacts:erase (needs contacts:write too). Keys created before 2 November 2026 keep everything they could do: one with conversations:write can still reply.
- POST /api/v1/chat: 60 calls a minute per key on paid plans, 20 on the free plan (within the overall limit). Each answer counts against the AI allowance like a widget message; when it is used up the conversation is handed to your team.
- Errors are RFC 7807 problem details (application/problem+json) with a stable code: missing_api_key, invalid_api_key, api_key_revoked, api_key_expired, insufficient_scope, not_found, validation_failed, rate_limited and others.
- Rate limit per key: 600 requests a minute on paid plans, 60 on the free plan. A 429 says how long to wait in Retry-After. Repeated invalid keys from one address are blocked for a few minutes.
- Each workspace's keys only ever see that workspace. Revoking a key in the dashboard stops it immediately; creating and revoking are recorded in the audit log.
- The API is for servers: it sends no CORS headers, so browsers cannot call it from another site.
Machine-readable spec: https://api.zchat.com/api/v1/openapi.json - import it into Postman, Insomnia or a code generator. AI assistants: see /docs/mcp.