# Chat agents

## What it is

The agent that answers your calls answers typed chats too, with the same job,
knowledge, actions and backup brain. There are three ways in:

- **The chat API**, for your own web chat or app: start a conversation, send
  each message, and get the agent's reply in the response.
- **A Telegram bot of your own**: connect it once, and the agent answers
  everyone who writes to it.
- **A WhatsApp number or an Instagram professional account on your own Meta
  app**: connect it once, paste two values into the app, and the agent answers
  everyone who writes to it, within Meta's 24-hour window.

Only an agent with the listen, think, speak setup (`pipeline_mode: "cascade"`)
can chat, because its language model writes the replies. A realtime agent has
no text step and is refused.

## Price

Each AI reply costs **₹0.05 or $0.0005** from your wallet, in the wallet's
currency (price versions `inr_chat_reply_2026_11_06_v1` and
`usd_chat_reply_2026_11_06_v1`). Messages people send are free. A reply that
was refused, never sent or may not have arrived is never charged; a reply
Telegram or Meta accepted stays charged even if it is later reported
undelivered.

Your own AI account bills the tokens, as it does on a call; Dvaarik adds
nothing to them. On WhatsApp and Instagram, Meta's own charges, if any, are
billed to you by Meta, on your own Meta account; we add nothing to them either.
A reply holds its price on the wallet when it starts and is charged once, after
it was delivered: in the API's response, or once Telegram or Meta accepted its
last part. The Money page lists each one as a chat reply
(`debit_chat_inr` or `debit_chat_usd`).

## The chat API

Conversations take a project key with `voice:read` and `voice:write`, the same
scopes as calls. The console's twins live under
`/console/v2/projects/{project_id}/chats`.

### 1. Start a conversation

```bash
curl -sS -X POST https://api.developers.dvaarik.com/v2/chats \
  -H "X-Api-Key: $DVAARIK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "agent_id": "'"$AGENT_ID"'",
    "external_id": "customer-4821",
    "variables": { "company": "Northline Dental" }
  }'
```

`variables` fill the agent's job for the whole conversation. `external_id` is
your own id for the person (128 characters at most) and `metadata` is yours to
keep. The `201` is the conversation: keep its `id`.

### 2. Send a message and get the reply

```bash
curl -sS -X POST https://api.developers.dvaarik.com/v2/chats/$CHAT_ID/messages \
  -H "X-Api-Key: $DVAARIK_API_KEY" \
  -H "Idempotency-Key: msg-7f3a" \
  -H "Content-Type: application/json" \
  -d '{ "text": "Do you open on Sunday?" }'
```

The request waits for the reply (a minute at most) and returns both messages:

```json
{
  "message": { "role": "user", "status": "received", "text": "Do you open on Sunday?" },
  "reply": {
    "role": "agent",
    "status": "sent",
    "text": "Yes, from 10 in the morning to 2 in the afternoon.",
    "currency": "INR",
    "money_scale": 100,
    "price_minor": "5",
    "cost_minor": "5",
    "input_tokens": 812,
    "output_tokens": 19,
    "model": "<your model>",
    "tool_calls": []
  },
  "duplicate": false
}
```

Money is an integer string in the wallet's own unit: paise for rupees, nano-USD
for dollars (`"500000"` is $0.0005). Each action the agent ran on the way is in
`tool_calls`, with what it sent and what it got back.

Send an `Idempotency-Key`, unique per message. If the connection drops, send the
same message with the same key: you get the first reply with
`duplicate: true`, never a second reply and never a second charge.

A reply the agent could not write comes back `failed`, with a sentence in
`error` and a code in `error_code`, and costs nothing. Send one message at a
time per conversation: the next one after the reply arrives. A message is text,
1 to 4,096 characters.

### 3. Read conversations

- `GET /v2/chats?agent_id=…&channel_id=…&limit=25&offset=0`: conversations,
  the most recent message first; `next_offset` reads the next page.
- `GET /v2/chats/$CHAT_ID?limit=50`: one conversation with its newest messages,
  oldest first. `next_before`, sent back as `before`, reads further back.

### When a message is refused

A refusal stores nothing and charges nothing. Its `detail` is a sentence that
says what happened.

