Skip to content
Documentation

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

  1. In the sidebar, under Settings, click API keys.
  2. Under Create a key, give the key a Name you will recognize later, for example CRM sync.
  3. 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.
  4. Click Create key.
  5. 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.

ScopeAllows
agents:readList agents and get an agent
chatSend a message to an agent. Each reply counts as a message on your plan.
conversations:readList conversations and get a conversation with its transcript
leads:readList leads, and the lead, form, booking and return events of List events and Get sample events
sources:readList sources
sources:writeAdd a source and delete a source, which also starts training
webhooks:writeSubscribe 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:

HeaderValue
X-RateLimit-Limit60
X-RateLimit-RemainingRequests left in the current minute
X-RateLimit-ResetWhen 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.

StatusCodeMeaning
400invalid_requestThe request is missing something or has a wrong value. The message says which.
401unauthorizedNo key, or the key is malformed, unknown or revoked.
403insufficient_scopeThe key doesn't have the scope this endpoint needs.
403plan_requiredThe account is on Free. The API is included from Starter.
403message_limit_reachedThe account has used all the messages its plan includes this period.
403agent_cap_reachedThe agent reached the monthly message cap set under Share, Limits and access.
403agent_privateThe agent is private, so it answers only in the dashboard. See Private agents.
403character_limit_exceededA new source would take the agent past its plan's training characters.
404not_foundNo such agent, conversation, source or webhook in this account.
429rate_limitedThe key made more than 60 requests this minute.
429agent_busyThe agent is answering too many messages right now.
500internal_errorSomething went wrong on our side. Try again.
504timeoutThe 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:

ParameterContent
limitItems per page, from 1 to 100. The default is 20.
cursorThe nextCursor of the previous page, to get the next one.
sinceAn 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.

FieldContent
messageRequired. The text, up to 10,000 characters.
conversationIdOptional. Continue a conversation you started through the API, with the conversationId of an earlier reply.
sessionIdOptional. 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.
streamOptional. true streams the reply's text; see Streaming. The default is false.
timeZoneOptional. 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
}
FieldContent
replyThe 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).
conversationIdThe conversation, as in Get a conversation.
sessionIdThe conversation's session in intoCHAT. It always starts with icapi.
sourcesThe 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.
followUpsSuggested next questions, when Suggest follow-up questions is on.
buttonsCustom buttons the agent chose, as label and url.
productsProduct cards from your store or API actions, with name, url and, when known, image, price, compareAtPrice, currency and available.
formA 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.
leadFormThe 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.
bookingOpen 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.
liveChatnull, 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, reply is empty and liveChat is paused. The team member's replies appear in the conversation: read them with Get a conversation, where they have an authorName. A message during a takeover uses none of your plan's messages.
  • A reply that takes longer than 55 seconds ends with a 504 error and the code timeout. 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.

FieldContent
urlRequired. 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.
eventsRequired. One or more of lead.created, handoff.requested, form.submitted, booking.created, live_chat.requested and return.requested. See Events.
sourceOptional. 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).

eventAlso needsBuilt from
lead.createdleads:readLeads, newest saved first
form.submittedleads:readForm submissions
booking.createdleads:readBookings
return.requestedleads:readReturn requests
handoff.requestedconversations:readConversations handed off by email
live_chat.requestedconversations:readConversations where the agent asked your team to join
conversation.createdconversations:readNew 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:

  • id is the record's ID, not a delivery ID, so the same event always has the same id: 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_at is when the record was saved.
  • The data is rebuilt from what intoCHAT stores. A hand-off's summary isn't stored, so it is null, and test is false. A hand-off that opened a helpdesk ticket carries its ticket, as the webhook does. A booking's providerStatus is pending while it waits for your confirmation, otherwise accepted. A return request has its current status, requested or handled.
  • conversation.created data has conversationId, channel (widget or api), page, country and createdAt.

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

View as Markdown