# List chat sessions

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

Source: https://developer.kustomer.com/api/chat/list-chat-sessions

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

## Authorization

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

List chat sessions for a customer and/or tracking id using an API token.

Identity is supplied via query parameters (`customerId`, `trackingId`),
not via a `/c/` tracking JWT. At least one of `customerId` or `trackingId`
is required (**400** if neither is provided). When both are present,
sessions matching either identity are returned (SDK-style `$or` union).

Optional `active` and `brandId` match Chat SDK `GET /c/v1/chat/sessions`
filter semantics. Results are sorted by `lastMessageAt` descending and
paginated.

This is distinct from the private `GET /v1/customers/{customerId}/chat/sessions`
route (customer path only; not documented here).

## 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`.

## Query parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `customerId` | string | no | Kustomer customer ID. Required if `trackingId` is omitted. |
| `trackingId` | string | no | Chat tracking ID. Required if `customerId` is omitted. |
| `active` | boolean | no | When set, only return sessions whose lock/deleted state matches (SDK parity). Active sessions must also have `lastMessageAt`. |
| `brandId` | string | no | Filter sessions by brand. |
| `page` | integer | no | Page number for pagination. |
| `pageSize` | integer | no | Page size for pagination. |

## Responses

### 200

Paginated list of chat sessions.

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

```json
{
  "data": [
    {
      "type": "chat_session",
      "id": "5679792fbc351e1d007a2993",
      "attributes": {
        "trackingId": "55d7998f2d3f9f8800099aef",
        "createdAt": "2026-07-15T13:30:00.000Z"
      },
      "relationships": {
        "org": {
          "data": {
            "type": "org",
            "id": "55d7998f2d3f9f8800099aed"
          }
        },
        "customer": {
          "data": {
            "type": "customer",
            "id": "55d7998f2d3f9f8800099aee"
          }
        }
      },
      "links": {
        "self": "/c/v1/chat/sessions/5679792fbc351e1d007a2993"
      }
    }
  ],
  "meta": {
    "page": 1,
    "pageSize": 100
  }
}
```

### 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 GET \
  --url 'https://api.kustomerapp.com/v2.1/chat/sessions' \
  --header "Authorization: Bearer $KUSTOMER_API_KEY"
```
