Generating a production access token the right way
The previous lessons named the temporary-vs-permanent token distinction. This one covers actually generating a System User token, scoping it correctly, and treating it with the security discipline it needs once it's the one credential a real backend depends on.
4 min read
Why a personal login token is the wrong foundation
A temporary token generated from the App's "API Setup" page is tied to whoever was logged in at the time. That's fine for a first "send a test message" check, but building production code on top of it means the integration silently breaks the moment that person's session expires (24 hours) — or permanently, if that person ever loses access, changes their password in a way that invalidates sessions, or leaves the organization entirely. None of that has anything to do with whether the code is correct; it's an identity problem disguised as a bug report.
The fix: a System User
A System User is a non-human identity created in Business Settings → Users → System Users, specifically so a production integration's credential isn't tied to any one person's personal account. Generating a token from a System User instead:
The token this produces has no expiry by default (unlike the 24-hour personal-login token) and is scoped to exactly the assets it was granted on — not a blanket "do anything on this Business Portfolio" credential.
Scope narrowly, on purpose
It's tempting to grant a System User access to every asset in the Business Portfolio "to avoid dealing with this again later." Resist that. A token scoped only to the specific App and WABA it actually needs limits the blast radius if that token ever leaks — a leaked token that can only send messages through one WABA is a contained incident; a leaked token with Business Portfolio-wide admin rights is a much worse one.
What actually needs to hold this token
// A shape worth copying regardless of framework:
// treat "not configured" as a valid, expected runtime state --
// never let it be an uncaught exception.
export const whatsappConfig = {
enabled: Boolean(process.env.WHATSAPP_ACCESS_TOKEN && process.env.WHATSAPP_PHONE_NUMBER_ID),
accessToken: process.env.WHATSAPP_ACCESS_TOKEN,
phoneNumberId: process.env.WHATSAPP_PHONE_NUMBER_ID,
apiVersion: process.env.WHATSAPP_API_VERSION ?? "v21.0",
};The enabled flag matters more than it looks. A missing or misconfigured WhatsApp credential should mean "skip sending this message, log it, move on" — never "throw and break the actual business operation" (a booking, a payment, a signup) that the message is just a side effect of. A messaging integration failing should never be the reason a core feature stops working.
Never let "attempted" quietly mean "delivered"
A send function should return a clear success/failure signal — a boolean, or better, the message ID Meta assigns on a confirmed send — and it should never throw: catch every failure inside the function, log it, and return the failure signal instead. The dangerous version of this bug is subtler than a crash: a caller that treats "the send function didn't throw" as "the message was delivered" can end up marking something as done in a database (a reminder sent, a notification handled) when the actual send silently failed — permanently losing that notification with no error anywhere to point at.
Treat the token like a password, because it functions like one
- Never commit it to source control, even in a private repository.
- Store it in environment variables or a secrets manager, not in application code.
- If a token is ever exposed (committed by accident, logged, pasted somewhere public), regenerate it immediately in Business Settings — a System User's token can be revoked and reissued without disrupting the WABA or phone number itself.
- Rotate credentials when someone with access to them leaves a team, the same discipline as any other production secret.
Further reading
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. A System User is being set up for a new backend integration. What's the recommended scoping approach?
2. A messaging integration's WHATSAPP_ACCESS_TOKEN env var is accidentally left unset in one environment. What should happen when the code tries to send a message?
3. A reminder job marks `reminderSent = true` in the database immediately after calling the send function, without checking its return value. What's the risk?
4. A System User's access token is accidentally committed to a public GitHub repository. What's the correct immediate response?