| Status | Why | What to do |
| --- | --- | --- |
| `402` | The wallet cannot pay for one reply | Top up, then send it again |
| `409` | Another reply in this conversation is still being written, or the agent cannot answer (switched off, deleted, its AI account not ready, a value missing) | Wait for the reply, or fix the agent |
| `429` | A reply limit is reached (see Limits), or the project's request limit | Wait `Retry-After` seconds |
| `503` | Too many chats on the server right now | Wait `Retry-After` seconds (2), then send it again with the same key |

### What a reply's status means

| `status` | Meaning |
| --- | --- |
| `received` | A message from the person |
| `pending` | The agent is writing the reply |
| `ready` | Written, about to be sent to the channel |
| `sending` | Being sent to the channel |
| `sent` | Delivered and charged |
| `failed` | Not sent; `error` says why. Never charged |
| `ambiguous` | We could not tell whether the channel received it (the request timed out after it was sent). Never sent again and never charged |

A WhatsApp or Instagram reply that missed Meta's 24-hour window is `failed`
with `error_code: "window_closed"`; see [The 24-hour window](#the-24-hour-window).

## Telegram

### Connect a bot

1. In Telegram, message **@BotFather**, send `/newbot`, and choose a name and a
   username that ends in `bot`.
2. Copy the token it gives you. It reads like `123456789:AA…`.
3. In the console, open **Chats** and press **Connect a Telegram bot**: give it
   a name, choose the agent and paste the token. Or use a key with
   `channel:write`:

```bash
curl -sS -X POST https://api.developers.dvaarik.com/v2/chat-channels \
  -H "X-Api-Key: $DVAARIK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "provider": "telegram",
    "name": "Support bot",
    "agent_id": "'"$AGENT_ID"'",
    "bot_token": "'"$TELEGRAM_BOT_TOKEN"'"
  }'
```

We check the token with Telegram and point the bot's webhook at us for you
(`webhook_url` in the response), so there is nothing to paste into Telegram. The
token is stored encrypted and never returned. A bot connects to one channel at
a time, and a project can have 20 channels: bots, WhatsApp numbers and
Instagram accounts together.

A bot's agent answers strangers, so it uses each variable's default: an agent
with a required variable that has no default is refused. The bot answers
private chats with people. Messages in groups and from other bots are recorded
and ignored. Photos and other attachments are named to the agent, never opened.

### Look after it

- `GET /v2/chat-channels` (`channel:read`): each bot's `status` (`active`,
  `error` or `disconnected`), `webhook_url`, `last_error` (Telegram's own words
  about its last failed delivery to us, or ours) and `pending_updates`.
- `POST /v2/chat-channels/{channel_id}/check`: ask Telegram now. It also
  notices when another app took the bot's webhook over.
- `PATCH /v2/chat-channels/{channel_id}`: rename it, answer with another agent,
  change its limits, or give the **same bot's** new token after *Revoke current
  token* in @BotFather (Rotate token in the console).
- `DELETE /v2/chat-channels/{channel_id}`: the bot stops answering at once, its
  token is forgotten and its webhook removed while it still points at us.

When Telegram stops accepting the token, the bot turns `error`: give it its new
token.

## WhatsApp

### What you need from Meta

1. **A Meta app** at developers.facebook.com, of the Business type, with the
   WhatsApp product added.
2. **Your number on it.** In *WhatsApp > API Setup*, add your business phone
   number. Copy its **Phone number ID** (not the number itself) and the
   **WhatsApp Business Account ID** shown there.
3. **A permanent access token.** In your business settings, add a system user,
   give it your app and the WhatsApp Business Account with full control, and
   generate its token with `whatsapp_business_messaging` (read and send the
   number's messages) and `whatsapp_business_management` (read the number and
   subscribe your app to it). The temporary token on *API Setup* stops working
   after a day.
4. **The app secret**, in *App settings > Basic*. It signs every message Meta
   sends us, so we know it really came from your app.

### Connect it

In the console, open **Chats** and press **Connect a WhatsApp number**. Or use a
key with `channel:write`:

```bash
curl -sS -X POST https://api.developers.dvaarik.com/v2/chat-channels \
  -H "X-Api-Key: $DVAARIK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "provider": "whatsapp",
    "name": "Front desk number",
    "agent_id": "'"$AGENT_ID"'",
    "phone_number_id": "'"$WA_PHONE_NUMBER_ID"'",
    "waba_id": "'"$WA_BUSINESS_ACCOUNT_ID"'",
    "access_token": "'"$WA_SYSTEM_USER_TOKEN"'",
    "app_secret": "'"$META_APP_SECRET"'"
  }'
```

