WhatsApp API & Developers

WhatsApp API Permanent Access Token: System User Guide

Your integration worked all morning. After lunch every request came back with error 190 and an "expired" message, and the fix you found online was "generate a new token", which works until it expires again. That loop happens because the token Meta's dashboard hands you first is a short-lived user token, built for clicking around, not for a server.

What you want is a WhatsApp API permanent access token: a system user token that represents your business rather than a person, survives staff changes and does not die every few hours. This guide shows how to create one, which permissions and assets it needs, why Meta now recommends a 60-day expiring version over a truly permanent one, and how to rotate tokens without downtime.

The three token types, and which one you need

Meta's WhatsApp docs describe three token types. Picking the right one is mostly about who your app serves.

TokenRepresentsLifetimeWho should use it
User access tokenYou, signed in to the dashboardShort; Meta says you regenerate it every few hoursFirst tests in API Setup only
System user access tokenYour business, as an automated serviceNon-expiring, or 60 days if you choose expiringDirect developers messaging from their own numbers
Business Integration System User token ("business token")One onboarded customerSet by your Facebook Login for Business configurationTech Providers and Solution Partners, via Embedded Signup

If you send messages from your own business's numbers, you need a system user token. If you build software that other businesses connect to, you need business tokens, which you get by exchanging the code Embedded Signup returns; see our WhatsApp Embedded Signup guide. Solution Partners also keep a system token for one job: sharing their credit line with new customers.

Create the system user and generate the token

The steps live in Meta Business Suite, not the developer dashboard.

  1. Open Business Settings for the business portfolio that owns your app and WhatsApp account, then System users.
  2. Click Add, name it after the service (for example whatsapp-prod-sender), and choose a role. An Admin system user gets full access to every WhatsApp account and Messaging account the portfolio owns or has shared with it. An Employee system user only gets the accounts you grant it.
  3. Click Assign assets. Give it your app with Manage app under full control.
  4. Give it access to your WhatsApp account and your Messaging account. Since Meta's 2026 account model change, these are two separate assets, and Meta's docs are explicit that access to the Messaging account alone is not enough. Assign both in Business Suite; the panel does not cascade one to the other.
  5. Click Generate token, select your app, choose an expiration preference and add three permissions: business_management, whatsapp_business_management and whatsapp_business_messaging.
  6. Copy the token immediately into your secret store.

Meta notes that asset grants can take a few minutes to apply, so reload the page if the app does not show as assigned yet. If you skip step 4, the token looks fine but calls against that account fail with error 200, which is a permission error, not an HTTP 200.

For most single-business setups, an Employee system user with access to just the accounts it sends from is the least-privilege choice. Use an Admin system user only when the service genuinely needs every account the portfolio has, including ones added later.

Permanent or 60-day expiring?

Meta's system user documentation now lists two kinds. Non-expiring tokens never expire and never need a refresh. Expiring tokens are valid for 60 days and must be refreshed inside that window. Meta labels the expiring kind as the recommended one, says some businesses are required to use it, and says all integrations should adopt expiring tokens over time.

So "permanent" is still available, but the direction is clear. If you choose expiring, refresh on a schedule well before day 60:

curl -G "https://graph.facebook.com/v23.0/oauth/access_token" \
  --data-urlencode "grant_type=fb_exchange_token" \
  --data-urlencode "client_id=$APP_ID" \
  --data-urlencode "client_secret=$APP_SECRET" \
  --data-urlencode "set_token_expires_in_60_days=true" \
  --data-urlencode "fb_exchange_token=$WHATSAPP_TOKEN"

The response carries a fresh access_token and expires_in in seconds. Miss the window and the token is forfeited; you have to generate a new one by hand. Use the current Graph API version from Meta's changelog in place of v23.0.

Rotation without downtime follows Meta's own sequence: refresh to get a new token (the old one keeps working until its original expiry), deploy the new one everywhere, then revoke the old one. Revocation is immediate:

curl -G "https://graph.facebook.com/v23.0/oauth/revoke" \
  --data-urlencode "client_id=$APP_ID" \
  --data-urlencode "client_secret=$APP_SECRET" \
  --data-urlencode "revoke_token=$OLD_WHATSAPP_TOKEN" \
  --data-urlencode "access_token=$WHATSAPP_TOKEN"

Even with a non-expiring token, rehearse this rotation once. The day you need it is usually the day a token leaked.

Store and use the token safely

