# 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.

Source: https://whasp.ai/blog/whatsapp-onboarding-wa-me-links · 2026-10-20

## 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.

## What is the exact wa.me link format?

WhatsApp's [click to chat help page](https://faq.whatsapp.com/5913398998672934) documents three forms:

```text
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.

## How do you build the link in code?

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`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent) handles both.

```ts
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:

```bash
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.

## How do you make a QR code for a wa.me link?

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.

```ts
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](https://github.com/soldair/node-qrcode). 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](https://developers.facebook.com/docs/whatsapp/business-management-api/qr-codes). 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.

```ts
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](https://whasp.ai/blog/whatsapp-webhook-handling).

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.

## How does whasp pick the number in the link?

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](https://whasp.ai/blog/whatsapp-number-replaced-migrate-users) for the full playbook. [How many WhatsApp numbers does your AI agent need?](https://whasp.ai/blog/scale-whatsapp-ai-agent-multiple-numbers) 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"](https://github.com/WhiskeySockets/Baileys/issues/2441), 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](https://whasp.ai/blog/whatsapp-463-error-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 should the link and the QR code go?

| 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:

```ts
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](https://whasp.ai/docs#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](https://whasp.ai/blog/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.
