In India, wedding and mehndi invitations go out on WhatsApp. The printed card still exists, but the real guest list lives in a family member's phone, and the real RSVP is a reply saying "Yes, 4 of us". I built EventGuru to plan these events (budgets, vendors, checklists, payment schedules), and the most-used feature turned out to be sending personalised invitations to a few hundred guests and keeping track of who replied.
This article explains how that works without any WhatsApp API: turning messy phone numbers into something WhatsApp accepts, filling in a personalised message, building a link that survives emoji and ampersands, and tracking RSVPs when the app can't read the replies. The examples come from running EventGuru's own functions.
Why not use the WhatsApp Business API?
The official WhatsApp Business Platform is built for companies messaging their customers. It needs a business account, messages that start a conversation must use pre-approved templates, it is billed per conversation, and the messages come from a business number. None of that fits a family sending invitations from their own phone. Guests expect the message to come from someone they know, and nobody wants to wait for template approval before inviting their cousins.
WhatsApp has a much simpler feature for this called Click to Chat. You open a URL containing a phone number and some text, WhatsApp opens a chat with that number with the text already typed, and the user presses Send. EventGuru is built entirely on that: the app prepares everything, and a human still taps Send for every message. That also keeps the user in line with WhatsApp's rules on bulk and automated messaging, because nothing is sent automatically.
Step 1: Turning a phone number into a WhatsApp number
Click to Chat expects the number in full international format with digits only: country code, then the number, no plus sign, spaces, dashes or leading zero. Guest lists are never typed that way. People paste numbers from their contacts, type them from memory, or copy them out of an old Excel sheet. EventGuru stores a default country code on each event (91 for India) and normalises every number with toWhatsAppNumber:
const trimmed = phone.trim();
let digits = trimmed.replace(/\D/g, ''); // keep digits only
if (trimmed.startsWith('+') || trimmed.startsWith('00')) {
digits = digits.replace(/^00/, ''); // already international
} else {
// Local number: drop the trunk prefix 0, add the event's country code
digits = countryCode.replace(/\D/g, '') + digits.replace(/^0+/, '');
}
return digits.length >= 8 ? digits : null;
With the event's country code set to 91, this is what the function returns for the kinds of numbers people actually type:
| Typed as | WhatsApp number | What happened |
|---|---|---|
98765 43210 | 919876543210 | Local number, country code added |
098765-43210 | 919876543210 | Trunk prefix 0 dropped, dash removed |
+91 98765 43210 | 919876543210 | Already international, kept as typed |
0091 98765 43210 | 919876543210 | The 00 international prefix is stripped |
+1 (415) 555-0132 | 14155550132 | A relative abroad: the + overrides the event default |
12345 | null | Too short to be a real number |
ask mama | null | No digits at all |
Two design choices matter here. First, the function returns null rather than guessing when a number is unusable, and the rest of the app treats null as "no WhatsApp number": the guest's card shows no Invite button and Invite next skips them. A wrong guess would open a chat with a stranger. Second, the + is the only signal that a number is already international. A US number typed as (415) 555-0132 in an Indian wedding becomes 914155550132, which is wrong, and there's no reliable way to detect that from the digits alone. The fix is to type the + for overseas guests, which is why the phone field's hint reads 98765 43210 or +971 50 123 4567.
Step 2: A personalised message from a template
Each event has an invitation template that the host can edit, with five placeholders: {name}, {event}, {date}, {venue} and {host}. The default template is:
Assalam-o-Alaikum / Namaste {name}! π
You are warmly invited to *{event}*
π
{date}
π {venue}
Please reply *YES*, *NO* or *MAYBE* and how many of you will join us.
β {host}
The asterisks are WhatsApp's own formatting, so the event name arrives in bold. fillTemplate replaces each placeholder, and every one of them has a fallback, because hosts often send the first invitations before the venue is booked:
.replace(/\{name\}/g, guest?.name ?? 'there')
.replace(/\{event\}/g, event.name)
.replace(/\{date\}/g, [prettyDate(event.date), event.time].filter(Boolean).join(' at ') || 'Date TBC')
.replace(/\{venue\}/g, event.venue || 'Venue TBC')
.replace(/\{host\}/g, event.host || 'Your hosts')
For a mehndi on 12 December 2026 at 7 PM, the message for a guest named Priya comes out as:
Assalam-o-Alaikum / Namaste Priya! π
You are warmly invited to *Ayesha & Imran's Mehndi*
π
Sat, 12 Dec 2026 at 7:00 PM
π Rang Mahal Banquets, Andheri
Please reply *YES*, *NO* or *MAYBE* and how many of you will join us.
β The Khan family
The date is stored as 2026-12-12 and formatted with the day of the week, because "Saturday" is what people remember. One small trap: new Date('2026-12-12') is parsed as midnight UTC, which is still 11 December in timezones west of UTC. The app parses '2026-12-12T00:00:00' instead, which is local midnight, so the weekday is right for everyone. The Send to a group button uses the same template with no guest, so {name} becomes "there" and the message reads naturally in a family group.
Step 3: Building a link that survives emoji and ampersands
The message goes into the URL's text parameter, so it has to be percent-encoded with encodeURIComponent. Skipping that breaks in a predictable way: the & in "Ayesha & Imran" would end the parameter, and the guest would receive a message that stops mid-name. Encoded, the start of the link looks like this:
https://api.whatsapp.com/send?phone=919876543210&text=Assalam-o-Alaikum%20%2F%20Namaste%20Priya!%20%F0%9F%8E%89%0A%0AYou%20are%20warmly%20invited%20to%20*Ayesha%20%26%20Imran's%20Mehndi*...
Newlines become %0A, the ampersand becomes %26, and the π becomes its four UTF-8 bytes, %F0%9F%8E%89.
The best-known Click to Chat format is the short wa.me/<number>?text=β¦ link, and it is the obvious one to use. The problem is emoji: messages opened through wa.me arrived with every emoji replaced by "οΏ½". The short link works by redirecting to another WhatsApp URL, and the emoji get damaged somewhere along the way. The fix was to skip the redirect and open WhatsApp directly:
// The app's own URL scheme on phones; api.whatsapp.com on the web,
// or if the app isn't installed.
const openWhatsApp = async (params) =>
(Platform.OS !== 'web' && (await open('whatsapp://send?' + params))) ||
open('https://api.whatsapp.com/send?' + params);
On a phone, whatsapp://send hands the message straight to the installed app. On a laptop, api.whatsapp.com/send offers to continue in WhatsApp Desktop or WhatsApp Web. Leaving out the phone parameter opens WhatsApp's own chat picker instead, which is how group sharing works.
Step 4: Tracking RSVPs when you can't read the replies
Click to Chat only goes one way. The app can open a chat, but it never sees the reply. So EventGuru treats RSVP tracking as a small state machine that the host moves along by hand, with the app doing the bookkeeping:
- Not invited β Invited happens automatically. When WhatsApp opens successfully for a guest who hasn't been invited, the app marks them Invited and records the time. If WhatsApp fails to open, the status doesn't change, so a guest is never marked invited when nothing was sent.
- Invited β Attending, Maybe or Declined is one tap on the guest's card when their reply comes in.
- Remind sends a gentler follow-up template to a guest who has already been invited, and doesn't change their status.
The button that makes sending a few hundred invitations bearable is Invite next. It picks the first guest who is still Not invited and has a usable number, opens WhatsApp with their personalised message, and marks them Invited. The host presses Send, comes back to the app and taps Invite next again. That takes a few seconds per guest, with no copying and pasting.
Counting heads, not guests
The question the caterer asks is how many plates, not how many invitations. Each guest has a party size, and every total in the app adds up party sizes rather than counting guest entries. Share RSVP summary turns those totals into a message for the family group. For a guest list where Priya (2) and Farhan (4) are attending, Meera (1) is a maybe, Rohit (3) hasn't replied, Sana (2) declined and Uncle Javed's family (5) hasn't been invited yet, it produces:
*Ayesha & Imran's Mehndi β Guest update*
β
Attending: 6
π€ Maybe: 1
β³ Awaiting reply: 3
β Declined: 2
π Not invited yet: 5
*Attending*
β’ Priya (+1)
β’ Farhan (+3)
*Awaiting reply*
β’ Rohit (+2)
The "(+3)" format is how people already talk about it: Farhan plus three. The list of people who haven't replied is there so someone in the family group can say "I'll call Rohit".
Getting the guest list in
Nobody types three hundred guests one at a time. The bulk add box accepts one guest per line in the form Name, phone, party size, with the phone and party size optional and commas, tabs or semicolons as separators. That means you can paste two or three columns straight from a spreadsheet. Pasted numbers are normalised the same way as typed ones when the invitation is sent, so the mix of formats in a typical family spreadsheet doesn't matter.
Where the data lives
A wedding guest list is a few hundred people's names and phone numbers. EventGuru keeps all of it on the device, in AsyncStorage on phones and localStorage in the browser, and never sends it to a server. Opening WhatsApp shares a single guest's number and message with WhatsApp, which is the same thing that happens when you start a chat yourself.
Lessons that apply beyond invitations
- Check whether you need the API at all. A URL scheme plus a human pressing Send covered the whole use case, with no approval process, no cost and no account to manage.
- Normalise input at the edge, and refuse to guess. Returning
nullfor an unusable number is safer than sending an invitation to a stranger. - Test with the content people actually send. The emoji bug only showed up with real invitation text, never with "hello test".
- When you can't observe an outcome, make recording it effortless. One tap per reply is what keeps the RSVP list accurate.
EventGuru is free, runs in the browser on your phone or laptop, and comes with starter budgets and checklists for weddings, mehndi, birthdays and family events.