A system user token can send messages as your business and change templates. Treat it like a database password.

  • Server side only. Never ship it in a mobile app, browser bundle or a no-code tool that exposes it to every editor.
  • Secret manager or environment variable, read at runtime. Not in the repo, not in a config file that gets copied around.
  • Treat it as opaque. Meta says tokens vary in length and their structure can change. Store them in a variable-length field with no fixed maximum, and never parse them.
  • Keep it out of logs. Redact the Authorization header in request logging and error trackers.
  • One system user per service or environment. Separate tokens for staging and production make revoking one painless.
  • Keep the app secret separate. It signs webhooks and refreshes tokens, so a leak of both is worse than a leak of one.

Our WhatsApp API security checklist covers webhook signatures and key rotation for the rest of the stack.

Token errors and how to read them

When a call fails, the error.code in the response body tells you whether the token, its permissions or its asset access is the problem.

CodeMeta's meaningUsual fix
190Access token has expiredRefresh or generate a new token; check you are not still using the dashboard token
0Unable to authenticate the app userToken expired or was invalidated; generate a new one
200Permission error, often missing asset accessGrant the system user both the WhatsApp account and the Messaging account
10 or 3Permission not granted or capability missingCheck the token has the three WhatsApp permissions
131005Permission not granted or removedSame check, in the access token debugger

To see what a token can actually do, ask the Graph API:

curl -G "https://graph.facebook.com/v23.0/debug_token" \
  --data-urlencode "input_token=$WHATSAPP_TOKEN" \
  --data-urlencode "access_token=$APP_ID|$APP_SECRET"

Look at is_valid, expires_at and scopes. The same check is available in Meta's Access Token Debugger tool. In code, handle 190 by alerting a person rather than retrying in a loop, because retries cannot fix a dead token:

const res = await fetch(`https://graph.facebook.com/v23.0/${process.env.PHONE_NUMBER_ID}/messages`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.WHATSAPP_TOKEN}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify(payload),
});
const data = await res.json();
if (data.error?.code === 190) {
  await notifyOnCall("WhatsApp token rejected with 190. Rotate it.");
}

For the wider list, see our table of WhatsApp API error codes, and for the full first-day sequence our WhatsApp Cloud API setup walkthrough. The bigger context is in the complete guide to the WhatsApp API.

Where Tenashi fits

Tokens are one of several moving parts when you run your own Cloud API integration. If your goal is to send and receive WhatsApp messages from your systems on the number your team already uses, Tenashi's API is simpler to authorise. A workspace admin creates a key in Settings, it is shown once, and you send it as Authorization: Bearer tk_live_.... Each key acts with the permissions of the teammate who created it, can be revoked at any time, and stops working with 401 key_owner_removed if that teammate leaves the workspace, so name keys after a service owner, not an intern.

The trade-off: Tenashi runs on your own number as a linked device, not Meta's official platform. There are no templates, buttons or green tick, and sends are paced for safety, so it suits conversations and team workflows rather than OTPs or bulk alerts. The API is included on every paid plan, from $18/mo.

Read the API docs · Get started

FAQ

How long does the WhatsApp API temporary token last?

Meta's docs say the token from API Setup expires quickly and is not suitable for development; user tokens need regenerating every few hours. Once it expires, calls fail with error 190. Use it only for your first test message, then switch to a system user token generated in Business Settings for anything that runs on a server.

Is a WhatsApp system user token really permanent?

It can be. Meta offers non-expiring system user tokens and 60-day expiring ones, and recommends the expiring kind as a security practice, noting some businesses must use it. A non-expiring token still stops working if it is revoked or its system user is deleted, so build for rotation either way.

Which permissions does a WhatsApp system user token need?

For Cloud API messaging, Meta lists three: business_management, whatsapp_business_management and whatsapp_business_messaging. The system user also needs the app assigned with Manage app control, plus access to both the WhatsApp account and the Messaging account. Missing asset access is the most common cause of error 200 with an otherwise valid token.

Why do I get error 200 with a valid token?

Code 200 is a permissions error in the response body, not HTTP success. It usually means the system user behind the token lacks access to the WhatsApp account or Messaging account you are calling. Since Meta split those assets in 2026, access to one does not cover the other in Business Suite, so assign both explicitly.

Can one token send from several phone numbers?

Yes. A system user token works for every phone number in the accounts that system user can access. An Admin system user covers all accounts in the portfolio; an Employee covers only the ones granted. You choose the sending number per request through the phone number ID in the URL, not through the token.


Create the system user once, store the token like a password and rehearse a rotation before you need it. If you would rather skip token plumbing for team conversations, read the Tenashi API docs.