We read the number with your token first: a token Meta refuses, or a number it
cannot read, is never stored. With `waba_id`, we also subscribe your app to the
account's messages for you (`meta.subscription` says how it went); without it,
make sure your app is subscribed yourself. The token and the secret are stored
encrypted and never returned. The `201` carries what to paste:

```json
{
  "provider": "whatsapp",
  "status": "active",
  "account_id": "106540352242922",
  "account_name": "Northline Dental",
  "webhook_url": "https://api.developers.dvaarik.com/chat/whatsapp/<channel id>/<guard>",
  "meta": {
    "verify_token": "<48 characters>",
    "webhook_object": "whatsapp_business_account",
    "webhook_fields": ["messages"],
    "waba_id": "102290129340398",
    "display_phone_number": "+91 98765 43210",
    "verified_name": "Northline Dental",
    "subscription": { "ok": true, "at": "2026-11-08T09:00:00Z", "error": null }
  }
}
```

### Paste the callback URL and verify token

In your Meta app, open *WhatsApp > Configuration*. Under **Webhook**, paste
`webhook_url` as the callback URL and `meta.verify_token` as the verify token,
press **Verify and save**, then subscribe to the `messages` field. The verify
token never changes, even when you give the channel a new token or secret.

A Meta app sends all its WhatsApp messages to one callback URL, so connect one
number per Meta app. Messages for another number of the same app are ignored and
noted on the channel. A message sent from a WhatsApp number that is itself
connected here as a channel (yours or anyone's) is ignored, so two agents never
answer each other in a loop. While the app is in Development mode, Meta may not send
us your customers' messages: switch it to **Live** before real people write
(Meta may ask for App Review of the two permissions first).

### What WhatsApp says after a reply

A sent reply carries `delivery_status` as Meta reports it: `sent`, `delivered`,
`read` or `failed`. It only moves forward. A reply is charged once Meta accepts
it, so a later `failed` is recorded on it and keeps its charge.

## Instagram

### What you need from Meta

1. **An Instagram professional account** (Business or Creator).
2. **A Meta app** of the Business type with Instagram added, set up with
   Instagram login (*API setup with Instagram login*).
3. **A long-lived access token.** On that page, add your account and generate
   its token with `instagram_business_basic` (read the account) and
   `instagram_business_manage_messages` (read and send its messages). It lasts
   60 days, and we renew it for you.
4. **The account ID and the app secret.** The account's ID is the `user_id` that
   `GET graph.instagram.com/me` returns for the token, shown beside it in the
   app. The app secret is in *App settings > Basic*; you may also give the
   Instagram app secret (`instagram_app_secret`). A message signed with either
   is accepted.

### Connect it

Press **Connect an Instagram account** on the Chats page, or:

```bash
curl -sS -X POST https://api.developers.dvaarik.com/v2/chat-channels \
  -H "X-Api-Key: $DVAARIK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "provider": "instagram",
    "name": "Shop Instagram",
    "agent_id": "'"$AGENT_ID"'",
    "instagram_account_id": "'"$IG_ACCOUNT_ID"'",
    "access_token": "'"$IG_ACCESS_TOKEN"'",
    "app_secret": "'"$META_APP_SECRET"'"
  }'
```

The token must belong to the account you name; a token of another account is
refused with a sentence that says which one it is. We subscribe your app to
the account's messages for you, every time (`meta.subscription`). Then, in your
Meta app's *API setup with Instagram login*, find **Configure webhooks**, paste
`webhook_url` and `meta.verify_token` (object `instagram`), press **Verify and
save** and subscribe to the `messages` field. One account per Meta app, and the
app must be **Live** for Meta to send messages from everyone. Instagram does not
tell us who else is a channel here, so between two Instagram agents only the
replies-an-hour limit stops a loop.

### The token renews itself

`meta.token_expires_at` says when the token runs out. We renew it well before:
from 20 days ahead, retrying every few hours. Meta renews a token only once it
is a day old, so right after connecting the date is unknown until the first
renewal. If Meta refuses the token, the channel turns `error`; if renewal still
fails a week before it runs out, `last_error` says so. Either way you get
`chat.channel.attention`: log in again and give the channel its new token.

## The 24-hour window

Meta lets a business answer freely only within 24 hours of the person's last
message. A WhatsApp or Instagram reply after that is not sent: it ends `failed`
with `error_code: "window_closed"` and costs nothing. We check before the agent
writes (so no tokens are spent) and again before each part is sent. The agent
answers again as soon as the person writes. Telegram and the chat API have no
such window.

## Look after a WhatsApp number or an Instagram account

- `GET /v2/chat-channels`: each channel's `status`, `last_error`, `meta` (what to
  paste and what Meta says about the account) and limits. Never a token or a
  secret.
