wa.me links and QR codes: get users to text your AI agent

How to build a WhatsApp wa.me link with a prefilled message, turn it into a QR code, and tell which user wrote in, with working TypeScript and the whasp API.

An abstract square pattern of green tiles beaming light into a phone screen.

Key takeaways

  • A wa.me link is https://wa.me/ plus the full international number with no plus sign, zeros, brackets or dashes, and ?text= adds a URL-encoded message that appears in the user's chat box.
  • The user still has to tap send and can edit the prefilled text, so a code inside it is a claim to match against your records, not proof of identity.
  • With whasp, GET /v1/numbers/pick returns the number with the most room and a ready wa_link, so every chat starts with the user writing first.
  • A QR code is the same link encoded, and anything you print should encode your own redirect URL, because numbers fill up and can be replaced.

A wa.me link is WhatsApp’s click-to-chat URL. The format is https://wa.me/<number>, where the number is in full international format with no plus sign, zeros, brackets or dashes, and ?text= adds a URL-encoded first message. Tapping the link opens a chat with your agent’s number, so the user writes first and your agent can answer.

WhatsApp’s click to chat help page documents three forms:

https://wa.me/15555550100
https://wa.me/15555550100?text=Hi%20Ava
https://wa.me/?text=Hi%20Ava

The first opens a chat with that number. The second also puts the text in the message box. The third has no number: WhatsApp shows a list of contacts to send the message to, so it is for sharing, not for starting a chat with your agent.

The number rule on that page is “a full phone number in international format”, with any zeroes, brackets or dashes left out. Its example of what not to use is https://wa.me/+001-(XXX)XXXXXXX. whasp returns numbers in E.164 form (+15555550100), so drop the plus sign.

Three behaviors to know:

  • The link works on phones and on WhatsApp Web.
  • It works for a number that is not in the user’s address book, as long as the number has an active WhatsApp account.
  • The prefilled text lands in the text field. It is not sent. The user taps send, and can edit or delete the text first.

Strip every non-digit from the E.164 number, then URL-encode the text. Never paste raw text into the URL: an & ends the parameter early and a # starts a fragment. encodeURIComponent handles both.

export function waLink(e164: string, text?: string): string {
  const digits = e164.replace(/\D/g, "").replace(/^0+/, ""); // "+1 555 555 0100" -> "15555550100"
  return `https://wa.me/${digits}` + (text ? `?text=${encodeURIComponent(text)}` : "");
}

Four mistakes to avoid:

  • Keeping the plus sign, a leading 00 or spaces in the number. WhatsApp’s page says to leave them out.
  • Skipping the encoding, so an & in the message cuts the text short.
  • Building the link once and caching it. Numbers fill up and get replaced.
  • Treating the prefilled message as sent. It sits in the text field until the user taps send.

Keep the prefilled text short and plain. WhatsApp’s page states no length limit for it, but Meta caps prefilled text at 140 characters on its own QR codes and short links, which is a sensible ceiling for any entry message.

Skip emoji in that text. On 19 October 2026 we requested wa.me links with emoji in ?text= and the redirect to api.whatsapp.com/send came back with the emoji replaced by the Unicode replacement character (%EF%BF%BD). Accented Latin letters came through intact, and the same text sent straight to api.whatsapp.com/send kept the emoji. You can repeat the check:

curl -sI "https://wa.me/15555550100?text=Hi%20%F0%9F%91%8B" | grep -i location

WhatsApp may change this, so test on real phones before relying on either behavior.

A QR code for click-to-chat is the link itself, encoded. Generate it from the full URL with any QR library and show it on desktop pages, where the user is not holding a phone. A phone camera reads a QR code that holds a URL and offers to open it, and wa.me then hands the chat to WhatsApp. Meta’s documentation describes the same scan-with-the-camera flow for its own QR codes.

import QRCode from "qrcode";

const link = waLink("+15555550100", "Hi Ava");
const svg = await QRCode.toString(link, { type: "svg", margin: 2, errorCorrectionLevel: "M" });
await QRCode.toFile("ava.png", link, { width: 512, margin: 2 });

That is the qrcode package. Use the SVG for print, because it scales without blurring (Meta’s QR code documentation makes the same recommendation), and the PNG for screens. A shorter URL gives a simpler code that scans more easily. Test the printed code at its final size, on two or three different phones, before you order a thousand stickers.

Do not confuse this with Meta’s own QR codes and short links. That feature belongs to the WhatsApp Business Platform: up to 2,000 per business phone number, prefilled text up to 140 characters, no scan analytics, managed through the Business Management API. A whasp number is a personal WhatsApp account, so you use plain wa.me links.

How does your agent know who just wrote?

A message.received event carries the sender’s phone number (chat), their WhatsApp name (sender_name) and the text. It does not carry your user id. If the person clicked a button inside your app, you want to connect the two. Put a one-time code in the prefilled text.

import { randomBytes } from "node:crypto";

const API = "https://api.whasp.ai/v1";
const pending = new Map<string, { userId: string; expires: number }>(); // use your database

