Yes, you can message a WhatsApp username through an API. With Mossmoon you pass to_username (plus username_pin if the person set one) to the send endpoint, or call POST /usernames/lookup to find the account without sending anything.
Messages from people with a username reach your webhook with from_username, and people reachable only by username have no phone number at all, so your CRM should key them on from_wa_id. Below are the real requests and payloads, the PIN rules and the limits. It is part of the contacts features on the $15 per line plan.
What is a WhatsApp username?
A WhatsApp username is a handle like @sarah.k that people can share instead of their phone number. WhatsApp is still rolling them out, so not everyone has one yet. Three facts shape how an API has to handle them:
- No phone number
- Someone reached only by username has a WhatsApp id ending in
@lid, like777000111222333@lid. That is an internal id, not a number: never dial or text its digits. - An optional PIN
- Some people set a username PIN (4 digits, or 6 letters and digits) that strangers must give to find them.
- Strict format
- 3 to 35 characters of letters, digits,
.and_, with at least one letter, no leading, trailing or double dot, nowww.or.com, and none of the Meta brand names.
Mossmoon links your customer’s own WhatsApp as a linked device, so the line can find people by username exactly like the “new chat” search on their phone. If you’re weighing that against Meta’s platform, the Business API vs personal API explainer covers the trade-offs.
How do I send a WhatsApp message to an @username?
Use the normal send endpoint and swap the recipient field. Give exactly one of to (a phone number), to_wa_id (a WhatsApp id) or to_username; sending more than one is a 400.
- 1
Link a WhatsApp number
Create a line withPOST /api/v1/wa/linesand have your customer scan the QR on itsconnect_urlfrom WhatsApp’s Linked devices screen. The line is ready whenline.readyarrives. Personal WhatsApp and the WhatsApp Business app both work. - 2
Get the username, and the PIN if there is one
The person gives you their@username, in a form, on a call or on a card. If they set a username PIN, they need to give you that as well. - 3
Send with to_username
POST /lines/:id/sendwithto_username(andusername_pinwhen given) in place ofto. Text and media both work.curl https://mossmoon.app/api/v1/wa/lines/wa_8f3c2e1a/send \ -H "Authorization: Bearer mm_live_..." \ -H "Content-Type: application/json" \ -d '{ "to_username": "@sarah.k", "username_pin": "4821", "text": "Hi Sarah, this is Anna from the open house." }'const res = await fetch("https://mossmoon.app/api/v1/wa/lines/wa_8f3c2e1a/send", { method: "POST", headers: { Authorization: `Bearer ${process.env.MOSSMOON_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ to_username: "@sarah.k", username_pin: "4821", // only if they set one text: "Hi Sarah, this is Anna from the open house.", }), }); const { to_wa_id } = await res.json(); // save this for follow-upsres = requests.post( "https://mossmoon.app/api/v1/wa/lines/wa_8f3c2e1a/send", headers={"Authorization": f"Bearer {MOSSMOON_KEY}"}, json={ "to_username": "@sarah.k", "username_pin": "4821", # only if they set one "text": "Hi Sarah, this is Anna from the open house.", }, ) to_wa_id = res.json()["to_wa_id"] # save this for follow-ups202 Accepted { "message_id": "true_777000111222333@lid_3EB0...", "status": "queued", "line_id": "wa_8f3c2e1a", "to": "777000111222333@lid", "to_wa_id": "777000111222333@lid", "to_username": "@sarah.k", "created_at": "2026-10-16T10:02:11.402Z" } - 4
Save to_wa_id and use it from now on
The response addsto_wa_id, the person’s WhatsApp id. Store it on the contact and send follow-ups withto_wa_id. Their replies arrive with the same value infrom_wa_id, so the thread lines up.
A username that can’t be resolved is refused before anything is sent and doesn’t count against the line’s daily send limit. The full contract is in the docs under WhatsApp usernames and send a message.
How do I look up a WhatsApp username without messaging them?
POST /lines/:id/usernames/lookup finds the account behind a username and sends nothing. The person is not notified. It is a POST so a PIN never ends up in a URL or an access log.
POST /api/v1/wa/lines/wa_8f3c2e1a/usernames/lookup
Authorization: Bearer mm_live_...
Content-Type: application/json
{ "username": "@sarah.k", "username_pin": "4821" }
→ 200 OK
{
"line_id": "wa_8f3c2e1a",
"wa_id": "777000111222333@lid",
"username": "@sarah.k",
"is_business": false,
"business_name": null,
"phone_e164": null,
"is_me": false,
"source": "whatsapp"
}wa_idis the person’s WhatsApp id. Use it asto_wa_idanywhere in the API.phone_e164is set only when WhatsApp itself shows a number. For someone who shared just a username, expectnull.is_businessandbusiness_nametell you whether you’re about to message a company, andis_meflags the line’s own username.sourceis"whatsapp", or"local"when the line already knew the account.
A lookup can also fail in a handful of well-defined ways, and each has its own error code:
One more: 503 username_lookup_failed means WhatsApp didn’t answer, and it is safe to retry. A 404 doesn’t always mean the name is free; a username that is only reserved can’t be found yet.
What is a WhatsApp username PIN, and how do I pass it?
The PIN is WhatsApp’s guard against strangers. If a person turned it on, nobody can find them by username alone, through the phone or through an API. Pass it as username_pin on both send and lookup. It is 4 digits, or 6 letters and digits; anything else is refused with a 400 before WhatsApp is asked.
Ask for the PIN in the same place you collect the username, and store it with the contact only as long as you need it. Treat 403 username_pin_wrong as a reason to ask the person again, never as a cue to try other codes: WhatsApp limits PIN attempts. 429 username_pin_unavailable means WhatsApp says the account can’t be reached by PIN right now.
How do usernames arrive on webhooks?
Every message from someone with a username carries it in from_username. For a person reachable only by username, from_e164 is null, addressing is "lid" and from_wa_id ends in @lid.
POST https://your-app.com/webhooks/whatsapp
X-Mossmoon-Event: message.received
{
"event": "message.received",
"line_id": "wa_8f3c2e1a",
"ts": "2026-10-16T10:05:40.118Z",
"data": {
"message_id": "wamsg_xyz",
"from_me": false,
"from": "777000111222333@lid",
"from_e164": null,
"from_wa_id": "777000111222333@lid",
"from_name": "Sarah K",
"from_username": "@sarah.k",
"addressing": "lid",
"is_group": false,
"author_username": null,
"text": "Thanks! Is Saturday 11:00 still free?",
"type": "chat"
}
}- New contacts. The first time someone writes,
contact.first_seenfires once with their profile, includingusernameandphone_e164(null when WhatsApp doesn’t share it). See contact.first_seen. - Groups. The member who wrote a group message comes with
author_username, andauthor_e164is null for members known only by username. - Your own line.
GET /lines/:id/profilereturns the linked account’susername(set on the phone), ornull.
Replying needs no special case. Prefer to: from_e164 when it is set, and fall back to to_wa_id: from_wa_id when it is null:
// One reply shape for every contact
const d = event.data;
const target = d.from_e164 ? { to: d.from_e164 } : { to_wa_id: d.from_wa_id };
await fetch(`https://mossmoon.app/api/v1/wa/lines/${event.line_id}/send`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.MOSSMOON_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ ...target, text: "Saturday 11:00 is yours." }),
});If you’re new to receiving events, the webhook setup guide covers signatures, retries and idempotency.
How should a CRM store WhatsApp username contacts?
Most CRMs assume every contact has a phone number. Usernames break that assumption, so the contact record needs a different primary key.
- Key every WhatsApp contact on
from_wa_id. Same person, same wa_id, so it is your dedup key. Phone-number contacts have one too. - Keep the phone field optional. Fill it from
from_e164when it is present. Never derive a number from the digits of an@lidid. - Show the username as the handle. Store
from_usernamefor display and search, so your team sees@sarah.kinstead of a long id. - Match other channels carefully. Cross-channel matching (SMS, email) works only when a phone number is present. For username-only contacts, link records by hand or by something the person tells you.
The background on why WhatsApp ids and phone numbers differ is in the docs under contact identity.
Can I add someone to a WhatsApp group by username?
Yes. On POST /groups/:group_id/participants, a participant can be "@sarah.k", or "@sarah.k:4821" with their PIN. The line finds them the way WhatsApp’s own “Add member” search does. Their entry comes back with username, an @lid wa_id and e164: null.
POST /api/v1/wa/lines/wa_8f3c2e1a/groups/[email protected]/participants
Authorization: Bearer mm_live_...
{ "participants": ["+15553182290", "@sarah.k:4821"] }If a username can’t be resolved, nothing is added and the call fails with that lookup’s error, naming the username. To @mention a username-only member, pass their wa_id in mentions and put @ plus its digits in the text; WhatsApp shows their name in the tag. Removing, promoting and demoting take the wa_id. More in the groups API guide.
What are the limits of the WhatsApp usernames API?
- 10 lookups a minute per line. That counts lookups that reach WhatsApp, and WhatsApp has its own limit on top. Both return
429 username_lookup_rate_limitedwithretry_after_s. - No scraping. Lookups are a query from your customer’s own WhatsApp account. Use them when someone gives you a username, not to test lists of names; WhatsApp treats that as scraping.
- Still rolling out. Not every account has a username, and reserved ones can’t be found yet, so keep phone numbers as the default route.
- PIN attempts are limited by WhatsApp. Ask; don’t retry with guesses.
- Normal send caps apply once a username resolves: a soft cap of 750 and a hard cap of 1,500 sends a day on warmed lines (rate limits).
Mossmoon vs goghl.ai for WhatsApp usernames
goghl.ai also links WhatsApp by QR, inside GoHighLevel. It handles people who write in by username: it creates the contact with an empty phone field, routes replies by their WhatsApp id and includes the username on its webhooks, so none of that is listed here. These are the username features we offer that their public docs don’t list.
$15 a line. First line free for 7 days.
| Feature | RecommendedMossmoon$15 a line. First line free for 7 days.Start free → | goghl.ai |
|---|---|---|
| Start a new chat from an @username | Yesto_username on /send, text or media | Not listedTheir docs cover username contacts created when the person writes first |
| Look up a username without messaging | YesPOST /usernames/lookup returns the WhatsApp id; the person isn't notified | Not listed |
| Username PINs | Yesusername_pin on send and lookup, with separate errors for a missing or wrong PIN | Not listed |
| Add group members by username | Yes“@name” or “@name:PIN” on /participants | Not listedTheir group action takes a phone number |
Checked against goghl.ai’s public site and help center on October 16, 2026. “Not listed” means we couldn’t find it in their docs, not that it can’t exist. Only features goghl.ai doesn’t list are shown. Sources: goghl.ai usernames, webhooks, group participant action, goghl.ai.
Ready to try it?
Start free: your first line is free for 7 days, then $15 a month. Link a number, send to one username you were given, and watch the reply arrive with from_username.
Frequently asked questions
Yes. With Mossmoon, call POST /api/v1/wa/lines/:id/send with to_username (for example "@sarah.k") instead of a phone number, plus username_pin if the person set one. The line looks the username up the way WhatsApp's own new chat search does and sends text or media. The response includes to_wa_id, which you use for follow-ups.
POST /api/v1/wa/lines/:id/usernames/lookup with the username (and PIN if needed) returns the account's WhatsApp id, whether it is a business and its business name. Nothing is sent and the person is not notified.
Not as a rule. The lookup returns phone_e164 only when WhatsApp itself shows one. Someone who shared only a username usually has a WhatsApp id ending in @lid and no phone number, and the digits of that id are not a phone number.
An optional code (4 digits, or 6 letters and digits) a person can set so strangers can't find them by username alone. Pass it as username_pin. Without it you get 403 username_pin_required, and a wrong one gets 403 username_pin_wrong. WhatsApp limits PIN attempts, so never guess.
Mossmoon allows 10 lookups per minute per line that reach WhatsApp, and WhatsApp has its own limit on top. Over either one you get 429 username_lookup_rate_limited with retry_after_s. Look up usernames people give you; scanning lists of names is treated by WhatsApp as scraping.
Because WhatsApp didn't share that person's number, which is what happens when someone is reachable only by username. The message still carries from_wa_id (ending in @lid) and usually from_username. Key the contact on from_wa_id and reply with to_wa_id.