---
id: agencias/funil-de-whatsapp
industry: Marketing agencies
usage: common
effort: medium
grain: Service ticket (one occurrence); client x channel x campaign x day for the funnel
---

# WhatsApp funnel

From ad to conversation, to handling and to sale, with a cost per conversation that does not pass for cost per sale.

## Executive summary

**For:** Agencies that generate WhatsApp conversations for clients who close the sale in customer service.

**What it solves**

- Cost per conversation stays separate from cost per sale, and the client stops comparing the two.
- Each service ticket has a date, a line and an agent, without confusing contact with person.
- The sale that came from the conversation is tied to the campaign that generated it.

**Why it matters:** For many small and mid-sized businesses, the only conversion the platform sees is the conversation that was opened. A low cost per conversation convinces the client that media is doing well until they look at the cash. Without tying the conversation to service and sale, the agency optimizes for whoever clicks, not for whoever buys.

**Business questions it answers**

- How many conversations did each campaign generate and how much did each cost?
- How many of those conversations were handled and how many became a sale?
- Which WhatsApp line do customers reach most?
- What is the wait time until first response, by day?
- How many messages failed, and why?

**What you need**

- The client's Meta Ads and Google Ads connected, with conversation campaigns identified by objective
- The support tool or CRM connected, with the client's official list of numbers
- An identifier linking the conversation to the ad: ad ID, ctwa_clid or a WhatsApp tracking tool

**What you get**

- A service ticket table with date, line, department, agent and closing tag
- The ad, conversation, service and sale funnel by campaign
- An alert for campaigns pointing to an untracked number
- An AI agent that answers about WhatsApp without calling a conversation a sale

## Detailed

### The problem

The platform counts conversations started and calls them results. The support tool counts occurrences, contacts and messages, each with several dates, and fields like closed mean a technical close, not a sale. Without a key between conversation and ad, the funnel breaks in the middle and the agency does not know which campaign sells.

### Business rules

1. **A conversation started is top of funnel.** In campaigns with optimization_goal CONVERSATIONS, the result is onsite_conversion.messaging_conversation_started_7d, and cost per conversation is those campaigns' spend divided by conversations started. On Google, the conversion is the WhatsApp click action, identified by the conversion action name. Neither is a qualified lead or a sale. (Avoid: Reporting cost per conversation as cost per sale.)
2. **Ticket, contact and person are different.** In the support tool, each occurrence (report.id in RD Station Conversas) is a service ticket, and the same contact has several. Unique contacts = distinct count of the contact ID within the tool, and that is not a unique person across systems. Always say which of the three is being counted. (Avoid: Counting table rows as customers served.)
3. **One date for each question.** The ticket's time axis is created_at, in the client's time zone. opened_at is the latest service session and, in a real API, only covered the last month, which would stack months of history into one. started_at, finished_at and closed_at answer their own questions and do not replace the creation date. (Avoid: Using opened_at as the conversation date.)
4. **Closed is not a sale.** closed and closed_at record the technical close of the occurrence. They are not flow completion, success, conversion or sale. A sale comes from the CRM or a result field filled in by the team. The closing tag set by the agent (no reply, unqualified lead, sale) is worth more than any inference. (Avoid: Counting closed tickets as sales.)
5. **The WhatsApp line is a dimension.** Clients with several numbers (sales, toll-free, events, branches) have the line identified by a technical integration key. Map the key to the operational name in a client table and filter by line only when the question is about a line: the client's name alone means the whole company, even if one line has the same name. (Avoid: Filtering the line that carries the company name when asked about the company.)
6. **All traffic to the tracked number.** When the client uses a WhatsApp tracking tool, every paid campaign points to the number connected to it. A campaign that sends people to another number is a setup error and becomes an alert, because its conversations do not enter the funnel. (Avoid: Accepting campaigns that point to any number.)
7. **The bridge is the ad ID.** Link the conversation to the ad through the ad ID that the tool or CRM stores (or the ctwa_clid), joined with the latest version of each ad. With no ID, use the normalized phone number (55, area code and number) and the phone's first conversation in the period. A typed campaign name is not a key. (Avoid: Matching by campaign name.)
8. **Handling time by median.** The tool delivers times as text (18h 10m 00s): convert to seconds. Use the median, never the mean, because a conversation answered at dawn destroys the mean. Separate business hours (valid_business_period) from the rest. (Avoid: Mean wait time.)
9. **A failed message has a cause.** A message sent with status sent, delivered or read is a success; failed is an error. Separate errors of the team itself, errors from a closed 24-hour window and errors from the client's account. Template cost = messages sent x unit price of the template category; a category with no price stays null, not zero. (Avoid: Summing all errors into one number.)
10. **Phone numbers do not leave Nekt.** For an external dashboard or app, pseudonymize the phone number (hash plus the last 4 digits) before it leaves. The full number stays in the internal layer only, used as a key. (Avoid: Sending the full phone number to the dashboard.)

