REST API
Chat with your intoCHAT agents, read their conversations, leads and knowledge sources, and subscribe webhooks from your own server or an automation tool: API keys, scopes, rate limits, errors, pagination and every endpoint with examples.
The REST API lets your own server talk to your agents. Send a message and get the agent's reply, read conversations and leads, add or remove knowledge sources, and subscribe webhooks to an agent's events, with JSON over HTTPS. The Zapier and Make apps are built on it. A reply through the API is written by the same code as a reply in the chat widget, from the same knowledge and settings.
The API is for servers. It doesn't send CORS headers, so a web page on another site can't call it, and your API key must never appear in a browser or in your embed code. To show a chat on your site, use the script tag. To get events pushed to you as they happen, use webhooks.
Which plans include the API
The API is included on Starter, Pro and Enterprise. On Free you can't create keys.
If an account moves back to Free, its keys stay in the list but every request gets a 403 error with the code plan_required. They work again as soon as the account is on Starter or higher.
Each reply an agent writes through the API counts as one message on your plan, exactly like a reply in the widget, and counts toward the agent's monthly message cap if you set one. Reading data, and adding or deleting sources, uses no messages. See What counts as a message.
Create an API key
- In the sidebar, under Settings, click API keys.
- Under Create a key, give the key a Name you will recognize later, for example
CRM sync. - Under Access, keep Every scope, including ones added later, or pick Only the scopes I choose and tick the ones this key needs. See Scopes.
- Click Create key.
- Click Copy, store the key where your server can read it, for example in an environment variable, and click I've saved it.
A key looks like ic_live_ followed by 32 letters and digits. It is shown only once: intoCHAT keeps only a fingerprint of it, so nobody can show it to you again. If you lose it, revoke it and create a new one.
A key belongs to the account, not to the person who created it. It works for every agent in the account, within its scopes, and keeps working if that person leaves the team. An account can have up to 20 active keys.
Only the account owner and admins can create and revoke keys. Editors and viewers see the list of keys, with their first characters only. See Team members and roles.
The key list
Each key shows its name, its first characters (for example ic_live_3f9Aโฆ), its scopes, when it was created and when it was last used. Last used is updated at most once a minute.
Revoke a key
Click Revoke next to the key and confirm. The key stops working from its next request, and this can't be undone. Revoke a key as soon as you think someone else may have it.
Revoking a key also deletes the webhooks it subscribed, for example for Zapier or Make, with their delivery history: nothing could remove them otherwise. Webhooks you added on the Leads tab are not affected.
Authentication
Send the key in the Authorization header of every request:
curl https://www.intochat.ai/api/v1/agents \
-H "Authorization: Bearer ic_live_YOUR_KEY"
All endpoints are under https://www.intochat.ai/api/v1. Requests with a body send JSON with Content-Type: application/json, and every response is JSON, except a streamed chat reply. The API uses no cookies and no sign-in session: the key alone decides what a request may do.
A missing, malformed, unknown or revoked key gets a 401 error with the code unauthorized.
Scopes
A scope lets a key use a group of endpoints. A key created with Every scope, including ones added later can use every endpoint, also ones added in the future.
| Scope | Allows |
|---|---|
agents:read | List agents and get an agent |
chat | Send a message to an agent. Each reply counts as a message on your plan. |
conversations:read | List conversations and get a conversation with its transcript |
leads:read | List leads, and the lead, form, booking and return events of List events and Get sample events |
sources:read | List sources |
sources:write | Add a source and delete a source, which also starts training |
webhooks:write | Subscribe a webhook and unsubscribe a webhook |
conversations:read also covers the hand-off, live chat and new conversation events of List events.
A request to an endpoint the key has no scope for gets a 403 error with the code insufficient_scope. To change a key's scopes, create a new key and revoke the old one.
Rate limits
Each key can make 60 requests a minute, across all endpoints. Every response that passed the key check carries these headers:
| Header | Value |
|---|---|
X-RateLimit-Limit | 60 |
X-RateLimit-Remaining | Requests left in the current minute |
X-RateLimit-Reset | When the minute ends, in Unix seconds |
Over the limit, the request gets a 429 error with the code rate_limited and a Retry-After header with the seconds to wait.
Chat has two more limits that don't depend on the key: the plan's messages, and an agent answers at most 1,000 messages an hour, widget and API together. Over that, a chat request gets a 429 error with the code agent_busy.
Errors
Every error has the same shape:
{
"error": {
"code": "insufficient_scope",
"message": "This API key doesn't have the \"chat\" scope."
}
}
Use code in your code; message explains it for a person and may change.
| Status | Code | Meaning |
|---|---|---|
400 | invalid_request | The request is missing something or has a wrong value. The message says which. |
401 | unauthorized | No key, or the key is malformed, unknown or revoked. |
403 | insufficient_scope | The key doesn't have the scope this endpoint needs. |
403 | plan_required | The account is on Free. The API is included from Starter. |
403 | message_limit_reached | The account has used all the messages its plan includes this period. |
403 | agent_cap_reached | The agent reached the monthly message cap set under Share, Limits and access. |
403 | agent_private | The agent is private, so it answers only in the dashboard. See Private agents. |
403 | character_limit_exceeded | A new source would take the agent past its plan's training characters. |
404 | not_found | No such agent, conversation, source or webhook in this account. |
429 | rate_limited | The key made more than 60 requests this minute. |
429 | agent_busy | The agent is answering too many messages right now. |
500 | internal_error | Something went wrong on our side. Try again. |
504 | timeout | The agent took too long to answer. |
An agent, conversation or source that belongs to another account gets 404, the same as one that doesn't exist.
Pagination
List conversations and list leads return one page at a time, newest first. They take these query parameters:
| Parameter | Content |
|---|---|
limit | Items per page, from 1 to 100. The default is 20. |
cursor | The nextCursor of the previous page, to get the next one. |
since | An ISO 8601 time, for example 2026-10-01T00:00:00Z. Only items with activity at or after it are returned: a conversation with a message since then, or a lead that was saved or submitted again since then. |
A page looks like this:
{
"data": [ ],
"hasMore": true,
"nextCursor": "MjAyNi0xMC0wNFQwOTowMDowMC4wMDBafGNsdjEyMw"
}
Request the next page with ?cursor= and that value, keeping the same limit and since, until hasMore is false and nextCursor is null. The order is fixed by creation time, so new activity while you page through doesn't make an item appear twice or go missing. To sync regularly, store the time you started the last sync and pass it as since next time.
List agents
GET /api/v1/agents ยท scope agents:read
Every agent in the account, newest first.
curl https://www.intochat.ai/api/v1/agents \
-H "Authorization: Bearer ic_live_YOUR_KEY"
{
"data": [
{
"id": "AGENT_ID",
"name": "Acme Assistant",
"private": false,
"status": "trained",
"trainedAt": "2026-10-02T09:00:00.000Z",
"characters": 48210,
"counts": { "conversations": 312, "messages": 2240, "leads": 41, "sources": 18 },
"createdAt": "2026-09-01T09:00:00.000Z",
"updatedAt": "2026-10-02T09:00:00.000Z"
}
]
}
status is untrained, training, trained or error (the last training failed). characters is how many training characters the agent's trained sources use.
Get an agent
GET /api/v1/agents/{agentId} ยท scope agents:read
One agent, in data, with the same fields as in List agents.
Send a message
POST /api/v1/agents/{agentId}/chat ยท scope chat
Sends one message to the agent and returns its reply. The agent answers as it does in the widget: from its knowledge and instructions, with your API actions, custom buttons, chat forms, booking, hand-off by email and live chat where you set them up.
| Field | Content |
|---|---|
message | Required. The text, up to 10,000 characters. |
conversationId | Optional. Continue a conversation you started through the API, with the conversationId of an earlier reply. |
sessionId | Optional. Your own ID for the person you chat for, for example your user ID, up to 100 characters. The same value always continues the same conversation. You can also send the sessionId of an earlier reply. |
stream | Optional. true streams the reply's text; see Streaming. The default is false. |
timeZone | Optional. The person's IANA time zone, for example Europe/Berlin, for booking times. Without it, times are in UTC. |
Without conversationId and sessionId, every message starts a new conversation.
curl https://www.intochat.ai/api/v1/agents/AGENT_ID/chat \
-H "Authorization: Bearer ic_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "message": "Do you ship to Switzerland?", "sessionId": "customer-4821" }'
{
"reply": "Yes, we ship to Switzerland. Delivery takes 3 to 5 working days.",
"conversationId": "CONVERSATION_ID",
"sessionId": "icapi5f0c2b9e4d7a41f8a3c6e1b2d9f07a64",
"sources": [
{ "title": "Shipping", "url": "https://www.example.com/shipping" }
],
"followUps": ["How much does shipping cost?", "Can I track my order?"],
"buttons": [],
"products": [],
"form": null,
"leadForm": null,
"booking": null,
"liveChat": null
}
| Field | Content |
|---|---|
reply | The agent's reply as plain text with Markdown, without the widget's control blocks. Empty while someone from your team has the chat (see liveChat). |
conversationId | The conversation, as in Get a conversation. |
sessionId | The conversation's session in intoCHAT. It always starts with icapi. |
sources | The pages the reply drew on, as title and url, when Show sources under replies is on. Pages from web search are always listed, with "web": true. |
followUps | Suggested next questions, when Suggest follow-up questions is on. |
buttons | Custom buttons the agent chose, as label and url. |
products | Product cards from your store or API actions, with name, url and, when known, image, price, compareAtPrice, currency and available. |
form | A chat form the agent offered, with its formId, name, fields and submitLabel, or null. The API can't submit it; show it in your own interface or ignore it. |
leadForm | The lead form, with its message, contact fields and form, when the agent asked for contact details, or null. Contact details written in a message are saved as a lead when Save contact details written in the chat is on, as in the widget. |
booking | Open meeting times, with provider, eventTitle, eventLength in minutes, timeZone and days, each with its date and slots as UTC times, or null. The API can't book a time. |
liveChat | null, or the state of live chat: offered (the agent may ask your team to join), requested (it asked them) or paused (a team member has the chat, so the agent didn't reply). |
How API chats differ from the widget
- The conversation is saved like a widget chat and appears on the agent's Conversations tab, with its leads, ratings and statistics. It has no country and no page.
- Allowed domains, blocked countries and messages per visitor don't apply: there is no visitor's page or address to check. The plan's messages, the agent's monthly cap and the key's rate limit do.
- A private agent refuses API chats with
agent_private. - Client actions run in a visitor's browser, so the agent isn't offered them in API chats. API actions, buttons, forms and booking work.
- Files can't be attached, and there are no temporary chats.
- Only conversations started through the API can be continued through the API. The agent sees the last 40 messages of the conversation as its history.
- While someone from your team has taken over the chat in the Live chat inbox, your message is saved,
replyis empty andliveChatispaused. The team member's replies appear in the conversation: read them with Get a conversation, where they have anauthorName. A message during a takeover uses none of your plan's messages. - A reply that takes longer than 55 seconds ends with a
504error and the codetimeout. It may still count as a message.
Streaming
With "stream": true, the response is the reply's text as it is written, as text/plain, without the JSON fields. The X-Conversation-Id and X-Session-Id headers carry the conversation and session, and X-Live-Chat the live chat state when there is one. Sources, buttons and the other blocks are not streamed; read them afterwards with Get a conversation. Errors are JSON, as without streaming.
curl -N https://www.intochat.ai/api/v1/agents/AGENT_ID/chat \
-H "Authorization: Bearer ic_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "message": "What are your opening hours?", "stream": true }'
List conversations
GET /api/v1/agents/{agentId}/conversations ยท scope conversations:read
The agent's conversations, from the widget and the API, newest first, without their messages. Takes limit, cursor and since; see Pagination.
curl "https://www.intochat.ai/api/v1/agents/AGENT_ID/conversations?limit=50&since=2026-10-01T00:00:00Z" \
-H "Authorization: Bearer ic_live_YOUR_KEY"
{
"data": [
{
"id": "CONVERSATION_ID",
"sessionId": "8f14e45f-ceea-467a-9575-1a2b3c4d5e6f",
"channel": "widget",
"createdAt": "2026-10-04T09:00:00.000Z",
"lastActivityAt": "2026-10-04T09:12:00.000Z",
"country": "CH",
"page": "https://www.example.com/pricing",
"messageCount": 6,
"topics": ["Pricing"],
"sentiment": "positive",
"liveChat": null,
"csatScore": null
}
],
"hasMore": false,
"nextCursor": null
}
channel is api for a conversation started through the API, otherwise widget (the script tag, the inline iframe, the direct link and the Playground). topics and sentiment are set by topic analysis when it's on. liveChat is null, requested, active or ended. csatScore is the visitor's 1-to-5 rating after a live chat, or null.
Get a conversation
GET /api/v1/conversations/{conversationId} ยท scope conversations:read
One conversation of any agent in the account, with every message in order.
{
"data": {
"id": "CONVERSATION_ID",
"agentId": "AGENT_ID",
"sessionId": "icapi5f0c2b9e4d7a41f8a3c6e1b2d9f07a64",
"channel": "api",
"createdAt": "2026-10-04T09:00:00.000Z",
"lastActivityAt": "2026-10-04T09:14:00.000Z",
"country": null,
"page": null,
"topics": [],
"sentiment": null,
"handoffRequestedAt": null,
"liveChat": { "status": "ended", "startedAt": "2026-10-04T09:05:00.000Z", "endedAt": "2026-10-04T09:13:00.000Z" },
"csat": { "score": 5, "comment": "Quick and friendly.", "ratedAt": "2026-10-04T09:14:00.000Z" },
"messages": [
{ "id": "MESSAGE_ID", "role": "assistant", "content": "Hello! How can I help you today?", "createdAt": "2026-10-04T09:00:00.000Z", "authorName": null, "sources": [] },
{ "id": "MESSAGE_ID", "role": "user", "content": "Can I change my delivery address?", "createdAt": "2026-10-04T09:01:00.000Z", "authorName": null, "sources": [] },
{ "id": "MESSAGE_ID", "role": "assistant", "content": "Yes, I'm on it. What's your order number?", "createdAt": "2026-10-04T09:06:00.000Z", "authorName": "Anna", "sources": [] }
]
}
}
role is user for the visitor or your API messages, and assistant for the agent and your team. authorName is the first name of the team member who wrote a live chat reply, or null when the agent wrote it. content is the text without the widget's control blocks; an agent reply's sources lists the pages it showed. handoffRequestedAt is when the agent emailed the conversation to your team, or null. liveChat and csat are null when there was no live chat or no rating.
List leads
GET /api/v1/agents/{agentId}/leads ยท scope leads:read
The agent's leads, newest first. Takes limit, cursor and since; see Pagination.
{
"data": [
{
"id": "LEAD_ID",
"email": "jane@example.com",
"phone": null,
"name": "Jane",
"customFields": [{ "id": "FIELD_ID", "label": "Company", "value": "Acme" }],
"source": "form",
"conversationId": "CONVERSATION_ID",
"collectedAt": "2026-10-04T09:03:00.000Z",
"lastSeenAt": "2026-10-04T09:03:00.000Z",
"submissionCount": 1
}
],
"hasMore": false,
"nextCursor": null
}
source is form for the lead form or chat for contact details written in a message. customFields uses the labels your form has now. lastSeenAt and submissionCount change when the same person submits again. conversationId is null when the conversation was deleted.
List sources
GET /api/v1/agents/{agentId}/sources ยท scope sources:read
The agent's knowledge sources in the order of its Knowledge tab, with training progress. The text of a source isn't included, except a Q&A pair's question and answer.
{
"data": [
{
"id": "SOURCE_ID",
"type": "qa",
"title": "Do you ship to Switzerland?",
"url": null,
"filename": null,
"question": "Do you ship to Switzerland?",
"answer": "Yes, in 3 to 5 working days.",
"characters": 71,
"status": "trained",
"error": null,
"trainedAt": "2026-10-02T09:00:00.000Z",
"createdAt": "2026-10-02T08:59:00.000Z",
"updatedAt": "2026-10-02T09:00:00.000Z"
}
],
"training": { "total": 18, "trained": 18, "pending": 0, "processing": 0, "failed": 0, "done": true },
"characters": { "used": 48210, "limit": 500000 }
}
type is website, file, text, qa or notion. status is pending, processing, trained or failed, with the reason in error. characters.limit is the training characters per agent on your plan.
Add a source
POST /api/v1/agents/{agentId}/sources ยท scope sources:write
Adds a text snippet or a Q&A pair and starts training, as the Knowledge tab does. The agent uses the new source once it is trained; follow its status with List sources.
A text snippet:
curl https://www.intochat.ai/api/v1/agents/AGENT_ID/sources \
-H "Authorization: Bearer ic_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "type": "text", "title": "Opening hours", "content": "We are open Monday to Friday, 9:00 to 17:00." }'
A Q&A pair:
{ "type": "qa", "question": "Do you ship to Switzerland?", "answer": "Yes, in 3 to 5 working days." }
{
"data": { "id": "SOURCE_ID", "type": "text", "title": "Opening hours", "status": "pending", "characters": 44 },
"result": "created",
"training": { "total": 19, "trained": 18, "pending": 1, "processing": 0, "failed": 0, "done": false }
}
data has all the fields of List sources; some are left out above. result is created (status 201), updated when a Q&A pair with the same question, ignoring upper and lower case, already existed and now has the new answer, or unchanged when it had exactly this answer (both status 200). A text snippet is always added as a new source. A title and a question can each be up to 200 characters.
The source counts toward the agent's training characters. A source that would go over your plan's limit gets a 403 error with the code character_limit_exceeded, and nothing is added. See Training characters per agent.
Delete a source
DELETE /api/v1/agents/{agentId}/sources/{sourceId} ยท scope sources:write
Deletes the source and what the agent learned from it, at once, as deleting it on the Knowledge tab does. Nothing else needs training again.
{ "id": "SOURCE_ID", "deleted": true, "training": { "total": 17, "trained": 17, "pending": 0, "processing": 0, "failed": 0, "done": true } }
Subscribe a webhook
POST /api/v1/agents/{agentId}/webhooks ยท scope webhooks:write
Subscribes a webhook to the agent's events: a REST hook, as Zapier's and Make's instant triggers use it. intoCHAT then sends each event to the address as described in Webhooks, signed and retried like a webhook added on the Leads tab.
| Field | Content |
|---|---|
url | Required. The address to send events to. As on the Leads tab, it must start with https:// and point to a public server; private and internal network addresses are refused. |
events | Required. One or more of lead.created, handoff.requested, form.submitted, booking.created, live_chat.requested and return.requested. See Events. |
source | Optional. zapier, make or api (the default): what the Leads tab shows next to the webhook, for example via Zapier. |
curl https://www.intochat.ai/api/v1/agents/AGENT_ID/webhooks \
-H "Authorization: Bearer ic_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "url": "https://hooks.example.com/intochat", "events": ["lead.created"], "source": "api" }'
{
"id": "WEBHOOK_ID",
"agentId": "AGENT_ID",
"url": "https://hooks.example.com/intochat",
"events": ["lead.created"],
"source": "api",
"secret": "whsec_โฆ",
"createdAt": "2026-10-08T09:00:00.000Z"
}
The response has status 201. Keep id to unsubscribe later. secret is the webhook's signing secret, returned only here; use it to verify the signature if your receiver can. Tools that can't check signatures, such as Zapier and Make, can ignore it.
A subscribed webhook appears on the agent's Leads tab with a via Zapier, via Make or via API badge, where the owner can pause or remove it. It remembers the key that subscribed it: revoking that key deletes it. An agent can have up to 25 subscribed webhooks, apart from the 5 added on the Leads tab; past that, the request gets a 400 error.
Unsubscribe a webhook
DELETE /api/v1/webhooks/{webhookId} ยท scope webhooks:write
Deletes a webhook that was subscribed through the API, with its delivery history. Any key of the same account can do it. A webhook added on the Leads tab, another account's webhook, or one already removed gets 404.
{ "id": "WEBHOOK_ID", "deleted": true }
List events
GET /api/v1/agents/{agentId}/events/{event} ยท scope agents:read, plus leads:read or conversations:read
The agent's latest events of one kind, newest first, each shaped exactly like the body of a webhook delivery. Automation tools poll it instead of waiting for a webhook, for example to show real sample data. Takes limit, from 1 to 100 (default 20).
event | Also needs | Built from |
|---|---|---|
lead.created | leads:read | Leads, newest saved first |
form.submitted | leads:read | Form submissions |
booking.created | leads:read | Bookings |
return.requested | leads:read | Return requests |
handoff.requested | conversations:read | Conversations handed off by email |
live_chat.requested | conversations:read | Conversations where the agent asked your team to join |
conversation.created | conversations:read | New conversations. This event has no webhook; it can only be listed. |
curl "https://www.intochat.ai/api/v1/agents/AGENT_ID/events/lead.created?limit=5" \
-H "Authorization: Bearer ic_live_YOUR_KEY"
{
"data": [
{
"id": "LEAD_ID",
"event": "lead.created",
"created_at": "2026-10-07T09:30:00.000Z",
"agent": { "id": "AGENT_ID", "name": "Acme Assistant" },
"data": {
"leadId": "LEAD_ID",
"agentId": "AGENT_ID",
"agentName": "Acme Assistant",
"email": "jane@example.com",
"phone": null,
"name": "Jane",
"customFields": [],
"source": "form",
"conversationId": "CONVERSATION_ID",
"collectedAt": "2026-10-07T09:30:00.000Z"
}
}
]
}
The differences from a delivery:
idis the record's ID, not a delivery ID, so the same event always has the sameid: the lead, submission, booking, return request or conversation ID. For hand-offs and live chat requests it is the conversation ID, a colon and the time in milliseconds, because a conversation can ask again later.created_atis when the record was saved.- The data is rebuilt from what intoCHAT stores. A hand-off's
summaryisn't stored, so it isnull, andtestisfalse. A hand-off that opened a helpdesk ticket carries itsticket, as the webhook does. A booking'sproviderStatusispendingwhile it waits for your confirmation, otherwiseaccepted. A return request has its currentstatus,requestedorhandled. conversation.createddata hasconversationId,channel(widgetorapi),page,countryandcreatedAt.
Get sample events
GET /api/v1/agents/{agentId}/events/{event}/samples ยท scope agents:read
Up to 3 of the agent's latest events, as in List events, so a tool always has data to map fields from. When the agent has none yet, or the key doesn't have the scope that event needs (leads:read or conversations:read), it returns one documented example with the same fields instead, and sample is true.
{
"data": [
{
"id": "sample_booking.created",
"event": "booking.created",
"created_at": "2026-10-07T09:30:00.000Z",
"agent": { "id": "AGENT_ID", "name": "Acme Assistant" },
"data": { "bookingId": "sample_booking", "eventTitle": "30 min intro call", "attendeeEmail": "jane@example.com" }
}
],
"sample": true
}
The example's data has every field of the event; some are left out above.
Limits
- The API is included from Starter. On Free, keys can't be created and existing ones get
plan_required. - 60 requests a minute per key, and at most 20 active keys per account.
- Each chat reply counts as one message on your plan. An agent answers at most 1,000 messages an hour, widget and API together.
- A chat reply must finish within 55 seconds.
- Sources: text snippets and Q&A pairs only. Website pages, files and Notion pages are added in the dashboard.
- Up to 25 subscribed webhooks per agent, apart from the 5 added on the Leads tab.
- There are no endpoints yet to create or change agents, submit forms, book meetings or take over live chats.
Next steps
- Get leads and hand-offs pushed to your server as they happen: Webhooks.
- Use intoCHAT in Zapier or Make: Zapier and Make.
- Let your agent call your own API while it answers: API actions.
- Write good text snippets and Q&A pairs: Text and Q&A.
- Check what your plan includes: Plans and limits.