export async function startChat(userId: string) {
  const code = randomBytes(3).toString("hex"); // e.g. "9f3a1c"
  pending.set(code, { userId, expires: Date.now() + 24 * 60 * 60 * 1000 });

  const text = `Hi Ava, my code is ${code}`;
  const res = await fetch(`${API}/numbers/pick?text=${encodeURIComponent(text)}`, {
    headers: { Authorization: `Bearer ${process.env.WHASP_API_KEY}` },
  });
  if (res.status === 409) return null; // no_capacity: every number is full, offline or unhealthy
  if (!res.ok) throw new Error(`whasp answered ${res.status}`);
  return (await res.json()) as { number: string; wa_link: string };
}

// In your message.received handler: who is this?
export function claimCode(text: string): string | null {
  const code = /my code is ([0-9a-f]{6})\b/i.exec(text)?.[1]?.toLowerCase();
  const hit = code ? pending.get(code) : undefined;
  if (!code || !hit || hit.expires < Date.now()) return null;
  pending.delete(code); // single use
  return hit.userId;
}

When claimCode returns a user id, save the pair: the sender’s chat, the number that received the message, and your user. From then on, match that person by phone number. The webhook handling itself (signature check, dedupe on the event id) is in WhatsApp webhooks for AI agents.

The code is a claim, not proof. The user can edit the text, and anyone who receives the link can send it. Keep it single use and short-lived, make it opaque (never an email address or a token), and never put a secret in a link that ends up in browser history and in a chat.

whasp’s GET /v1/numbers/pick returns the healthy, online number with the most room for a new user, plus a wa_link with your text prefilled. It answers 409 no_capacity when every number is full, offline or unhealthy. A number takes up to 100 active chats in 30 days and 40 new chats in 24 hours, then it drops out of pick (inbound messages still arrive). Call pick when the user asks for the chat, not at build time and not once for a whole campaign.

Returning users should go back to the same number. The message.received event includes the number that received it, and a send without from goes out from the number the person last wrote to. Store it with the user and build their next link with waLink(number, text).

Numbers also change. If WhatsApp bans one, whasp retires it and gives you a replacement. The chats do not move, and the new number cannot write first, so a link that points at the old number is dead. Generate links on demand and read Moving users to a new WhatsApp number after a ban for the full playbook. How many WhatsApp numbers does your AI agent need? covers capacity.

Why does it help that users write first?

A whasp number cannot open a chat: a send to someone who never wrote returns 409 chat_not_open. The link and the QR code are the front door. They also keep the number healthy. Developers on unofficial libraries have been reporting error 463, the “reachout timelock”, since at least March 2026, and the common theory ties it to messaging people who never wrote in. WhatsApp error 463 and the reachout timelock separates what is confirmed from what is theory. With click to chat, every conversation starts with an inbound message, and a prefilled message gives your agent its first input at once.

Where What to show Why
Signup success page, on a phone A button with the per-user link One tap, code included
Same page, on a desktop browser A QR code of that link, next to the button The phone camera does the handoff
Welcome email A button that points to your own URL The link is built when clicked, so it never expires or points at a retired number
Print, packaging, social profiles A QR code of your own URL, not of the wa.me link You can change the number behind it without reprinting

For the last two rows, point at a redirect on your own domain that asks whasp for a fresh number and sends the visitor on:

import { createServer } from "node:http";

createServer(async (req, res) => {
  if (req.url !== "/chat") return void res.writeHead(404).end();
  const pick = await fetch(`https://api.whasp.ai/v1/numbers/pick?text=${encodeURIComponent("Hi Ava")}`, {
    headers: { Authorization: `Bearer ${process.env.WHASP_API_KEY}` },
  });
  if (pick.ok) {
    const { wa_link } = (await pick.json()) as { wa_link: string };
    res.writeHead(302, { Location: wa_link }).end();
  } else {
    res.writeHead(503).end("All our numbers are busy. Try again in a few minutes.");
  }
}).listen(3000);

An anonymous visitor has no code, so let the agent ask for what it needs once the chat is open. For a known user, such as the email row, have the redirect call startChat(userId) from the previous section instead of using a fixed text. WhatsApp also publishes an official “Chat on WhatsApp” button in green and white, and its help page says to use it unchanged.

What to do next

Create a number in the dashboard, then follow the quickstart to receive the first message.received event. Add the redirect above to your site, put its URL behind a button and a QR code, and check the full path on an iPhone and an Android phone. If you are building a companion, How to launch an AI companion on WhatsApp shows where this link fits in onboarding.

Questions

How do I make a WhatsApp link with a prefilled message?

Use https://wa.me/<number>?text=<urlencodedtext>, where the number is in full international format and the text is URL-encoded. In code, build the text part with encodeURIComponent. The message appears in the chat box and the user taps send.

Does a wa.me link work if the number is not saved in the user's contacts?

Yes. WhatsApp's help page says click to chat starts a chat without the number saved in the address book, as long as the number has an active WhatsApp account. It works on phones and on WhatsApp Web.

How do I turn a wa.me link into a QR code?

A QR code for click-to-chat is just the URL encoded. Pass the full wa.me link to any QR library, for example the qrcode package for Node, and export SVG for print or PNG for screens.

Can I put emoji in a wa.me prefilled message?

Test it first. When we checked on 19 October 2026, wa.me replaced emoji in the text with a replacement character, while accented Latin letters survived. Plain text is the safe choice.

Can my whasp agent message a user before they write?

No. A whasp number cannot open a chat cold and the API answers 409 chat_not_open. The user writes first, and a wa.me link or QR code is how you get them to do it.

All articles