# Create a chat session

> POST /v2.1/chat/sessions · Chat conversations API

Source: https://developer.kustomer.com/api/chat/create-a-chat-session

```http
POST https://{orgname}.api.kustomerapp.com/v2.1/chat/sessions
Authorization: Bearer <API key>
```

## Authorization

Requires any one of these roles: `org.permission.conversation.create`. The API key or user must hold at least one of them.

Create a linked chat session for a customer using an API token.

Identity is supplied in the JSON body (`customerId`, optional `trackingId`),
not via a `/c/` tracking JWT. Unlike `POST /c/v2.1/chat/sessions`, this route
always creates a **linked** session (sobjects conversation).

If `trackingId` is omitted, the API may reuse the tracking id from the
customer's latest prior chat session that had one. If none is found, the
session is still created and `trackingId` may be unset.

### Assistant bootstrap

When the request includes **`assistant`**, Kustomer also bootstraps the
assistant dialog during this create call:

1. Resolves the assistant's opening dialog/node and greeting / opening
   messages (same logic as customer `assistant/init`).
2. Persists those outbound greeting messages on the session.
3. Creates assistant run state on that dialog/node with
   `progress.status: ready` so a later customer reply (for example via
   Chat SDK `/c/v2/chat/assistant/messages`) can continue the tree
   **without** a separate mock inbound kickoff.
4. May publish the greeting messages on the session's PubNub conversation
   channel immediately after create.

The **201** session payload may reflect updated `preview` /
`lastMessageAt` after greetings are persisted. Bootstrap failures are
**soft-failed**: they are logged and do **not** change the success
response (session create still returns **201**). Duplicate assistant
state (HTTP 409 from Kustomer) is treated as idempotent success.

## About Chat conversations

The server-side chat API lets your backend take part in Kustomer chat with a regular API key. It can open chat sessions for a customer, post messages as the customer, an agent or an assistant, show typing, import bot conversations and record satisfaction ratings.

A chat session is a Kustomer conversation on the chat channel, seen from the chat side. Its ID is the conversation ID, so the same conversation appears in the agent desktop and in the customer's chat widget or app, and you can read it with the [conversations API](https://developer.kustomer.com/api/core-resources#conversations) too. Sessions are usually created by the [chat SDKs](https://developer.kustomer.com/docs/chat) when a customer starts a chat. These v2.1 endpoints do the same jobs from your server, for custom chat front ends, messaging bridges and bots that answer before a human does. Anything you post is delivered in realtime to the customer's open chat on every device.

A session stays active until the chat is ended or deleted. Listing with `active=true` returns only active sessions that have at least one message.

#### Identity: customerId and trackingId

These endpoints never use a chat key or a customer's chat login token. Every request names the customer in `customerId`, and Kustomer checks it against the session: a mismatch fails with `403`. `trackingId` is the chat SDK's ID for a visitor's browser or device. It's optional, but when a session has one, any `trackingId` you send must match. Creating a session without one reuses the tracking ID from the customer's latest chat session, if there is one; handoff imports never set one.

Use a server-side API key with the roles each endpoint lists, never a chat key, which must hold only `org.tracking`.

#### Who a message is from

| Endpoint | Appears as |
| --- | --- |
| `POST /v2.1/chat/messages` | The customer (`direction: in`). Returns `202 Accepted`; the message is processed asynchronously. |
| `POST /v2.1/chat/assistant/messages` | The customer, replying inside an assistant (bot) flow, so the assistant continues from where it is. |
| `POST /v2.1/chat/outbound/messages` | An agent: `from` is the Kustomer user ID. Add `auto: true` for bot or AI replies so they don't count as a human first response. |
| `POST /v2.1/chat/outbound/assistant/messages` | An assistant line, attributed to `from`. Send exactly one of `body`, `meta.template` or `attachments`. |

Message bodies are up to 10,240 characters. Typing indicators are ephemeral and never stored. Your service owns their lifecycle: sending a message doesn't hide one, so send `typing: false` when you're done, and resend `typing: true` about every three seconds for long answers.

#### Which endpoint to use

