Too Long? Read This in 10 Seconds
Executive bullet points for fast decision-making
- ✓ wa.me and api.whatsapp.com do the same job; wa.me is shorter and preferred for print and bios.
- ✓ The text parameter must be percent-encoded — an unencoded ampersand truncates your message.
- ✓ In-app browsers inside social apps are where deep links most often fail.
- ✓ Keep tracking parameters on your own redirect, not inside the WhatsApp URL.
Click-to-chat is a URL that hands off to an app. Everything that goes wrong with it happens at one of three points: the number, the encoding, or the handoff.
The URL forms#
https://wa.me/<number>?text=<encoded>
https://api.whatsapp.com/send?phone=<number>&text=<encoded>
whatsapp://send?phone=<number>&text=<encoded>
| Form | Behaviour | Use for |
|---|---|---|
wa.me |
Short, resolves to app on mobile, WhatsApp Web on desktop | Bios, print, QR, buttons — anything human-visible |
api.whatsapp.com/send |
Same behaviour, longer, parameters in the query string | Programmatic construction where a query string is easier |
whatsapp:// |
Custom scheme, opens the app directly, fails hard if not installed | Native apps only, never on the open web |
Never use the custom scheme on a web page. If WhatsApp is not installed, the browser shows an error page rather than a graceful fallback.
Number formatting#
Country code, then subscriber number. No +, no spaces, no dashes, no leading trunk zero. A locally formatted number produces an "invalid number" screen the customer sees and you do not.
Encoding the message#
Percent-encode everything after text=:
| Character | Encoded | Why it matters |
|---|---|---|
| space | %20 |
Most common |
| newline | %0A |
Multi-line pre-fills |
& |
%26 |
Unencoded, it starts a new URL parameter and truncates your message |
# |
%23 |
Unencoded, everything after it is treated as a fragment and dropped |
+ |
%2B |
Otherwise decoded as a space |
? |
%3F |
Second question mark confuses some parsers |
In JavaScript, let the platform do it:
const number = "919876543210";
const text = "Hi, I want a quote\nProduct: Starter & Pro";
const url = `https://wa.me/${number}?text=${encodeURIComponent(text)}`;
encodeURIComponent handles all of the table above. Hand-rolled string replacement handles the first row and breaks on the rest.
There is a practical length limit on the pre-filled text. Long pre-fills get truncated at different points on different platforms, so keep them to a sentence or two.
How the link resolves#
- Android: the link is matched to WhatsApp's app links and opens the app directly.
- iOS: universal links do the same, though a first tap from some contexts opens Safari briefly before handing off.
- Desktop: the browser opens
web.whatsapp.com, which requires a linked session. If the visitor is not logged in, they get a QR screen instead of a chat — which is why many sites show the button on mobile only. See chat button placement. - No WhatsApp installed: the web page shows a prompt to install rather than an error, which is the main reason to prefer the HTTPS forms over the custom scheme.
In-app browsers: where links quietly fail#
Links opened inside another app's built-in browser — social apps, email clients, some messaging apps — behave differently. The in-app browser may not be permitted to hand off to another application, so the tap appears to do nothing, or lands on the WhatsApp web page instead of the app.
There is no reliable programmatic fix. What works in practice:
- Test every placement inside the app it will actually be used in, not in your desktop browser.
- Where you control the page, offer a visible fallback: display the number as text so it can be copied.
- For campaigns where in-app browsers dominate, put the click-to-chat destination on the ad platform's native chat objective rather than a link.
Tracking, done correctly#
WhatsApp does not forward query parameters into anything you can read. UTM parameters appended to a wa.me link are silently dropped.
Two approaches that do work:
A redirect you control.
https://yoursite.com/chat?src=pdp&campaign=diwali
→ log the hit, then 302 →
https://wa.me/919876543210?text=Hi%2C%20I%20have%20a%20question%20about%20the%20Rangoli%20set
You get click counts, source attribution and the ability to change the destination number later without editing every placement.
Context in the pre-filled text. The message itself becomes the attribution signal, readable by whoever answers. Crude, free, and effective for a handful of placements.
For paid campaigns there is a third mechanism entirely: the referral payload attached to the first inbound message from a click-to-WhatsApp ad, covered in ad tracking and attribution.
Debugging checklist#
- Paste the URL into a plain browser tab. Does it reach a chat screen?
- Check the number: country code present, no
+, no leading zero. - Check the encoding: any raw
&,#,+or space in the query string? - Test on both a phone with WhatsApp installed and one without.
- Test from inside the app where the link will live, not only from a browser.
- If it works on mobile but not desktop, that is WhatsApp Web login, not your link.
Building links by hand is fine for one; it stops scaling at ten. Generate them in seconds, and route the conversations they produce to the right person.
Frequently asked questions
What is the difference between wa.me and api.whatsapp.com? +
Functionally nothing for a normal click-to-chat link. wa.me is the short form and takes the number in the path; api.whatsapp.com takes it as a phone query parameter. Use wa.me for anything a human will see or type.
How do I add a message to a WhatsApp link? +
Append ?text= followed by the percent-encoded message. Spaces become %20 and line breaks become %0A.
Why does my WhatsApp link open the browser instead of the app? +
Usually because it was opened inside another app's built-in browser, which may not hand off to WhatsApp. Opening it in the system browser resolves it.
Can I add UTM parameters to a WhatsApp link? +
Not usefully — WhatsApp does not pass them anywhere you can read. Put the tracking on a redirect you control, or encode the campaign context in the pre-filled text.