> This page is for Voice AI Platform.

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.hams.ai/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.hams.ai/_mcp/server.

# Chat sessions and widget

> Start a chat with an agent, stream its replies, rate and end the session, and configure the embeddable widget.

A chat session is a conversation between one visitor and one `CHAT` agent. Your page or your
backend opens it, sends the visitor's messages one at a time, and reads the agent's reply as a
stream of events. When the visitor leaves, you end the session and, if you like, rate it.

The same five calls exist twice:

| From a browser or widget, with a public key          | From your systems, with a secret key              |
| ---------------------------------------------------- | ------------------------------------------------- |
| `POST /v1/ai-agents/{agent_id}/chat/init`            | `POST /ai-agents/{agent_id}/chat/init`            |
| `POST /v1/ai-agents/{agent_id}/chat/stream`          | `POST /ai-agents/{agent_id}/chat/stream`          |
| `GET /v1/ai-agents/{agent_id}/chat/{call_id}/status` | `GET /ai-agents/{agent_id}/chat/{call_id}/status` |
| `POST /v1/ai-agents/{agent_id}/chat/{call_id}/rate`  | `POST /ai-agents/{agent_id}/chat/{call_id}/rate`  |
| `POST /v1/ai-agents/{agent_id}/chat/{call_id}/end`   | `POST /ai-agents/{agent_id}/chat/{call_id}/end`   |

The public calls need the matching scope on the key (`chat:init`, `chat:stream`, `chat:status`,
`chat:rate`, `chat:end`) and a page whose origin is in the key's domains. The examples below use
the public form; swap the prefix and the header for the other.

#### Start the session

```bash
curl -X POST https://api.hams.ai/v1/ai-agents/$AGENT_ID/chat/init \
  -H "X-Public-Key: $HAMS_PUBLIC_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "dynamic_variables": { "customer_name": "Sara", "plan": "Pro" } }'
```

```json
{ "call_id": "…", "chat_history": [ { "role": "assistant", "content": "Hi Sara, how can I help with your Pro plan?" } ] }
```

Both fields are optional: `dynamic_variables` are strings the agent's prompts can use, and
`ip_address` lets a backend pass the visitor's address along. Keep the `call_id`; every other
call needs it. If the agent opens the conversation, its first message is already in
`chat_history`. When reCAPTCHA is enabled on the key, this call also needs a `g-token` header.

#### Send a message and stream the reply

```bash
curl -N -X POST https://api.hams.ai/v1/ai-agents/$AGENT_ID/chat/stream \
  -H "X-Public-Key: $HAMS_PUBLIC_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "call_id": "…", "content": "Can I change my billing date?" }'
```

The response is `application/x-ndjson`: one JSON object per line, each with an `event` and a
`data` object, until the reply is complete.

```json
{"event": "message_chunk", "data": {"content": "Yes, you can"}}
{"event": "message_chunk", "data": {"content": " move it from Settings > Billing."}}
{"event": "message_complete", "data": {"content": "Yes, you can move it from Settings > Billing."}}
```

Render `message_chunk` events as they arrive and replace them with `message_complete`, which
carries the whole message. Other events tell you what the agent is doing:

| Event                                      | Meaning                                                       |
| ------------------------------------------ | ------------------------------------------------------------- |
| `thinking`                                 | The agent is working on a reply                               |
| `tool`, `function_call`, `function_result` | The agent is calling a tool and got its result                |
| `node_transition`                          | The agent moved to the next step of its conversation flow     |
| `handoff`                                  | The conversation was handed over to another agent             |
| `client_action`                            | The agent asks your page to do something, such as open a link |
| `begin_message`                            | The opening message of a new turn                             |
| `end_conversation`                         | The agent decided the conversation is over                    |
| `error`                                    | Something failed; `data.message` says what                    |

One message at a time: sending another `stream` while a reply is in progress returns `410`
with `STREAM_BUSY`.

#### Check the session

`GET …/chat/{call_id}/status` returns `{ "status": "IN_PROGRESS" }`. A session is
`REGISTERED` after `init`, `IN_PROGRESS` once messages flow, `ENDED` after `end` or after the
agent's idle timeout, and `ERROR` if it failed. A page that reloads can use this to decide
whether to resume the session or start a new one.

#### Rate and end

```bash
curl -X POST https://api.hams.ai/v1/ai-agents/$AGENT_ID/chat/$CALL_ID/rate \
  -H "X-Public-Key: $HAMS_PUBLIC_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "call_rating": 5, "call_rating_comment": "Solved it in one message" }'

curl -X POST https://api.hams.ai/v1/ai-agents/$AGENT_ID/chat/$CALL_ID/end \
  -H "X-Public-Key: $HAMS_PUBLIC_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "end_reason": "user_ended" }'
```

A session can be rated once; the body of `end` is optional and `end_reason` defaults to
`user_ended`. Ended sessions appear in the agent's conversation log in the dashboard.

## Errors you will meet

| `error_code`                                  | When                                                                                  |
| --------------------------------------------- | ------------------------------------------------------------------------------------- |
| `AGENT_NOT_FOUND`, `AGENT_NOT_CHAT`           | No such agent in this entity, or it is a `VOICE` agent                                |
| `CALL_NOT_FOUND`, `NOT_A_CHAT_SESSION`        | The `call_id` does not belong to this agent, or is a phone call                       |
| `CHAT_ALREADY_ENDED`                          | Sending to, or ending, a session that is already `ENDED`                              |
| `STREAM_BUSY`                                 | A reply is still streaming for this session                                           |
| `ALREADY_RATED`                               | The session was rated before                                                          |
| `CHAT_STORAGE_DISABLED`                       | The agent's `data_storage` setting keeps no transcript, so history cannot be returned |
| `KEY_SCOPE_NOT_ALLOWED`, `DOMAIN_NOT_ALLOWED` | The public key may not make this call, or the page is not in its domains              |

Rate limits are 20 requests per minute per public key on each of `init`, `stream`, `end` and
`rate`, and 500 per minute per entity on `init` and `stream`; see
[API conventions](/platform/conventions#rate-limits).

## Widget configuration

The embeddable widget reads its look and behaviour from the agent's widget configuration, a
free-form JSON document you control:

```bash
curl -X PUT https://api.hams.ai/ai-agents/$AGENT_ID/widget-config \
  -H "X-Secret-Key: $HAMS_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "config": { "title": "Example Store support", "primaryColor": "#092652", "position": "bottom-right" } }'
```

`PUT` creates or replaces the configuration and needs the `WRITER` role; `GET
/ai-agents/{agent_id}/widget-config` returns it with `id`, `ai_agent_id`, `entity_id`, `created`,
`updated` and `presigned_urls` for any assets it references.

The widget itself fetches it from the browser with the public key:

```bash
curl https://api.hams.ai/v1/ai-agents/$AGENT_ID/widget-config \
  -H "X-Public-Key: $HAMS_PUBLIC_KEY" \
  -H 'If-None-Match: "…"'
```

The response carries an `ETag` and short-lived `Cache-Control`; send the tag back in
`If-None-Match` and you get `304` while nothing changed. The key needs the `widget-config:read`
scope.