Updated on October 25, 2026
To know which Click-to-WhatsApp ad generated a sale, read the referral object the Cloud API sends in the incoming message webhook: it carries the ad ID (source_id) and a unique click ID (ctwa_clid). Store this data with the contact and conversation, and when the sale happens, link the order to it. That lets you compute sales per ad in your own system and also send the sale back to Meta through the Conversions API.
This guide shows the payload, what each field means, how to build attribution in your database, how to check the free entry point window through the status webhook, and the cases where the data doesn’t arrive.
What a Click-to-WhatsApp ad is
Click-to-WhatsApp (CTWA) ads are Facebook and Instagram ads whose button opens a chat with the business on WhatsApp. The person taps the ad, WhatsApp opens with a pre-filled message and, when they send it, your integration receives that message through the webhook with the ad data attached.
The key point: Ads Manager shows how many conversations an ad started, but it doesn’t know what happened next. The sale closes in the chat, in your CRM or at your checkout. Your integration is what connects both ends.
The referral object in the webhook
According to the text messages webhook reference, when a message comes from a CTWA ad, it arrives with a referral object:
{
"object": "whatsapp_business_account",
"entry": [{
"id": "WABA_ID",
"changes": [{
"field": "messages",
"value": {
"messaging_product": "whatsapp",
"metadata": { "display_phone_number": "5511999990000", "phone_number_id": "PHONE_NUMBER_ID" },
"contacts": [{ "profile": { "name": "Ana" }, "wa_id": "5511988887777" }],
"messages": [{
"from": "5511988887777",
"id": "wamid.HBgN...",
"timestamp": "1761400000",
"type": "text",
"text": { "body": "Hi! I'd like to know more about the annual plan." },
"referral": {
"source_url": "https://fb.me/3cr4Wqqkv",
"source_id": "120226305854810726",
"source_type": "ad",
"headline": "Chat with us",
"body": "Annual plan with WhatsApp support",
"media_type": "image",
"image_url": "https://scontent.xx.fbcdn.net/...",
"ctwa_clid": "Aff-n8ZTODiE79d22KtAwQKj9e_mIEOOj27vDVwFjN80dp4...",
"welcome_message": { "text": "Hi there! How can we help?" }
}
}]
}
}]
}]
}
What each field means
| Field | What it is | Use in attribution |
|---|---|---|
source_id | Ad ID | Key to group sales by ad |
source_type | Source (ad for ads) | Filter what came from ads |
source_url | Ad URL | Reference to check on Facebook |
headline / body | Ad headline and primary text | Identify the creative without calling the Marketing API |
media_type | image or video | Compare formats |
image_url, video_url, thumbnail_url | Ad media (depending on type) | Show the creative in your CRM |
ctwa_clid | Click ID, unique per click | Send the conversion back to Meta |
welcome_message.text | Ad greeting text | Context for your support team |
Two notes from Meta’s documentation:
- The
ctwa_clidis omitted when the message comes from an ad in WhatsApp Status. You still get thesource_id. - The
ctwa_clidis a personal identifier, unique per click, according to the Conversions API for Business Messaging guide. Treat it as personal data, with the same protection as the customer’s phone number.
How to build attribution in your system
The logic is simple: store the click when it arrives, and link the sale to the click when it happens.
1. Store the referral as soon as the message arrives
Create a conversation source table:
CREATE TABLE conversation_source (
id BIGSERIAL PRIMARY KEY,
wa_id TEXT NOT NULL,
wamid TEXT NOT NULL UNIQUE,
ad_id TEXT,
ctwa_clid TEXT,
headline TEXT,
received_at TIMESTAMPTZ NOT NULL
);
And in the webhook handler:
export function recordSource(value, db) {
for (const msg of value.messages ?? []) {
const ref = msg.referral;
if (!ref || ref.source_type !== 'ad') continue;
db.insertSource({
wa_id: msg.from,
wamid: msg.id,
ad_id: ref.source_id,
ctwa_clid: ref.ctwa_clid ?? null, // missing for Status ads
headline: ref.headline ?? null,
received_at: new Date(Number(msg.timestamp) * 1000),
});
}
}
Use the wamid as a unique key: Meta may resend the same webhook, and the UNIQUE constraint prevents duplicate sources. To test with sample payloads before running ads, see how to test the webhook locally.
2. Link the sale to the last click
When the order is created (at checkout, in the CRM or by an agent), look up the latest source for that wa_id within a window you choose, for example 7 or 28 days:
SELECT ad_id, ctwa_clid
FROM conversation_source
WHERE wa_id = $1
AND received_at >= now() - interval '7 days'
ORDER BY received_at DESC
LIMIT 1;
Save ad_id and ctwa_clid on the order. This is your last-click model. If someone clicked two ads, the sale goes to the most recent one. Choose the window based on your sales cycle and keep the same rule when comparing ads.
If the sale happens at a checkout outside WhatsApp, include a contact identifier in the link you send in the chat (an order or session code), so the checkout can map back to the wa_id.
3. Put names on the IDs
The source_id is a number. To show “Black Friday campaign – Video 2” in your report, query the Marketing API with a token that has access to the ad account:
curl "https://graph.facebook.com/v26.0/120226305854810726?fields=name,adset{name},campaign{name}&access_token=TOKEN_WITH_ADS_READ"
Cache the result: ad names rarely change, and querying on every message is wasteful.
4. Report by ad
With ad_id on the order, the report is a query: conversations per ad (from the source table), orders and revenue per ad (from the orders table), and the conversion rate between them. Add each ad’s spend, exported from Ads Manager, and you get cost per sale.
The free entry point window
Conversations that start from a CTWA ad come with a cost benefit. According to Meta’s pricing page, if a user messages you from a CTWA ad on the Android or iOS app and the business replies within the customer service window, that reply isn’t charged and opens a free entry point (FEP) window. While it’s open, marketing, utility, authentication and service messages aren’t charged. Meta Business Agent messages are still charged.
Official sources disagree on the duration: the developer documentation says the window may stay open for up to 7 days, while the WhatsApp Business marketing site still says 72 hours. Instead of relying on a fixed number, check the status webhook. According to the status reference, messages sent inside the free window arrive with pricing.type set to free_entry_point. The same reference lists the referral_conversion category for free entry point conversations:
{
"id": "wamid.HBgN...",
"status": "delivered",
"recipient_id": "5511988887777",
"pricing": {
"pricing_model": "PMP",
"type": "free_entry_point",
"category": "referral_conversion"
}
}
Recording the pricing of every message lets you add to your report how much you saved on messages per ad. See the message status webhook.
Two practical consequences:
- Reply fast. The free window only opens if the business replies within the customer service window. An automatic welcome message solves that.
- Concentrate your follow-up. Reminders and offers sent inside the free window don’t pay the marketing rate. It’s a good moment for PIX and cart recovery (PIX is Brazil’s instant payment system).
When the referral doesn’t arrive
| Situation | What happens | What to do |
|---|---|---|
| Ad shown in WhatsApp Status | referral arrives, without ctwa_clid | Attribute by source_id |
| Customer saves the number and writes days later, outside the ad | Regular message, no referral | Count the conversation as organic in your system |
| Webhook down when the message arrived | Data is lost if you don’t process the retry | Return 200 quickly and process in a queue |
| Desktop or web app | The free window doesn’t apply (the pricing page only mentions Android and iOS) | Don’t count on free messages in these cases |
From reporting to optimization: the Conversions API
Knowing which ad sells is half the job. The other half is telling Meta, so ad delivery learns from sales and not just from started conversations. You do that by sending events (such as Purchase) with the ctwa_clid to the Conversions API. The step-by-step is in Meta Conversions API for WhatsApp.
How to do it with Vectalk
Vectalk forwards webhooks in Meta’s format, so the referral object reaches your URL exactly as described above, with no conversion. To get started:
- Connect your number at apps.vectalk.com.br/register.
- Register your webhook with
POST /api/webhooks(docs). You can register more than one URL per instance, for example one for support and another only for the attribution service. - Store
referralandpricingas shown in this article.
Frequently asked questions
Does the referral come with every message in the conversation?
Meta’s documentation describes the referral on the message the user sends from the ad. Store it as soon as it arrives and don’t rely on receiving it again in later messages.
What’s the difference between source_id and ctwa_clid?
The source_id is the ad ID: the same for everyone who clicked that ad. The ctwa_clid is the ID of one specific click by one person. For per-ad reporting, use source_id; to send the conversion back to Meta, use ctwa_clid.
Can I get the campaign and ad set?
Yes, by querying the ad ID in the Marketing API with a token that has access to the ad account. The WhatsApp webhook only carries the ad ID and the creative’s text.
Is the free window 72 hours or 7 days?
Meta’s developer documentation says up to 7 days; the WhatsApp Business marketing site still says 72 hours. Check message by message through pricing.type in the status webhook: free_entry_point means the message wasn’t charged because it was inside the window.
Does it work with Instagram ads?
Yes. Click-to-WhatsApp ads can run on Facebook and Instagram, and the message reaches WhatsApp with the referral in the same way.
Official sources checked on October 25, 2026: Text messages webhook (referral), Status webhook (pricing), Platform pricing, Pricing on the WhatsApp Business site, Conversions API for Business Messaging, CTWA welcome message sequences, Vectalk: webhooks.