| To | Call |
| --- | --- |
| Open a chat for a customer, optionally starting an assistant | [`POST /v2.1/chat/sessions`](https://developer.kustomer.com/api/chat/create-a-chat-session) |
| Import a conversation your own bot already had | [`POST /v2.1/chat/handoff`](https://developer.kustomer.com/api/chat/create-a-chat-session-via-handoff) |
| List a customer's or visitor's chats | [`GET /v2.1/chat/sessions`](https://developer.kustomer.com/api/chat/list-chat-sessions) |
| Read a chat's messages | [`GET /v2.1/chat/sessions/{id}/messages`](https://developer.kustomer.com/api/chat/list-messages-for-a-chat-session) |
| Post as the customer | [`POST /v2.1/chat/messages`](https://developer.kustomer.com/api/chat/create-an-inbound-chat-message) or, in an assistant flow, [`POST /v2.1/chat/assistant/messages`](https://developer.kustomer.com/api/chat/create-an-inbound-assistant-chat-message) |
| Post as an agent or bot | [`POST /v2.1/chat/outbound/messages`](https://developer.kustomer.com/api/chat/create-an-outbound-agent-chat-message) |
| Post as an assistant | [`POST /v2.1/chat/outbound/assistant/messages`](https://developer.kustomer.com/api/chat/create-an-outbound-assistant-chat-message) |
| Show or hide typing | [`POST /v2.1/chat/outbound/typing`](https://developer.kustomer.com/api/chat/show-or-hide-agent-ai-typing-indicator) |
| Record a CSAT rating or comment | [`PUT /v2.1/chat/satisfaction-responses/{id}`](https://developer.kustomer.com/api/chat/update-a-chat-satisfaction-response) |

A handoff takes 2 to 100 messages, including at least one inbound and one outbound, and sorts them by `date`; `active: false` creates the conversation already closed. When you pass `assistant` on session create, Kustomer posts the assistant's greeting, and if that fails the session is still created. Session lists are paginated, newest first by `lastMessageAt`, and don't include messages. The CSAT response must already exist, created by Kustomer for the chat's survey; this call fills it in, and sending `answers` marks it commented rather than rated.

Related: [Server-side chat API](https://developer.kustomer.com/docs/chat/server-chat-api), [How chat works](https://developer.kustomer.com/docs/chat/realtime-architecture), [Customer identity](https://developer.kustomer.com/docs/chat/identity), [Conversations](https://developer.kustomer.com/api/core-resources#conversations)

### Recipes

#### 1. Answer a customer's chat from your own bot

Reply in a live chat from a bot or AI service you host, with a typing indicator while it works, so the customer sees the answer in realtime.

**Step 1.** Hear about new chat messages with an [outbound webhook](https://developer.kustomer.com/docs/events/outbound-webhooks) on message creation, or call your service from a [workflow](https://developer.kustomer.com/docs/workflows). Act only on inbound chat messages, and take the conversation ID (the session ID) and customer ID from the event. Use a dedicated Kustomer user for your bot as `from` in the next calls.

**Step 2.** [`POST /v2.1/chat/outbound/typing`](https://developer.kustomer.com/api/chat/show-or-hide-agent-ai-typing-indicator.md). Show the typing indicator. `ai: true` shows the AI indicator instead of the agent one. Typing is never stored, and for long answers you must resend `typing: true` about every three seconds.

```bash title="cURL"
curl --request POST "https://api.kustomerapp.com/v2.1/chat/outbound/typing" \
  --header "Authorization: Bearer $KUSTOMER_API_KEY" \
  --header "Content-Type: application/json" \
  --data "{\"customerId\": \"$CUSTOMER_ID\", \"session\": \"$SESSION_ID\", \"from\": \"$BOT_USER_ID\", \"typing\": true, \"ai\": true}"
```

```bash title="kusty"
kusty api POST /v2.1/chat/outbound/typing \
  --field customerId=$CUSTOMER_ID --field session=$SESSION_ID --field from=$BOT_USER_ID \
  --field typing:=true --field ai:=true
```

**Step 3.** [`POST /v2.1/chat/outbound/messages`](https://developer.kustomer.com/api/chat/create-an-outbound-agent-chat-message.md). Post the reply. `auto: true` marks it as automated, so it doesn't count as the first human response in reporting. `customerId` must be the session's customer, or the call fails with `403`. Bodies are up to 10,240 characters.

```bash title="cURL"
curl --request POST "https://api.kustomerapp.com/v2.1/chat/outbound/messages" \
  --header "Authorization: Bearer $KUSTOMER_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{
    "customerId": "'"$CUSTOMER_ID"'",
    "session": "'"$SESSION_ID"'",
    "from": "'"$BOT_USER_ID"'",
    "body": "Your order shipped this morning.",
    "auto": true
  }'
```

```bash title="kusty"
kusty api POST /v2.1/chat/outbound/messages \
  --field customerId=$CUSTOMER_ID --field session=$SESSION_ID --field from=$BOT_USER_ID \
  --field "body=Your order shipped this morning." --field auto:=true
```

**Step 4.** [`POST /v2.1/chat/outbound/typing`](https://developer.kustomer.com/api/chat/show-or-hide-agent-ai-typing-indicator.md). Hide the indicator. Sending a message doesn't hide it, so always finish with `typing: false`, including when your service fails.

```bash title="cURL"
curl --request POST "https://api.kustomerapp.com/v2.1/chat/outbound/typing" \
  --header "Authorization: Bearer $KUSTOMER_API_KEY" \
  --header "Content-Type: application/json" \
  --data "{\"customerId\": \"$CUSTOMER_ID\", \"session\": \"$SESSION_ID\", \"from\": \"$BOT_USER_ID\", \"typing\": false}"
```

```bash title="kusty"
kusty api POST /v2.1/chat/outbound/typing \
  --field customerId=$CUSTOMER_ID --field session=$SESSION_ID --field from=$BOT_USER_ID --field typing:=false
```

#### 2. Hand a bot conversation over to your agents

When a chat that started in your own bot needs a human, import the whole exchange into Kustomer so agents pick it up with the history.

**Step 1.** [`GET /v1/customers/email={email}`](https://developer.kustomer.com/api/core-resources/get-customer-by-email.md). Find the customer by the email your bot collected. A `404` means they don't exist yet.

```bash title="cURL"
curl "https://api.kustomerapp.com/v1/customers/email=$EMAIL" \
  --header "Authorization: Bearer $KUSTOMER_API_KEY"
```

```bash title="kusty"
kusty api GET "/v1/customers/email=$EMAIL" --fields id
```

**Step 2.** [`POST /v1/customers`](https://developer.kustomer.com/api/core-resources/create-customer.md). Only if the lookup returned `404`: create the customer, and use the new `data.id`.

```bash title="cURL"
curl --request POST "https://api.kustomerapp.com/v1/customers" \
  --header "Authorization: Bearer $KUSTOMER_API_KEY" \
  --header "Content-Type: application/json" \
  --data "{\"name\": \"$NAME\", \"emails\": [{\"email\": \"$EMAIL\"}]}"
```

```bash title="kusty"
kusty api POST /v1/customers --field "name=$NAME" --field emails.0.email=$EMAIL --fields id
```

**Step 3.** [`POST /v2.1/chat/handoff`](https://developer.kustomer.com/api/chat/create-a-chat-session-via-handoff.md). Import the exchange. `messages` takes 2 to 100 messages with at least one `in` and one `out`, and Kustomer sorts them by `date`; `from` labels every outbound line. `active: true` creates the conversation open, ready for routing. Put your bot's thread ID in `externalId` to link back to it. The response's `data.id` is the new conversation ID.

```bash title="cURL"
curl --request POST "https://api.kustomerapp.com/v2.1/chat/handoff" \
  --header "Authorization: Bearer $KUSTOMER_API_KEY" \
  --header "Content-Type: application/json" \
  --data @handoff.json
```

```bash title="kusty"
# handoff.json: {"customerId", "active": true, "from": "Acme Bot", "externalId", "messages": [{"date", "direction", "body"}, ...]}
jq --arg c "$CUSTOMER_ID" '.customerId = $c' handoff.json \
  | kusty api POST /v2.1/chat/handoff --data @- --fields id
```

> A handoff doesn't need a tracking JWT. Send `trackingId` to tie the imported chat to a browser or device. If you leave it out, Kustomer may reuse the customer's latest chat session tracking ID, so omitting it doesn't guarantee an untracked session. Send `active: false` instead to keep a closed record of a conversation your bot finished on its own.

#### 3. Start a proactive chat without duplicating an open one

Reach out to a customer in chat, for example about a delayed order, reusing their active chat if they have one instead of opening a second conversation.

**Step 1.** [`GET /v2.1/chat/sessions`](https://developer.kustomer.com/api/chat/list-chat-sessions.md). List the customer's active sessions. They come back newest first by `lastMessageAt`, and only sessions with at least one message count as active. If there's one, use its `id` and skip the next step.

```bash title="cURL"
curl "https://api.kustomerapp.com/v2.1/chat/sessions?customerId=$CUSTOMER_ID&active=true" \
  --header "Authorization: Bearer $KUSTOMER_API_KEY"
```

```bash title="kusty"
kusty api GET /v2.1/chat/sessions --query customerId=$CUSTOMER_ID --query active=true \
  | jq -r '.data[0].id // empty'
```

**Step 2.** [`POST /v2.1/chat/sessions`](https://developer.kustomer.com/api/chat/create-a-chat-session.md). Otherwise open a session. `title` names the conversation, and `brand` picks which brand's chat it belongs to. The response's `data.id` is the session and conversation ID.

```bash title="cURL"
curl --request POST "https://api.kustomerapp.com/v2.1/chat/sessions" \
  --header "Authorization: Bearer $KUSTOMER_API_KEY" \
  --header "Content-Type: application/json" \
  --data "{\"customerId\": \"$CUSTOMER_ID\", \"title\": \"Your order is delayed\"}"
```

```bash title="kusty"
kusty api POST /v2.1/chat/sessions \
  --field customerId=$CUSTOMER_ID --field "title=Your order is delayed" --fields id
```

**Step 3.** [`POST /v2.1/chat/outbound/messages`](https://developer.kustomer.com/api/chat/create-an-outbound-agent-chat-message.md). Post the message as an agent; `from` is the Kustomer user it's attributed to. It's delivered to the customer's open chat on every device.

```bash title="cURL"
curl --request POST "https://api.kustomerapp.com/v2.1/chat/outbound/messages" \
  --header "Authorization: Bearer $KUSTOMER_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{
    "customerId": "'"$CUSTOMER_ID"'",
    "session": "'"$SESSION_ID"'",
    "from": "'"$AGENT_USER_ID"'",
    "body": "Your order 4417 is running two days late. Reply here if you have any questions."
  }'
```

```bash title="kusty"
kusty api POST /v2.1/chat/outbound/messages \
  --field customerId=$CUSTOMER_ID --field session=$SESSION_ID --field from=$AGENT_USER_ID \
  --field "body=Your order 4417 is running two days late. Reply here if you have any questions."
```

#### 4. Export a customer's chat transcripts

Pull every chat a customer has had, with the messages, for example to answer a data access request or archive chats in another system.

**Step 1.** [`GET /v2.1/chat/sessions`](https://developer.kustomer.com/api/chat/list-chat-sessions.md). Page through the customer's sessions. Leave out `active` to get closed chats too. Sessions don't include messages.

```bash title="cURL"
curl "https://api.kustomerapp.com/v2.1/chat/sessions?customerId=$CUSTOMER_ID&pageSize=100" \
  --header "Authorization: Bearer $KUSTOMER_API_KEY"
```

```bash title="kusty"
kusty api GET /v2.1/chat/sessions --query customerId=$CUSTOMER_ID --all | jq -r .data.id > sessions.txt
```

**Step 2.** [`GET /v2.1/chat/sessions/{id}/messages`](https://developer.kustomer.com/api/chat/list-messages-for-a-chat-session.md). Page through each session's messages. Each has `body`, `direction` (`in` from the customer, `out` from your team or a bot) and `createdAt`. Pass `fields` to return only the attributes you need, and sort by `createdAt` yourself before writing the transcript.

```bash title="cURL"
curl "https://api.kustomerapp.com/v2.1/chat/sessions/$SESSION_ID/messages?fields=body,direction,createdAt&pageSize=100" \
  --header "Authorization: Bearer $KUSTOMER_API_KEY"
```

```bash title="kusty"
while read -r s; do
  kusty api GET /v2.1/chat/sessions/$s/messages --query fields=body,direction,createdAt --all \
    | jq -c --arg s "$s" '{session: $s, at: .data.attributes.createdAt, dir: .data.attributes.direction, body: .data.attributes.body}'
done < sessions.txt > transcripts.jsonl
```

### Example object

A real record from [`POST /v2.1/chat/sessions`](https://developer.kustomer.com/api/chat/create-a-chat-session.md):

```json
{
  "type": "chat_session",
  "id": "5679792fbc351e1d007a2993",
  "attributes": {
    "trackingId": "55d7998f2d3f9f8800099aef",
    "createdAt": "2026-07-15T13:30:00.000Z",
    "isInAssistantMode": true,
    "preview": "How can I help you today?"
  },
  "relationships": {
    "org": {
      "data": {
        "type": "org",
        "id": "55d7998f2d3f9f8800099aed"
      }
    },
    "customer": {
      "data": {
        "type": "customer",
        "id": "55d7998f2d3f9f8800099aee"
      }
    }
  },
  "links": {
    "self": "/c/v1/chat/sessions/5679792fbc351e1d007a2993"
  }
}
```

Fields:

- `id` (string): The conversation ID. A chat session is a conversation on the chat channel, so the same ID works as `session` when you post messages and with [`GET /v1/conversations/{id}`](https://developer.kustomer.com/api/core-resources/get-conversation-by-id).
- `attributes.trackingId` (string): The chat SDK's ID for the visitor's browser or device. When a session has one, any `trackingId` you send for it must match. Create a session without one and Kustomer reuses the tracking ID from the customer's latest chat session, if there is one.
- `attributes.createdAt` (string (date-time)): When the session was created. Sessions are listed by `lastMessageAt`, newest first, not by this.
- `attributes.isInAssistantMode` (boolean): `true` when the session is in assistant (bot) mode. Send `isInAssistantMode: true` on create to start it that way.
- `attributes.preview` (string): A preview of the latest message. When you create a session with `assistant`, it can already show the assistant's greeting.
- `relationships.customer` (object): The customer the chat is with. Every message, typing and CSAT call for this session must send this ID as `customerId`, or it fails with `403`.
- `links.self` (string): The chat SDK's customer-facing route, which needs a customer chat token rather than an API key. From your server, use [`GET /v2.1/chat/sessions`](https://developer.kustomer.com/api/chat/list-chat-sessions) or the conversations API instead.

### Relationships

- Points to **Customers** via `relationships.customer`: The customer a chat session is with. A customer can have many chat sessions; list them with `GET /v2.1/chat/sessions?customerId=…`.

### Permissions

Fewest roles for a read-only integration: `org.permission.conversation.read`, `org.permission.message.read`. For every endpoint: `org.permission.message.create`, `org.permission.conversation.create`, `org.permission.conversation.read`, `org.permission.conversation.update`, `org.permission.message.read`.

## Request body

Content type: `application/json` (required)

- `customerId` (string, required): Required. Kustomer customer ID for the new session.
- `trackingId` (string): Optional tracking record id stored on the session.
- `title` (string): Session title. (max length 256)
- `isInAssistantMode` (boolean): Start in assistant/bot mode when true.
- `brand` (string): Chat settings / widget brand id.
- `assistant` (string): Assistant public id. When set on this linked create path, Kustomer bootstraps greeting / opening messages and assistant run state during the create call (see operation description). Soft-fails on bootstrap errors; session create still returns 201.
- `custom` (object): Custom attributes. Keys must use Kustomer custom-attribute suffix rules (for example `orderNumStr`, `priorityStr`, `chatSourceTree`).
- `externalId` (string): (max length 256)
- `importedAt` (string<date-time>)
- `createdAt` (string<date-time>)
- `assignedUsers` (array<ObjectId>): (min items 1; max items 10; unique items)
- `assignedTeams` (array<ObjectId>): (min items 1; max items 10; unique items)
- `tags` (array<string>): (min items 1; max items 20; unique items)

Example:

```json
{
  "customerId": "55d7998f2d3f9f8800099aee",
  "title": "Order help",
  "isInAssistantMode": false
}
```

## Responses

### 201

Created. When `assistant` was provided, greeting messages and assistant run state may already have been bootstrapped; soft-fail bootstrap errors still yield this status.

- `data` (object)
  - `type` (string)
  - `id` (string): 24-character hexadecimal ObjectId (case-insensitive). (pattern ^[A-Fa-f\d]{24}$)
  - `attributes` (object)
  - `relationships` (object)
  - `links` (object)

```json
{
  "data": {
    "type": "chat_session",
    "id": "5679792fbc351e1d007a2993",
    "attributes": {
      "trackingId": "55d7998f2d3f9f8800099aef",
      "createdAt": "2026-07-15T13:30:00.000Z",
      "isInAssistantMode": true,
      "preview": "How can I help you today?"
    },
    "relationships": {
      "org": {
        "data": {
          "type": "org",
          "id": "55d7998f2d3f9f8800099aed"
        }
      },
      "customer": {
        "data": {
          "type": "customer",
          "id": "55d7998f2d3f9f8800099aee"
        }
      }
    },
    "links": {
      "self": "/c/v1/chat/sessions/5679792fbc351e1d007a2993"
    }
  }
}
```

### 400

Bad Request — invalid request parameters or body

### 401

Unauthorized — authentication required or invalid token

### 403

Forbidden — insufficient permissions or session/customer mismatch

## Example request

```bash
curl --request POST \
  --url 'https://api.kustomerapp.com/v2.1/chat/sessions' \
  --header "Authorization: Bearer $KUSTOMER_API_KEY" \
  --header 'Content-Type: application/json' \
  --data '{
  "customerId": "55d7998f2d3f9f8800099aee",
  "title": "Order help",
  "isInAssistantMode": false
}'
```
