Updated on October 29, 2026
To hand off a WhatsApp conversation from a bot to a human, your system changes the conversation state from “bot” to “waiting for human”, stops generating automated replies, lets the customer know, and gives the agent a summary with the data already collected. The handoff should happen when the customer asks for it, when the topic is sensitive, when the bot fails twice, or when the customer is a priority. The WhatsApp Business policy allows automation within the 24-hour window, but requires prompt, clear and direct escalation paths.
The official WhatsApp Business API has no “transfer” button: your software does the handoff. This guide covers triggers, what to pass to the agent, how to pause and resume the bot, SLA, and pseudocode.
Why handoff is mandatory, not optional
The WhatsApp Business Messaging Policy says a business can use automation when replying within the 24-hour window, as long as it keeps prompt, clear and direct escalation paths. The list of accepted options includes:
- transfer to a human agent within the same conversation;
- a phone number;
- an email address;
- support on the business’s website;
- a visit to a store or branch;
- a support form.
An in-chat transfer gives the best experience; the other options work as a fallback outside business hours.
AI bots have one more reason: Meta’s terms restrict general-purpose AI assistants on the platform, and a customer service bot with a narrow scope and a path to a human is clearly on the permitted side. The details are in Meta’s AI policy for chatbots.
The four handoff triggers
| Trigger | How to detect it | Example |
|---|---|---|
| Explicit request | Keywords, a “Talk to an agent” button, or the LLM classifies the intent | “I want to talk to a person” |
| Sensitive intent | A list of topics the business decides to handle only with people | cancellation, complaint, disputed charge, health data |
| Repeated failure | A counter of unsuccessful replies in the same conversation | the bot didn’t understand twice in a row |
| Priority customer | A CRM lookup by WhatsApp number | corporate contract customer, order above a certain amount |
Explicit request
Three layers help:
- A button always at hand. Include “Talk to an agent” as an option in the bot’s interactive messages. See interactive messages.
- Keywords. “agent”, “human”, “person”, “manager”. Simple, fast and with no LLM cost.
- Intent classification. If the bot uses an LLM, ask it to flag the
talk_to_humanintent when the customer asks in other words.
Sensitive intent
Define the list with the customer service and legal teams, not with the tech team. Cancellation, disputed charges, formal complaints and any conversation involving sensitive data (health, for example) usually make the list. Brazil’s data protection law, the LGPD, treats health data as sensitive personal data (art. 5, II, of Law 13,709/2018); this article does not replace advice from a lawyer about your case.
Repeated failure
Count failures per conversation (“I didn’t understand”, empty search, customer repeating the question). Two in a row is a good starting point for handing off; reset the counter when the bot gets it right.
Priority customer
The webhook carries the customer’s number in messages[].from. With it, your system checks the CRM before the bot replies. If the customer has an account manager or a high-value order in progress, the conversation can go straight to the human queue, or the bot does only the triage and hands off.
Want to try it while you read? Create a Vectalk account and send your first message through the official API. Create free account →
The conversation state machine
The core of the handoff is a state field per conversation, stored in the database by the customer’s wa_id. Every event that arrives through the webhook passes through it before any reply.
handoff trigger
┌──────┐ ───────────────────────────▶ ┌────────────────────┐
│ bot │ │ waiting_human │
└──────┘ ◀──── timeout, no agent ────── └────────────────────┘
▲ (bot notifies and offers │
│ another channel) │ agent takes over
│ ▼
│ ┌──────────────┐
└──────── agent returns it ───────── │ human │
or conversation idle └──────────────┘
| State | Who replies | What the bot does | Exits when |
|---|---|---|---|
bot | Bot | Replies normally | A trigger fires |
waiting_human | No one automatically | Notifies once and stays silent | An agent takes over, or the maximum wait time runs out |
human | Agent | Nothing; only logs the messages | The agent returns it, closes it, or the conversation goes idle |
Two common mistakes:
- The bot talking over the agent. This happens when the state lives only in process memory or when two workers process the same conversation. Store the state in the database and process each conversation’s messages in order.
- A conversation stuck in
humanforever. The customer comes back a week later and nobody answers, because the bot thinks someone is handling it. Use an inactivity timeout to return it to the bot.
What to pass to the agent
The agent shouldn’t have to read 40 messages to understand the case. Along with the handoff, deliver a card with:
| Item | Where it comes from |
|---|---|
| Name and number | contacts[].profile.name and messages[].from in the webhook |
| Handoff reason | The trigger that fired (request, sensitive, failure, priority) |
| 2 or 3 line summary | Generated by the LLM from the history, or assembled from the flow’s answers |
| Data already collected | Order number, partial CPF (Brazilian taxpayer ID), email, product |
| Source | Ad, website, QR code; the webhook carries the referral object when the conversation starts from a Click-to-WhatsApp ad |
| Time of the customer’s last message | messages[].timestamp, to know how much of the 24-hour window is left |
| Link to the full history | Your database or CRM |
An example payload your system can publish to the support queue or the CRM:
{
"conversation_id": "5511988887777",
"reason": "sensitive_intent:cancellation",
"summary": "Customer wants to cancel the annual subscription. Already provided email and order number. Asked about a prorated refund.",
"data": { "order": "48213", "email": "customer@example.com" },
"source": { "type": "ad", "source_id": "120226305854810726" },
"last_customer_message": "2026-10-29T14:02:11-03:00",
"window_expires_at": "2026-10-30T14:02:11-03:00",
"priority": "normal"
}
The LLM summary can be wrong: show the full history right below it.
How to notify the customer
The transition message needs to say three things: that a person will take over, how long it should take, and what to do if it’s outside business hours.
- During business hours: “I’m bringing in someone from our team. The average wait right now is a few minutes, and you don’t need to repeat anything.”
- Outside business hours: “Our team is available Monday to Friday, 8am to 6pm. I’ve logged your case and someone will reply here first thing. If it’s urgent, call (11) 0000-0000.”
Send the notice once. If the customer writes again while waiting, the bot doesn’t reply to every message; at most, it repeats the notice after an interval. Welcome and away messages based on business hours are covered in welcome and away messages on the API.
Watch the 24-hour window
Free-form text can only be sent up to 24 hours after the customer’s last message. If the human queue is slow and the window closes, the agent can no longer reply with free-form text: they need an approved template to reopen contact. That’s why the agent’s card shows when the window expires, and the queue should be sorted with that deadline in mind. The rules are in the WhatsApp 24-hour window.
How to return to the bot
The return to the bot can happen in three ways:
- The agent returns it. A “Return to bot” button in the support tool changes the state to
bot. The bot shouldn’t send a message at that moment; it only resumes replying on the customer’s next message. - The agent closes the case. When the case is closed, the state goes back to
botand the support history is recorded for the next conversation. - Inactivity. If the conversation is in
humanand nobody writes for a set period (for example, a few hours), the system returns it to the bot.
When it comes back, include a note in the LLM context such as “case handled by an agent on such date”, so the bot doesn’t reopen the topic.
SLA: what to measure
Measure the handoff with data your own system already records:
| Metric | How to calculate it |
|---|---|
| Handoff rate | Handed-off conversations ÷ conversations handled by the bot |
| Handoffs by trigger | Count by reason (request, sensitive, failure, priority) |
| Time until an agent takes over | Time it entered human − time it entered waiting_human |
| Windows lost in the queue | Conversations where the 24-hour window closed before the agent replied |
| Returns to the bot | Conversations that went back to bot and were resolved without another handoff |
Many handoffs due to “failure” mean the bot is missing content; many due to “explicit request” right on the first message suggest the bot doesn’t introduce itself well. General support metrics (first response time, average handling time, CSAT) are covered in WhatsApp customer service metrics.
Router pseudocode
The router runs for every incoming message, after deduplication by the wamid ID.
def route(msg, conversation):
conversation.last_customer_msg = msg.timestamp
log_to_history(conversation, msg)
if conversation.state == "human":
notify_agent(conversation, msg) # the bot stays silent
return
if conversation.state == "waiting_human":
if max_wait_exceeded(conversation):
send_text(msg.from_, OTHER_CHANNEL_NOTICE)
conversation.state = "bot"
return # don't reply to every message
# state == "bot"
reason = detect_trigger(msg, conversation) # request, sensitive, failure, priority
if reason:
return hand_off(conversation, reason)
reply = bot_reply(conversation)
if reply.failed:
conversation.failures += 1
if conversation.failures >= 2:
return hand_off(conversation, "repeated_failure")
else:
conversation.failures = 0
send_text(msg.from_, reply.text)
def hand_off(conversation, reason):
conversation.state = "waiting_human"
conversation.queued_at = now()
send_text(conversation.wa_id, handoff_notice(within_business_hours()))
publish_to_human_queue({
"conversation_id": conversation.wa_id,
"reason": reason,
"summary": summarize(conversation.history),
"window_expires_at": conversation.last_customer_msg + hours(24),
})
def agent_took_over(conversation, agent_id):
conversation.state = "human"
conversation.agent = agent_id
record_metric("time_to_take_over", now() - conversation.queued_at)
def return_to_bot(conversation):
conversation.state = "bot"
conversation.failures = 0
add_context_note(conversation, "human support completed")
send_text calls the API with Meta’s standard payload (type: "text"); the queue and notifications live in your tool (tickets, CRM, your own screen).
What if the agent replies from their phone?
With coexistence, the same number lives in the WhatsApp Business app and on the API. Messages the agent sends from the app reach your system as echo events. The router can use that echo to switch the conversation to human automatically: if a person replied from the phone, the bot steps aside. What syncs and what doesn’t is covered in coexistence: what syncs.
How to do the handoff with Vectalk
Vectalk provides the official API: you receive webhooks in Meta’s format and send through the POST https://apis.vectalk.com.br/api/messages endpoint, with the same payload. The Vectalk dashboard has no support inbox and no built-in bot. The router, the conversation state and the agent screen live in your system or in the tool you connect to the API (a CRM, a customer service platform, an n8n flow).
In practice, the setup looks like this:
- Register your router’s URL as the instance webhook (webhooks documentation). You can register more than one URL; all of them receive the same event.
- The router decides between bot and human and sends replies through the API.
- The support tool reads and replies through the same calls, or the team uses the app on their phone with coexistence.
If several agents will use the same number, see multiple agents on the same number. To get started, create your account at apps.vectalk.com.br/register and read the guide to the official WhatsApp Business API.
Frequently asked questions
Does the WhatsApp Business API have a native human handoff feature?
No. The Cloud API sends and receives messages; your system decides whether the reply comes from the bot or from a person. The handoff is a state change in your database, not an API call.
What happens if nobody takes over the conversation?
Set a maximum time in the queue. When it runs out, the bot tells the customer, offers another channel (phone, email, form) and logs the case for follow-up. If the 24-hour window closes, follow-up can only be done with a template.
Are the agent’s messages charged like the bot’s?
Yes. For Meta, a free-form text reply within the window is a service message, whether written by a person or by a third-party AI. Since October 1, 2026, each number gets 1,000 free service messages per month and pays from the 1,001st on; for recipients in Brazil, the rate is R$ 0.0350 (Brazilian reais) per delivered message.
Can the bot suggest replies to the agent?
Yes. In this model, the LLM generates a draft on the agent’s screen, and the agent reviews and sends it. It’s a way to keep a human in control on sensitive topics without losing speed.
Official sources consulted on October 29, 2026: WhatsApp Business Messaging Policy (updated on September 23, 2026), Meta Terms for WhatsApp Business Platform, Customer service window, Platform pricing, Text message webhook and referral object, Webhooks (Vectalk), Law 13,709/2018 (LGPD, Brazil’s data protection law).
Put your business on the official WhatsApp
Create your account, connect your number to the official API (you can keep the app with coexistence) and start serving customers and sending messages.