- `POST /v2/chat-channels/{channel_id}/check`: read the account with the stored
  token now and subscribe your app again. A refused token turns the channel
  `error`, with Meta's reason in `last_error`.
- `PATCH /v2/chat-channels/{channel_id}`: rename it, change the agent or the
  limits, or give the **same** number's or account's new `access_token`,
  `app_secret`, `instagram_app_secret` or `waba_id`. Only what you send changes.
  A token of another number or account is refused: connect that one as a new
  channel. If another request changed the channel's credentials while yours was
  checked, you get a `409`: read the channel again and send yours again.
- `DELETE /v2/chat-channels/{channel_id}`: it stops answering at once and its
  token and secrets are erased. Nothing changes in your Meta app: point its
  callback URL elsewhere if you use it for other software.

| Refusal | Why |
| --- | --- |
| `422` | A field is missing or the wrong shape (the item names the field and never repeats what you sent), or Meta refused the token, cannot read the number, or the token belongs to another account; `detail` says which |
| `409` | That number or account is already connected, the project has its 20 channels, the channel is disconnected, or another request changed its credentials while yours was checked (read it again and retry) |

`last_error` also carries what we notice on delivery: a message whose
signature does not match the app secret saved here (give the channel that app's
secret), messages for another number or account of the same app, or a correctly
signed delivery we could not read (answered, so Meta stops sending it again, then
dropped).

## Limits

| Limit | Default | Set on | Most |
| --- | --- | --- | --- |
| Replies a minute | 30 | each channel | 300 |
| Replies a day | 2,000 | each channel | 50,000 |
| Replies an hour in one conversation | 60 | each channel | 300 |
| Messages a minute from one person | 20 | fixed | — |
| Replies of one project's channels written at the same time | 2 | fixed | — |

Chat API conversations share the defaults, per project. Only replies that were
written count. Past a limit, the API answers `429`; a channel writes nothing to
the person and sends you `chat.reply.failed`. Messages beyond 20 a minute from
one person are ignored as a flood. Messages that arrive close together (about a
second and a half apart) get one reply. A reply is 4,000 characters at most and
goes out in parts a second apart: Telegram and WhatsApp take 4,096 characters a
part (WhatsApp counts an emoji as two), Instagram 1,000 bytes (a reply in Hindi
or another non-Latin script takes several parts, never cut inside a letter).

## How long messages are kept

Each message is deleted on its own clock:

- after the agent's `retention_days` (90 days at most) when the agent has
  `store_transcript` on;
- **24 hours** after it arrived when `store_transcript` is off (the default),
  which is long enough to answer.

A conversation is deleted once it has no messages left. Everything else is on
[Your data](/docs/your-data).

## Events

Project webhooks subscribed to `chat.*` or `*` get four chat events.
`call.*` does not include them.

| Event | When |
| --- | --- |
| `chat.message.received` | A person's message arrived |
| `chat.reply.sent` | A reply was delivered and charged |
| `chat.reply.failed` | A reply was not sent, or may not have arrived (`ambiguous: true`). Never charged |
| `chat.channel.attention` | A channel needs you: its token was refused, or an Instagram token could not be renewed a week before it runs out |

The body is described under [Chat events](/docs/webhooks#chat-events).

## In the console

**Chats** starts with where your agents chat: a big button each for WhatsApp,
Instagram and Telegram. A WhatsApp or Instagram form lists what you need from
Meta first; once connected, its card shows the callback URL and verify token
with one-click copy, whether your app is subscribed, an Instagram token's life,
and Check, Edit, Change token or secret, and Disconnect. Telegram bots have
Check, Edit, Rotate token and Disconnect. Below them is every conversation,
read as a chat with each reply's status, what WhatsApp said after it was sent,
cost and tokens. **Try a chat** on an agent card types to the agent through the
console, exactly as your app would through the chat API.

---

Source: https://developers.dvaarik.com/docs/chat-agents · every page as one file: https://developers.dvaarik.com/docs.md
