# WhatsApp messaging

> Put a published text agent on a Twilio or Gupshup WhatsApp number: provider_account_info, the whatsapp.message.inbound trigger, userdata.

A WhatsApp number answered by a text agent takes three things: a sender
registered at your BSP (Twilio or Gupshup), a `whatsapp` integration holding its
credential, and a `whatsapp.message.inbound` [trigger](https://docs.talqing.com/integrations/triggers.md)
pointing at a published text agent. The same integration sends
[template batches](https://docs.talqing.com/whatsapp/creating-a-batch.md) and, on Twilio, takes
[WhatsApp calls](https://docs.talqing.com/whatsapp/calls.md).

Talqing does not register senders or create templates; do both at your BSP. Your
BSP and Meta bill you for WhatsApp traffic; Talqing charges its
[per-message platform fee](https://docs.talqing.com/get-started/pricing-and-credits.md) when the agent
answers.

## Set it up

**Step 1. Connect the number**

In **WhatsApp → Numbers**, press **Connect number**.

- **Twilio** — the Account SID and the account's **Auth Token** (not an API
  key: Twilio signs its webhooks with the Auth Token). **Find numbers** lists
  the account's senders (`POST /v1/whatsapp/twilio-senders`); only `ONLINE`
  senders can be picked. Each becomes its own integration.
- **Gupshup** — the app ID, the app name exactly as Gupshup shows it, an API
  key from the app's **Settings → API Keys**, and the WhatsApp number.

The connection is checked live before it is saved: Twilio must list the
sender as `ONLINE`, and Gupshup must accept the key for that app. The
credential is stored as a workspace [secret](https://docs.talqing.com/platform/secrets.md).

```bash
curl -X POST https://api.talqing.com/v1/integrations \
  -H "Authorization: Bearer $TALQING_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "display_name": "Sales WhatsApp",
    "provider": "whatsapp",
    "credentials_ref": "{{secrets.TWILIO_AUTH_TOKEN}}",
    "provider_account_info": {
      "bsp": "twilio",
      "account_sid": "AC0123456789abcdef0123456789abcdef",
      "sender_e164": "+14155550100"
    }
  }'
```

For Gupshup, `credentials_ref` is the API key and `provider_account_info` is
`{"bsp": "gupshup", "app_id": "…", "app_name": "AcmeSupport", "sender_e164": "+919876543210"}`.

One number is one integration; a second connection for the same number is
refused. The number cannot be changed later — connect the other number
instead. Rotate the credential with a `PATCH` of `credentials_ref`.

**Step 2. Choose who answers messages**

Pick a published text agent in the number's **Messages** cell, or create the
trigger over the API:

```bash
curl -X POST "https://api.talqing.com/v1/integrations/$INTEGRATION_ID/triggers" \
  -H "Authorization: Bearer $TALQING_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "trigger_type": "whatsapp.message.inbound",
    "agent_id": "9f1c0a3e-4b21-4f7a-9c6d-2a5b8e13d704",
    "enabled": true
  }'
```

Enabling registers Talqing's webhook with the BSP. On Twilio it replaces any
URL set on that sender. On Gupshup it appears in the app's **Webhooks** tab as
`talqing-<integration id>`; leave it there. Disabling the trigger, or
disabling or deleting the integration, removes it.

**Step 3. Message the number**

Send the number a WhatsApp message. On Twilio you see a typing indicator
(Gupshup has none), then the reply. The thread appears under
**Conversations** with surface `whatsapp`.

## Using a number from another carrier

Twilio registers any number you own as a WhatsApp sender, including one rented
from a carrier such as Plivo; the carrier keeps its phone calls and Twilio
carries WhatsApp. Meta verifies the number with a code by SMS or voice call, so
if the number already answers calls through a Talqing agent, forward it to a
phone you can answer while you register, then put it back (**Run setup again**
on the number in **Telephony → Phone Numbers**). A message sent before the new
sender is `ONLINE` fails with Twilio `63007`. On a number that does both, a
person is one contact whether they call it or message it on WhatsApp — see below.

## What the agent knows

The contact key is `phone:<your number>:<their number>` — the same contact their
phone calls and WhatsApp calls to that number use, with one `userdata`. Someone who
hides their number on WhatsApp is `whatsapp:<your number>:<their WhatsApp id>`.

| Variable | Value |
| --- | --- |
| `{{system_vars.human_phone_number}}` | Their number, E.164 (absent if they hide it) |
| `{{system_vars.agent_phone_number}}` | Your WhatsApp number |
| `{{system_vars.direction}}` | `inbound` — they wrote first; `outbound` on a chat an [instruction](https://docs.talqing.com/channels/posting.md) opened |
| `{{system_vars.channel}}` | `whatsapp_message` |
| `{{userdata.whatsapp.phone}}` | The customer's number, E.164 |
| `{{userdata.whatsapp.profile_name}}` | Their WhatsApp profile name, when WhatsApp sends one |

Their 24-hour WhatsApp window — when free-form messages are allowed — is on the
conversation's contact as `ref.whatsapp_window_expires_at`. Any message or call from
them refreshes it, answered or not.

Someone who first heard from you through a [batch](https://docs.talqing.com/whatsapp/creating-a-batch.md)
also carries that row's columns as `{{userdata.<column>}}`, and the template sits
in the thread as the agent's first message. The same goes for a template or
message you [posted](https://docs.talqing.com/channels/posting.md) with `userdata`: a chat their reply
starts begins from the `userdata` of everything posted since the thread's last
session, so even an agent on `context: none` knows what the send carried.

To message someone yourself — a template, text as the agent, a note for the agent,
or an instruction it acts on — see [posting into a conversation](https://docs.talqing.com/channels/posting.md).

## One chat until it ends

All of a person's messages join one [chat](https://docs.talqing.com/channels/text-conversations.md). It ends
when the agent runs [`end_call`](https://docs.talqing.com/tools/end-call.md), you call
`POST /v1/chats/{id}/end`, or 24 hours pass without a message from them
(`inactivity_timeout`); their next
message starts a new chat, in a new conversation unless the agent's
[`conversation.context`](https://docs.talqing.com/agents/conversation-memory.md) is `transcript`.
[Analysis](https://docs.talqing.com/observability/call-analysis.md) and `session.completed` happen when a
chat ends, so give the agent an `end_call` tool.

On an `inactivity_timeout` WhatsApp's 24-hour window has usually closed, so a
message the agent's `on_exit` hook sends is not delivered (it is recorded as
`failed`); the hook's tools and userdata changes still run.

- A reply to a template joins the template's conversation, whatever `context` says.
- Switching the **Messages** agent ends the chats it opened (`close_reason:
  "replaced"`) at once; the new agent answers their next message in a new chat.
  Turning **Messages** off ends every open chat (`messages_off`).
- A [post](https://docs.talqing.com/channels/posting.md#choosing-the-agent) or batch can choose the agent
  instead; that chat keeps running whatever **Messages** is switched to.
- A message to a number with no agent on **Messages**, and none chosen for them,
  is stored in the thread and starts no chat.
- `{{vars.*}}`, secrets, overrides and labels come from the post that chose the
  agent, or else from your [inbound webhook](https://docs.talqing.com/platform/inbound-webhook.md), asked once
  when a chat opens, for an agent that turns it on; otherwise the agent runs on its
  declared defaults.

## What is delivered

What reaches the agent:

| They send | The agent reads | On the item |
| --- | --- | --- |
| Text | The text | `text` |
| A quick-reply button tap | The button's text, as if typed | `content`: `{kind: "button", button: {text, payload}, replied_to}` |
| Photos, with or without a caption | The photos if the model has `vision: true`, otherwise a line asking them to describe it; then the caption | `attachments` — see [images](https://docs.talqing.com/agents/vision-and-images.md#whatsapp) |
| A PDF or Word (`.docx`) file | Its text, and a PDF's scanned pages as images when the model has `vision: true` | `attachments` — see [documents](https://docs.talqing.com/agents/vision-and-images.md#documents) |
| A voice note or audio file | What was said, when the agent has a [speech-to-text model](https://docs.talqing.com/channels/text-conversations.md#voice-notes); otherwise a line asking them to type it | `attachments`: `kind: "audio"`, with `transcript` once transcribed |
| A shared contact | The name, phone numbers, emails and organization on each card | `attachments`: `kind: "contact"`, with `contacts` and the `.vcf` |
| A video, spreadsheet or any other file | A line saying it arrived and that it cannot open it, then the caption | `attachments`: you can play a video, and download anything else |
| A file that could not be received (over 20 MB, or the download failed) | A line saying so, asking them to send it again | `attachments_not_kept` |

A button's `payload` is what you attached to it in the template; it is stored for your
systems and never shown to the agent. `replied_to` is the `provider_message_id` of the
template message the tap answers. Up to 4 files per message are kept. Buttons, files
and captions are read on Twilio senders; Gupshup reads plain text only and drops
everything else.

Every stored file also fires [`conversation.attachment.received`](https://docs.talqing.com/api-reference/webhook-events.md#conversation-attachment-received)
as soon as it is stored, with a link to download it.

Replies are text. One longer than 4,096 characters is split at the last line or word
break and sent in order.

WhatsApp lets a business send free-form messages only within 24 hours of the
customer's last message. A reply is always inside that window; starting a
conversation takes an approved template, which is what
[batches](https://docs.talqing.com/whatsapp/creating-a-batch.md) send.

## `reply_mode`

| Value | On WhatsApp |
| --- | --- |
| `public_reply` | The reply is sent. Default |
| `none` | The agent runs and its reply is recorded, but nothing is sent |
| `internal_note` | Not usable: WhatsApp has no private notes; the item settles `skipped` |

## When something fails

| What you see | Why |
| --- | --- |
| `400` on connect: `… is not a WhatsApp sender on Twilio account …` | The number is not a sender on that account |
| `400` on connect: `Twilio reports sender … as OFFLINE` | New senders go offline briefly after registration. Retry in a few minutes |
| `400` on connect: `Gupshup rejected the API key …` | Create a new key on the app's **Settings → API Keys** |
| Trigger `error`: `could not point the Twilio sender at Talqing: …` | Twilio refused the webhook update. Fix the cause and enable again |
| Trigger `error`: `could not register Talqing's webhook with Gupshup: …` | Gupshup refused the subscription. Fix the cause and enable again |
| A reply marked `failed` with `63016` | Outside the 24-hour window |
| Message marked with an error, chat closed as *Your inbound webhook did not answer* | The agent asks your [inbound webhook](https://docs.talqing.com/platform/inbound-webhook.md#refusals) and it failed; nothing was sent, and their next message asks again |

## Next

- [WhatsApp calls](https://docs.talqing.com/whatsapp/calls.md): Answer calls to a Twilio number with a voice agent.
- [Creating a batch](https://docs.talqing.com/whatsapp/creating-a-batch.md): Start conversations with an approved template.
