Server-side chat API

Create chat sessions, post customer and agent messages, show typing indicators, hand off bot conversations, and record CSAT from your backend with an API key, using the v2.1 chat endpoints.

The chat SDKs act as a customer, from a browser or device. The v2.1 chat API does the same jobs from your server with a regular API key, which is what you need when chat runs through something you host: your own bot, a messaging bridge, or an AI agent that answers before a human.

Every route takes the customer in the request (customerId) rather than a chat SDK token, and the messages show up in the customer's chat on every device, like any other chat message.

You want toEndpoint
Open a chat conversation for a customerPOST /v2.1/chat/sessions
Import a conversation your bot already hadPOST /v2.1/chat/handoff
Post a message as the customerPOST /v2.1/chat/messages
Post a message as an agentPOST /v2.1/chat/outbound/messages
Show or hide a typing indicatorPOST /v2.1/chat/outbound/typing
Post assistant (bot) messagesPOST /v2.1/chat/outbound/assistant/messages
List a customer's chat conversationsGET /v2.1/chat/sessions
Read a conversation's messagesGET /v2.1/chat/sessions/{id}/messages
Record a CSAT rating or commentPUT /v2.1/chat/satisfaction-responses/{id}

Roles

EndpointRole
Create sessionorg.permission.conversation.create
Handofforg.permission.conversation.create and org.permission.message.create
Messages and typingorg.permission.message.create
List sessionsorg.permission.conversation.read
List messagesorg.permission.message.read
CSATorg.permission.conversation.update

Use a server-side API key with these roles, never a chat key. See API keys and roles.

Answer chats with your own agent

A common pattern: your service (a bot or an AI agent) answers customers in chat, and hands off to Kustomer routing when it can't help.

Your serviceKustomerCustomer (chat SDK)Your serviceKustomerCustomer (chat SDK)Sends a message1Webhook or workflow: kustomer.message.create2POST /v2.1/chat/outbound/typing (typing: true)3Work out the answer4POST /v2.1/chat/outbound/messages (auto: true)5POST /v2.1/chat/outbound/typing (typing: false)6Reply and typing state, in realtime7
Diagram source
sequenceDiagram
  autonumber
  participant C as Customer (chat SDK)
  participant K as Kustomer
  participant S as Your service
  C->>K: Sends a message
  K-->>S: Webhook or workflow: kustomer.message.create
  S->>K: POST /v2.1/chat/outbound/typing (typing: true)
  S->>S: Work out the answer
  S->>K: POST /v2.1/chat/outbound/messages (auto: true)
  S->>K: POST /v2.1/chat/outbound/typing (typing: false)
  K-->>C: Reply and typing state, in realtime
  1. Hear about new messages

    Subscribe to kustomer.message.create with an outbound webhook, or call your service from a workflow. Only act on inbound chat messages: check the message's channel is chat and its direction is in.

  2. Show that you're typing

    cURL
    curl -X POST "https://acme.api.kustomerapp.com/v2.1/chat/outbound/typing" \
      -H "Authorization: Bearer $KUSTOMER_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{"customerId": "6716a3a9c4e5f60012ab34b0", "session": "6716a3a9c4e5f60012ab34c1", "from": "5f1b2c3d4e5f600012ab0001", "typing": true, "ai": true}'

    ai: true shows the AI typing indicator instead of the agent one. You own the indicator's lifecycle: sending a message doesn't hide it, so send typing: false when you're done, and repeat typing: true about every three seconds for long answers.

  3. Reply

    cURL
    curl -X POST "https://acme.api.kustomerapp.com/v2.1/chat/outbound/messages" \
      -H "Authorization: Bearer $KUSTOMER_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "customerId": "6716a3a9c4e5f60012ab34b0",
        "session": "6716a3a9c4e5f60012ab34c1",
        "from": "5f1b2c3d4e5f600012ab0001",
        "body": "Your order shipped this morning. Tracking: 1Z999AA10123456784",
        "auto": true
      }'

    from is the Kustomer user the message is attributed to; use a dedicated user for your bot. auto: true marks the message as automated, so it doesn't count as the first human response in your reporting.

  4. Hand off

    When a human should take over, stop replying and let routing assign the conversation, for example by updating a conversation attribute that your queue rules match on. A workflow can make that update for you.

Create a session

post/v2.1/chat/sessions

Opens a new chat conversation for an existing customer, for example to start a proactive chat:

Body
{
  "customerId": "6716a3a9c4e5f60012ab34b0",
  "title": "Your order is delayed",
  "brand": "5f1b2c3d4e5f600012ab0099"
}

trackingId links the session to a chat SDK identity. Leave it out and Kustomer reuses the one from the customer's latest chat session, if any. When you pass assistant, Kustomer also starts that assistant's opening dialog in the new session.

Import a bot conversation

post/v2.1/chat/handoff

If a conversation started in your own bot, import the whole thing so agents see the history:

Body
{
  "customerId": "6716a3a9c4e5f60012ab34b0",
  "active": true,
  "from": "Acme Bot",
  "externalId": "bot-thread-88123",
  "messages": [
    { "date": "2026-10-21T15:00:02Z", "direction": "in", "body": "Where's my order?" },
    { "date": "2026-10-21T15:00:04Z", "direction": "out", "body": "I can help. What's your order number?" },
    { "date": "2026-10-21T15:00:31Z", "direction": "in", "body": "4417, and I need to talk to a person" }
  ]
}
FieldRule
messages2 to 100 messages, with at least one in and one out. Sorted by date. Bodies up to 10,240 characters.
fromApplied to every outbound message
activetrue creates the conversation open, ready for routing. false creates it closed, as a record only.
externalIdStored on the session so you can link back to your bot's thread
subStatusOptional conversation substatus ID. It's an open substatus when active is true, and a done substatus when active is false.
trackingIdOptional tracking identity ID to attach to the session. Left out, Kustomer may reuse the customer's latest chat session tracking ID, so omitting it doesn't guarantee an untracked session.

The chat SDKs have the same feature as handoff(), for imports that happen in the customer's browser or app.

Customer messages from your server

post/v2.1/chat/messages

Posts a message as the customer, for bridges that receive customer messages somewhere else. customerId must match the session's customer. Send body, attachments, or both.

Read chats

List a customer's open chats
curl "https://acme.api.kustomerapp.com/v2.1/chat/sessions?customerId=6716a3a9c4e5f60012ab34b0&active=true" \
  -H "Authorization: Bearer $KUSTOMER_API_KEY"

Pass customerId, trackingId, or both (sessions matching either are returned); with neither you get a 400. Results are newest first by lastMessageAt. Session metadata doesn't include messages: fetch those with GET /v2.1/chat/sessions/{id}/messages, using fields to limit the attributes returned.

CSAT

put/v2.1/chat/satisfaction-responses/{id}

Records a rating, a comment, or both on a CSAT response that Kustomer already created for the chat:

BodyResult
rating onlyStatus rated
answers (with or without rating)Status commented
read: trueMarks the survey read

rating is 0 to 10, and answers takes up to three answers. Add ?include=satisfaction to get the form back, with the follow-up questions for that rating.