A WhatsApp support agent on connic is a deployed agent with two Twilio Messaging connectors: an inbound connector that starts a run for every message a customer sends, and an outbound connector that sends the agent's answer back. This tutorial sets up both, keeps each customer's conversation in a persistent session, makes sure every reply reaches the person who wrote, and covers the cases where WhatsApp requires a template or Twilio doesn't take a message.
Prepare the sender
connic sends and receives WhatsApp messages through Twilio's US1 region. IE1 handles SMS text only, and AU1 doesn't support Messaging. Under Project Settings → Connections, create a Twilio Connection with region US1, the Account SID (it starts with AC), and the Auth Token of that region. The connector files below refer to it by its name, support-twilio, so the credentials never appear in code. The same Connection can also serve Twilio Voice connectors for phone calls.
For a first test, Twilio's WhatsApp Sandbox replaces a registered sender. Once the Sandbox is activated in Twilio, the test phone joins it by sending the join code to the Sandbox number, whatsapp:+14155238886. The Sandbox session expires three days after joining, and the phone has to join again. In production, the sender is a registered WhatsApp sender in the same Twilio account, and switching to it means changing sender in both connector files.
Telnyx Messaging connects WhatsApp the same way, with separate inbound and outbound connectors. This tutorial uses Twilio.
Connect an inbound and an outbound connector
The inbound connector receives messages and starts a run of each linked agent, here whatsapp-support. It answers Twilio with an empty response and never sends anything itself. Replies leave through a separate outbound connector. Both are declared in connectors/*.yaml and deploy with the agent. Phone numbers need quotes, because YAML reads a bare +14155238886 as an integer and the deploy fails:
type: twilio_messaging
mode: inbound
connection: support-twilio
config:
channel: whatsapp
sender: "whatsapp:+14155238886" # Twilio's WhatsApp Sandbox
agents:
- whatsapp-supportThe outbound connector uses the same Connection, channel, and sender:
type: twilio_messaging
mode: outbound
connection: support-twilio
config:
channel: whatsapp
sender: "whatsapp:+14155238886"
agents:
- name: whatsapp-support
outbound: { usage: automatic, source: [support-whatsapp] }usage: automatic sends the agent's final output when its run completes, and source limits that to runs the inbound file started. Without source, the connector would also send after runs from other triggers, which have no WhatsApp customer to answer. In the dashboard, the same setup is Automatic outbound connector with Only selected inputs and the inbound connector selected.
On a registered sender, connic sets the incoming-message webhook in Twilio when it creates the inbound connector. It replaces the URL that was there and writes the previous value to the connector's logs and the project's audit log. If a Messaging Service contains the sender, the service has to be set to Defer to sender's webhook first. Twilio offers no documented API for the Sandbox webhook, so that step is manual: copy the Incoming-message webhook from the connector's details drawer, paste it into the Sandbox settings under When a Message Comes in, select HTTP POST, and save.
What a run receives
Each incoming message starts one run per linked agent with this input, shortened here:
{
"message_sid": "SM00000000000000000000000000000001",
"channel": "whatsapp",
"from": "whatsapp:+14155550124",
"to": "whatsapp:+14155238886",
"text": "Can I still return the blender I bought three weeks ago?",
"conversation_id": "twilio_messaging:9e0f77d9-2fd8-5a8e-8d47-59fbf5b8a091",
"raw": {
"MessageSid": "SM00000000000000000000000000000001",
"From": "whatsapp:+14155550124",
"To": "whatsapp:+14155238886",
"NumMedia": "0"
}
}from is the customer and to the sender, both with the whatsapp: prefix. message_sid is Twilio's ID for the message; if Twilio sends the same message twice, no second run starts. raw holds every field Twilio sent.
Customers often send a photo instead of a description. connic downloads incoming media into files, up to 50 files and 32 MiB per message, and lists every file it had to leave out in file_issues, which the agent should check as well. The model has to understand what customers send: connic/gemini-3.8-flash understands photos, PDFs and voice notes, so the agent answers a voice note like a typed message.
Keep each customer's conversation
Every message starts a separate run, and without a session the agent sees only the message in front of it. conversation_id stays the same for each combination of inbound connector, Twilio account, region, channel, sender, and customer, so it identifies one customer's conversation with one number. As the key of a persistent session, it gives every customer their own history:
version: "1.0"
name: whatsapp-support
type: llm
model: connic/gemini-3.8-flash
description: "Answers customer questions on WhatsApp"
system_prompt: |
You answer customer questions for the Acme store on WhatsApp.
Returns: within 30 days of delivery, started under Orders in the
customer's account; the return label arrives by email.
Keep replies short, answer in the customer's language,
and return the reply as plain text.
max_concurrent_runs: 10
session:
key: input.conversation_id
ttl: 604800 # history expires after 7 days without a message
concurrency:
key: conversation_id
on_conflict: queueSessions are off until the agent file sets session, and the change takes effect with the next deploy. ttl ends a history after seven days without a message, so the customer's next message starts fresh. Without ttl, history never expires. The same two messages, with and without the session key:
Customer: Can I still return the blender I bought three weeks ago?
Agent: Yes, returns are possible within 30 days of delivery. Do you want to know how?
- Two hours later
Customer: Yes, please
Agent: Open Orders in your account, choose the blender, and select Return. The return label arrives by email.
Customer: Can I still return the blender I bought three weeks ago?
Agent: Yes, returns are possible within 30 days of delivery. Do you want to know how?
- Two hours later
Customer: Yes, please
Agent: Happy to help. What would you like to do?
Without the session, the second run has no idea what the customer agreed to. Photos in a long conversation would reach the model again with every message; files: false in the session block replaces earlier files with a short note and still passes the files of the current message.
Messages sent in quick succession
WhatsApp users often split one thought across several messages. Each one starts a run seconds after the last. Runs that share a session don't wait for each other: when two overlap, the run that writes its history second fails, and that message goes unanswered. The concurrency block prevents this. With key: conversation_id and on_conflict: queue, the run for a second message from the same customer waits until the first run has finished, then starts with that exchange in its history. Each message still gets its own reply, and the runs handle the messages in the order they arrived. on_conflict: coalesce doesn't fit here because it cancels a waiting run when a newer message arrives, so the cancelled message never reaches the history.
The concurrency rule works per conversation. max_concurrent_runs limits the agent as a whole and defaults to 1, which would make every customer wait for every other one. The example raises it to 10, which needs the developer plan or higher. Basic allows 1, and a deploy above the plan's cap fails. Runs that wait for their conversation don't take one of those slots.
Send the reply to the right number
The outbound connector needs a recipient for every message and takes the first one it finds:
toin the agent's output, when the output is a JSON object.- The outbound connector's Default recipient,
toin itsconfig. - The customer who wrote, for a run that an inbound Twilio Messaging connector started. Both connectors must use the same Connection, channel, and sender.
The third rule covers the normal case, which is why the example sets no Default recipient. A Default recipient comes first, so one left over from testing, such as a team member's phone, receives every customer's reply.
The third rule also requires an exact match. If the outbound connector uses a different sender, for example because only the inbound file was switched from the Sandbox to the registered sender, the reply has no recipient, and the connector run fails with "Set a recipient, provide 'to' in the output, or reply through the same Twilio connection and sender".
Message customers outside the 24-hour window
WhatsApp allows free-form messages only within 24 hours of the customer's last message. After that, a business can only send a template that WhatsApp has approved, such as a shipping notice with placeholders for the order number and the delivery day. The template is created and submitted for approval in Twilio, which gives it a Content SID starting with HX; Twilio's template guide covers the steps. An outbound message names the template and fills its variables:
{
"to": "whatsapp:+14155550124",
"content_sid": "HX00000000000000000000000000000000",
"content_variables": { "1": "12345", "2": "Friday" }
}Since the customer hasn't written for a day, a message like this usually isn't a reply and sets to itself. The agent passes it as its JSON output on an automatic link, or as the payload when it calls the connector as a tool. Variable values must be strings, and a message with content_sid can't also carry text or media_urls. Custom templates need a registered sender; the Sandbox only offers Twilio's pre-approved test templates.
The connector can set content_sid and content_variables as defaults, which the payload overrides. On the reply connector, a default template turns every reply into that template. Plain text from the agent is then ignored, and media URLs are rejected. A second outbound connector for template messages leaves the reply connector free for normal answers.
When a message doesn't go out
Every send is a run of the outbound connector. A completed run means Twilio accepted the message, not that it reached the phone. Its output holds the Message SID and the status Twilio returned. Twilio's Messaging Logs show the delivery status and later channel errors for that SID. A failed run falls into one of these cases:
| Case | What connic does | Next step |
|---|---|---|
connic can't connect to Twilio, or Twilio answers 429 | Tries up to 3 times in total, after a 429 waiting as long as Twilio's Retry-After asks, up to five minutes | None, unless the last attempt fails too |
| Twilio rejects the message | Fails the run with the HTTP status and Twilio's error code, without a retry | Check the sender, recipient, and template in Twilio |
The outcome is unknown: the connection fails during the request, or Twilio answers with a 5xx status | Fails the run without a retry, because Twilio may already have accepted the message | Look for the message in Twilio's Messaging Logs before resending |
The message is invalid: no recipient, text over 1,600 characters, more than one media URL, or a content_sid in the output together with text | Fails the run before contacting Twilio | Fix the agent's output or the connector |
The unknown case has no automatic retry because Twilio documents no way to recognize a repeated send request, so a retry could reach the customer twice. If the agent's own run fails, nothing is sent and the customer gets no answer; the run's trace shows why.
WhatsApp also limits media. A message takes one media URL, and WhatsApp drops text sent along with audio, video, documents, or other non-image media. Text that belongs with a PDF has to go out as a separate message, for example through a second outbound connector that the agent calls as a tool. The Twilio Messaging reference lists every field, limit, and status code.
Answer your customers on WhatsApp
Deploy an agent on connic, connect your Twilio sender, and answer every customer with their earlier messages in context.