Updated on October 29, 2026
To measure clicks and sales from a WhatsApp template in GA4, use a URL button with a dynamic suffix: the approved template has a fixed address ending in {{1}}, and on each send you pass a value that identifies the campaign (and, if you want, the individual send). That value becomes UTMs (utm_source=whatsapp, utm_medium, utm_campaign), either directly in the URL or through a redirect on your own domain, and GA4 attributes the session and the purchase to the campaign. On the WhatsApp side, you match this with the status webhook and Meta’s template analytics to get the full funnel: delivered, read, clicked and sold.
This guide covers Meta’s rules for URLs in templates, two ways to set up UTMs, the send payload and a report template.
Meta’s rules for the URL button
According to the template components documentation:
| Rule | Limit |
|---|---|
| URL buttons per template | Up to 2 |
| Button text | Up to 25 characters |
| URL length | Up to 2,000 characters |
| Variables in the URL | At most 1, always at the end of the address |
| Sample value | Required when the URL has a variable |
| Special characters | Must be percent-encoded (e.g., a space becomes %20); unencoded characters can make the send fail |
The practical consequence matters: you can’t put the variable in the middle of the URL or have two variables (one for the campaign and one for the contact). All tracking has to fit in a single suffix at the end.
Two ways to pass UTMs
Option 1: fixed UTMs in the URL, variable in the last parameter
The template is created with the campaign’s UTMs, and the variable fills only the last parameter:
https://www.store.com/offers?utm_source=whatsapp&utm_medium=template&utm_campaign=bf2026&utm_content={{1}}
On send, {{1}} gets something like sneaker_offer_a. Pros: simple, no server needed. Cons: utm_campaign is locked into the template, so each new campaign requires a new template (and a new approval); and the value you pass must be URL-encoded.
Option 2: redirect on your own domain (recommended)
The template has a short URL on your own domain:
https://www.store.com/w/{{1}}
On send, {{1}} gets a short code, such as k7Qm2x. Your server stores what that code means (campaign, template, destination, send ID) and replies with a 302 redirect to the final URL with all the UTMs. Pros:
- one template serves many campaigns, with no new approval;
- you can change the destination after sending, if the page changes;
- your server logs the click instantly, with the send’s code;
- the customer sees your company’s domain, not an unfamiliar shortener.
Meta’s documentation doesn’t address third-party URL shorteners. Using your own domain avoids the question and makes it clear to the customer where they’re going.
A minimal redirector in Node.js (Express):
import express from 'express';
const app = express();
app.get('/w/:code', async (req, res) => {
const link = await db.links.findOne({ code: req.params.code });
if (!link) return res.redirect(302, 'https://www.store.com/');
await db.clicks.insert({ code: link.code, wamid: link.wamid, at: new Date() });
const url = new URL(link.destination);
url.searchParams.set('utm_source', 'whatsapp');
url.searchParams.set('utm_medium', 'template');
url.searchParams.set('utm_campaign', link.campaign);
url.searchParams.set('utm_content', link.template);
url.searchParams.set('utm_id', link.campaign_id);
res.redirect(302, url.toString());
});
Want to try it while you read? Create a Vectalk account and send your first message through the official API. Create free account →
How to build UTMs for GA4
GA4 recommends always setting utm_source, utm_medium and utm_campaign, and also suggests utm_id (Google Analytics help on UTMs). Values are case-sensitive: whatsapp and WhatsApp become two different sources. Pick a convention and use lowercase.
| Parameter | Suggested value | Purpose |
|---|---|---|
utm_source | whatsapp | Traffic source |
utm_medium | template (marketing) or utility | Separate campaigns from transactional notices |
utm_campaign | bf2026, winback_oct | Campaign name |
utm_content | template name or test variant | Compare copy and buttons |
utm_id | campaign ID in your system | Join with cost data |
About channel groupings: in GA4’s default channel group, the channel rules look at source and medium. With utm_medium=template, traffic may land in “Unassigned” or in another channel, depending on the rules and the list of sites Google maintains. To see WhatsApp as its own channel, create a custom channel group with the condition “Session source exactly matches whatsapp.”
Don’t put phone numbers, names, emails or other personal data in UTMs. Google prohibits sending personally identifiable information to Analytics, and the URL also stays in the browser history. To link a click to a contact, use the redirect’s opaque code and store the mapping in your database.
The template and the send payload
Template creation (same format as the Cloud API), with a body, a dynamic URL button and an opt-out button:
{
"name": "weekly_offer_url",
"language": "en_US",
"category": "MARKETING",
"components": [
{
"type": "BODY",
"text": "Hi, {{1}}! The kit you looked at is 20% off until Sunday.",
"example": { "body_text": [["Ana"]] }
},
{
"type": "BUTTONS",
"buttons": [
{
"type": "URL",
"text": "See offer",
"url": "https://www.store.com/w/{{1}}",
"example": ["k7Qm2x"]
},
{ "type": "QUICK_REPLY", "text": "Stop offers" }
]
}
]
}
Sending to a contact, passing the name in the body and the code in the button. index is the URL button’s position in the list (starting at 0):
{
"messaging_product": "whatsapp",
"to": "5511999999999",
"type": "template",
"template": {
"name": "weekly_offer_url",
"language": { "code": "en_US" },
"components": [
{
"type": "body",
"parameters": [{ "type": "text", "text": "Ana" }]
},
{
"type": "button",
"sub_type": "url",
"index": "0",
"parameters": [{ "type": "text", "text": "k7Qm2x" }]
}
]
}
}
The flow in your system, per contact:
- Generate a unique short code and save
code → campaign, template, destination. - Send the template with the code in the button parameter.
- Store the
wamidreturned by the API alongside the code. - Update
delivered,readorfailedwhen the status webhook arrives. - Log the click in the redirector and let GA4 handle the session and the purchase.
If you’re sending to many contacts, generate the codes in bulk before the campaign. The article on how to send a template through the API details the other payload components.
Measuring clicks through Meta
Besides your redirector, Meta counts button clicks. According to the analytics documentation, template analytics return clicks on URL and quick reply buttons for marketing and utility templates, with the types url_button (total) and unique_url_button (distinct accounts that clicked). Three details:
- you must first enable template analytics on the account (in WhatsApp Manager or with
is_enabled_for_insights=true), and once enabled they can’t be turned off; - read and click data is available for up to 7 days from the send date;
- click tracking can be turned off per template with
cta_url_link_tracking_opted_out.
Sample query (with placeholder IDs):
curl -g 'https://graph.facebook.com/v26.0/<WABA_ID>/template_analytics?start=1793145600&end=1793232000&granularity=daily&metric_types=sent,delivered,read,clicked&template_ids=[<TEMPLATE_ID>]' \
-H 'Authorization: Bearer <TOKEN>'
Meta’s numbers and your redirector’s don’t have to match exactly: they’re different measurements. Use Meta to compare templates against each other and your redirector to link clicks to sends and sales.
Report: from send to sale
With the redirect code, you can bring everything together in one table per campaign. Example in SQL, assuming the tables sends (one row per message, with the link code and the latest webhook status), clicks (redirector) and orders (with the link code saved at checkout):
SELECT
s.campaign,
COUNT(*) FILTER (WHERE s.status IN ('delivered', 'read')) AS delivered,
COUNT(*) FILTER (WHERE s.status = 'read') AS read,
COUNT(c.code) AS clicked,
COALESCE(SUM(o.orders), 0) AS orders,
COALESCE(SUM(o.revenue), 0) AS revenue,
ROUND(COUNT(*) FILTER (WHERE s.status IN ('delivered', 'read')) * 0.3217, 2) AS meta_cost_brl
FROM sends s
LEFT JOIN (SELECT DISTINCT code FROM clicks) c ON c.code = s.code
LEFT JOIN (
SELECT link_code, COUNT(*) AS orders, SUM(amount) AS revenue
FROM orders GROUP BY link_code
) o ON o.link_code = s.code
GROUP BY s.campaign;
The result becomes a report like this (fictional example, just to show the format):
| Campaign | Delivered | Read | Clicked | Orders | Revenue | Meta cost |
|---|---|---|---|---|---|---|
| bf2026_sneakers | 4,000 | 2,900 | 610 | 48 | R$ 9,120.00 | R$ 1,286.80 |
| winback_oct | 1,500 | 820 | 95 | 7 | R$ 980.00 | R$ 482.55 |
The cost uses Meta’s marketing rate for Brazil in effect since October 1, 2026 (R$ 0.3217, in Brazilian reais, per delivered message); for other countries, use the rate for the recipient’s country. In GA4, the same analysis shows up in the traffic acquisition report filtered by Session source = whatsapp, with the purchase event marked as a key event.
One caveat: without user identification (User-ID), GA4 generally only attributes the sale if it happens in the same browser as the click. If the customer clicks on their phone and buys on their computer, the sale disappears from the GA4 report, but it can show up in yours if checkout stores the link code. To send the sale back to Meta, see the Conversions API for WhatsApp.
How to do this with Vectalk
In Vectalk you create the template with a dynamic URL button through the API (create template), with components in the Cloud API format shown above, and send it with the button parameter through the messages endpoint (send template, “Botão com parâmetro dinâmico” example). Status webhooks arrive at the endpoint you register, ready to feed the sends table.
The redirector, GA4 and the report live in your system: Vectalk has no campaign tool or URL shortener in its dashboard. Each delivered message is deducted from your prepaid balance, with no card registered with Meta. Create your account at apps.vectalk.com.br/register. If you’re just getting started, read the official WhatsApp API guide.
Frequently asked questions
Can I put the variable in the middle of the URL?
No. Meta accepts at most one variable in the button URL, and it is always appended to the end of the address.
Can I use a third-party shortened link in the button?
The components documentation doesn’t mention shorteners. We recommend a redirect on your own domain: the customer recognizes the address, and you control the destination and log the click.
Does it work in utility templates?
Yes, the dynamic URL button exists in utility templates, for example as an order tracking link. Use a different utm_medium (such as utility) so it doesn’t mix with campaigns. Meta also counts clicks from utility templates in analytics.
Why does GA4 show fewer clicks than my redirector?
Blockers, cookie consent, slow-loading pages and repeated clicks make the numbers diverge. The redirector counts the request; GA4 counts the session that loaded the tag.
How do I track ads that lead to WhatsApp?
That’s a different path: click-to-WhatsApp ads carry source data in the customer’s first message. See how to track Click-to-WhatsApp ads.
Official sources consulted on October 29, 2026: Template components, WhatsApp Business Platform analytics, Pricing, UTM parameters in Google Analytics, GA4 default channel group, Best practices to avoid sending PII to Analytics, Vectalk documentation: send template.
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.