### WhatsApp funnel stages

Where each stage comes from and the field that counts it.

- **Meta · CONVERSATIONS objective** → Conversation started (`onsite_conversion.messaging_conversation_started_7d`)
- **Meta · advertiser reply** → First reply (`onsite_conversion.messaging_first_reply`)
- **Google · conversion action** → WhatsApp click (`conversion_action_name`)
- **Support tool** → Service ticket (`report.id`)
- **Support tool** → Contact (`distinct customer.id`)
- **Sale closing tag or CRM** → Sale (`won deal`)
- **closed = true** → Technical close, not a sale (`closed_at`)

### Definitions

| Metric | Definition |
|---|---|
| Conversations started | sum of messaging_conversation_started_7d in conversation objective campaigns, in the period |
| Cost per conversation | spend of conversation campaigns / conversations started, over the same period |
| Service tickets | distinct count of occurrence IDs created in the period (created_at in the client's time zone) |
| Unique contacts | distinct count of the contact ID in the support tool, in the period |
| Service coverage | contacts that received a reply from the team / contacts that wrote in, in the period |
| Conversation to sale conversion | sales linked to conversations in the period / conversations in the period linked to an ad |
| Wait time | median, in seconds, of the time until first response, split by business hours |
| Cost per sale | spend of conversation campaigns / sales linked to those campaigns in the period |

### Pitfalls

- **Cheap conversation, expensive sale.** Cost per conversation was below the agency's benchmark, and the client was losing money: many people opened a conversation and did not buy. How to avoid: Show cost per conversation and cost per sale side by side.
- **Months stacked into one.** opened_at carried each conversation's latest session and pushed the whole history into the current month, without any chart flagging an error. How to avoid: Time axis on created_at.
- **Closed counted as a sale.** The dashboard counted tickets with closed true as conversions, and the funnel showed almost 100% closing. How to avoid: Sales only from the CRM or the closing tag.
- **Number outside tracking.** One campaign pointed to another branch's WhatsApp, and its conversations vanished from the funnel. How to avoid: Alert for campaigns with a number outside the client's list.
- **Ticket with no owner.** The API's agents table came empty, and the dashboard showed tickets with no owner. How to avoid: Take the agent from the ticket itself (employee field).
- **The company became a line.** Questions about the company filtered only the line with the same name and showed a fraction of the contacts. How to avoid: Line map applied only to line questions.

### Acceptance test

- [ ] For one client and month, conversations started and cost per conversation match the Results column in Ads Manager for conversation campaigns (difference below 1%).
- [ ] The sum of daily tickets in the month equals the distinct count of occurrence IDs created in the month.
- [ ] No ticket counts as a sale without a won deal or a sale closing tag.
- [ ] Every conversation campaign points to a number on the client's list or shows up in the alert.
- [ ] No full phone number appears outside the internal layer.

### Context document

```text
WhatsApp funnel
- A conversation started (onsite_conversion.messaging_conversation_started_7d) is top of funnel. It is not a qualified lead or a sale.
- Cost per conversation = spend of conversation objective campaigns / conversations started.
- Service ticket = one occurrence in the tool (report.id). Contact = contact ID in the tool. Neither is a unique person across systems.
- Ticket date = created_at in the client's time zone. opened_at is the latest session; closed_at is a technical close.
- A sale comes from the CRM or the sale closing tag set by the team. closed = true is not a sale.
- Filter by WhatsApp line only when the question is about a line. The client's name alone is the whole company.
- Wait and handling time: median in seconds, split by business hours.
- Phone numbers leave Nekt only pseudonymized.
Per-client exceptions:
- <client>: tracked number <number>; a campaign pointing to another number is a setup error.
- <client>: lines <integration-1> = <sales>, <integration-2> = <toll-free>.
- <client>: sale = closing tag <sale> at the end of the ticket; time zone <time zone>.
```

### Variations

- **Orders through chat:** For delivery and retail: contacts, orders created and orders delivered by day and by channel.
- **Template cost:** Messages sent by template category times the category price, per broadcast campaign.
- **AI assistant in service:** Handoff and resolution without a human measured from the closing tags.
