Features Pricing Use cases Compare Blog

The Dashboard Says Connected and Nothing Is Sending

15 min read

The short answer

Meta returns error 190 with the remedy "Get a new access token," which is misleading whenever the grant was revoked rather than aged out. A permission grant is scoped to the app inside the business that held it, so an app changing hands kills every grant while the app id, secret and configuration stay identical — and cached health status keeps reporting Connected until something forces a live call.

Numbers screen showing a connected WhatsApp number card carrying Connected and Default badges, a GREEN quality rating, a template count, and a last-checked time, with Refresh, Sync templates, Auto-assign, Edit and Disconnect actions in its header, and, on the same screen, a separate webhook callback URL field with a Copy control beside it.

What does a cached connection status actually prove?

A number's row on a WhatsApp management screen usually carries four things: a status badge, a quality rating, a template count, and a "last checked" timestamp. Every one of those is almost always a stored value — written the last time a background job asked Meta a question, not the instant a person is looking at the screen. A badge reading Connected is a claim about the past, not a live fact about right now.

That distinction is invisible on a healthy day, because most connections don't change state between checks. It stops being invisible the moment a token dies between two checks: the stored fields keep repeating the old, good answer for however long the schedule leaves them alone, while every real send is already failing on the other side.

Two columns compare a cached numbers-screen status against what a live Graph API call returns for the same WhatsApp number, joined by a single rung labelled the forced live call
What the screen shows against what Graph says right now

The gap in that picture is the whole article. Nothing about the badge lied — it reported the truth at the time it was written. It just hasn't been asked again. This is a different failure from Meta restricting a WhatsApp Business Account outright, which usually does announce itself; what follows here is quieter than a restriction, because nothing on Meta's side is refusing to talk to the platform — the platform has simply stopped being able to prove it's still allowed to.

Why does a token die without ever reaching its expiry date?

Every access token search engine result treats this as an arithmetic problem: the token had a lifetime, the lifetime ran out, mint a new one. That is a real failure mode, and it is not the interesting one.

Meta's own access token guide is blunt about the first half of this — token lifetimes are not a promise. Short-lived tokens last "about one to two hours," long-lived tokens last "about 60 days," and then, in the same breath: "Do not depend on these lifetimes remaining the same — they may change without warning or expire early" (Meta for Developers — Access Tokens). That single sentence is the whole diagnostic problem in miniature: even the number you'd use to predict expiry is documented as unreliable.

The second half is the one nobody writes up: a token can be perfectly within its stated lifetime and still stop working, because what actually authorizes it to act is a permission grant, and a grant can end for reasons that have nothing to do with a clock. A customer can revoke access from their own Business Settings. An admin can remove a partner. A password change, a two-factor reset, or an app being disconnected from a business asset can each end a grant the token itself never noticed. None of those is a timer expiring — they are someone, somewhere, deciding the grant should end, and the token is simply no longer backed by anything.

Checked against Meta's documentation on 4 September 2026.

What happens to permission grants when an app changes hands?

This is the version of the failure that is easiest to reproduce and hardest to predict from the outside, because nothing about the app itself changes.

A permission grant Meta issues through Embedded Signup or an OAuth dialog is scoped to the app, inside the business that held it — not to the app id alone. Move that app to a different business portfolio, and every grant issued under the old business dies, while the app id, the app secret, the webhook configuration and the Embedded Signup config id all stay byte-for-byte identical. There is nothing to diff. The thing that changed is not visible in any field a platform normally reads.

This is not a hypothetical. WabaCRM moved its own Meta app between business portfolios in mid-2026, and every connected customer's access token died in the same hour — while the numbers screen kept reading Connected, GREEN quality, 14 templates, because that screen was rendering last_health_check_at from twenty hours earlier. Only forcing a live call — the Refresh button, which hits Graph directly instead of reading the stored row — flipped the status to an honest one: "The WhatsApp connection has expired. Reconnect the account to resume sending." Nothing in any log flagged this as an incident on its own; the first thing that noticed was a customer.

The lesson generalizes past portfolio moves. Anything that changes ownership or scope at the business or app level — not the number, not the customer's own settings — can silently end every grant issued under the old arrangement, and a dashboard reading cached fields will not notice until it is forced to ask again. A platform offering more than one way to connect a number has somewhere to fall back to when one grant dies; a platform with exactly one connection path has none. How WhatsApp coexistence keeps a number live on both the phone and the API covers one such alternative path in detail.

How do you force a live check rather than reading a stored one?

The fix is mechanical: read the fields a stored row shows, then make the same Graph call live, and compare.

The numbers screen below shows what that looks like once it's rebuilt around forcing rather than trusting a check — a Refresh action that hits Graph on demand, sitting beside Sync templates and Disconnect, next to the quality rating and template count the cached row was already showing.

Numbers screen showing a connected WhatsApp number card with Default and Connected badges, GREEN quality rating, template count and last-checked time, and Refresh, Sync templates and Disconnect actions
A connection card with a forced live-check action next to the cached status it can override

Concretely, that Refresh should do two things a scheduled check does the same way, just on demand: call the number's own Graph endpoint for its current quality_rating and messaging status, and call debug_token against the credential that number uses, passing it as input_token. Meta's reference describes the field that matters most plainly — is_valid is "Whether the access token is still valid or not" (Meta for Developers — Debug Token) — and it returns expires_at and the token's actual granular_scopes alongside it, which is the only reliable way to see what a token can currently do rather than what it was originally issued to do.

Checked against Meta's documentation on 4 September 2026.

Which signals lie, and which ones can you trust?

Not every field on a connections screen carries the same weight. Some are genuinely current the moment they're read; most are exactly as stale as the last check that wrote them.

Signal What it actually tells you Trust it without a live call?
Status badge (Connected/Error) The result of the last scheduled or manual health check No — this is the field that lies longest
Quality rating (GREEN/YELLOW/RED) Message performance as of the last check No — cached alongside the status badge
Template count How many templates existed at last sync No, but rarely the field that matters during an outage
"Last checked" timestamp Exactly how stale every other field on the row is Yes — this is the one honest field, if the screen shows it
debug_token.is_valid (live call) Whether the credential itself still authenticates Yes, for the moment the call was made
debug_token.granular_scopes (live call) What the token can actually do right now, not what it was issued to do Yes, for the moment the call was made

The practical rule follows directly from that table: the only field worth reading without a live call is the one that admits it's old. Everything else needs to be re-asked before it's trusted, and a screen that hides its "last checked" timestamp has quietly removed the one honest signal it had.

How do you tell a revoked grant from an expired token?

Both failures produce the same symptom — sends fail, error 190 appears, the remedy line reads "Get a new access token" — but they are not the same problem, and the fields from a live debug_token call are what separate them.

An expired token shows is_valid: false with an expires_at timestamp in the past: the clock genuinely ran out, and Meta's official reference lists the field's job as reporting exactly that state. A revoked grant shows is_valid: false on a token that was never expiring in the first place — a long-lived System User token, for instance, which Meta's access-token guide describes generally, in an Ads/Pages context, as built for "programmatic, automated actions... without requiring input from an app user or re-authentication"; the same architecture underlies the long-lived System User token used for WhatsApp platform access, and implies an operational lifetime rather than a short countdown. When that kind of token goes invalid with no expiry timestamp to blame, the cause sits on the permission side, not the clock side.

This distinction matters because the two have different fixes. An expired token needs a fresh one minted the normal way. A revoked grant needs the underlying authorization restored — reconnecting the account, re-running Embedded Signup, or having whoever revoked it grant access again — and minting a new token from the same dead grant will fail identically to the last one. Meta's error code reference makes no distinction between the two at the level of the error itself: code 190 is titled "Your access token has expired" with the solution "Get a new access token" regardless of which actually happened, which is exactly why that remedy is misleading here — it correctly describes one cause and silently assumes it's the only one (Meta for Developers — Error Codes). It is the same shape of problem as error 131049 and why Meta's own text under-explains it: a terse official line that is accurate for the common case and silent about the rest.

Checked against Meta's documentation on 4 September 2026.

What does reconnecting involve for each affected customer?

Once the cause is confirmed as a revoked or re-scoped grant rather than a natural expiry, reconnecting is per-customer, not a platform-wide fix — each business has to re-authorize, because the authorization was theirs to give in the first place.

For most numbers that means re-running the same onboarding flow that connected them originally: Embedded Signup for a number that went through it, or re-entering credentials for one connected manually. Neither path should call the registration endpoint again for a number that's already registered on the Cloud API — Meta's documentation states registration requests are "limited to 10 requests per business number in a 72-hour moving window" with deregistration capped identically, and an automated reconnection flow that re-registers on every attempt can exhaust that budget and lock the number out for three days on top of whatever caused the outage (Meta for Developers — Business Phone Number Registration). Reconnection should re-attach credentials and re-confirm the webhook subscription, not repeat a step the number never needed.

Checked against Meta's documentation on 4 September 2026.

Message history, contacts and template records live in the platform's own database and are untouched by any of this — see what a WhatsApp Business app account keeps versus what the Cloud API keeps for the broader split between what Meta stores and what a connected platform stores locally. What a customer switching providers entirely has to plan for differently is covered in how to move a WhatsApp number between API providers — a revoked grant on the same provider is a smaller problem than that migration, but it's worth knowing the difference before assuming a reconnect requires starting over.

Is there any way to keep sending while grants are being restored?

No. This is worth saying plainly rather than softening, because every other section here is about detection and diagnosis, and detection doesn't change what Meta requires to send a message: a currently-valid, currently-scoped token. There is no cached credential, no offline queue, no local override that substitutes for one.

What can limit the damage is having a second connection path available for the same number. A number connected through Embedded Signup that loses its grant can, in principle, be re-attached through manual entry of a WABA id, phone number id and a System User token with the right permissions — the same third option that exists precisely because Embedded Signup itself can fail in ways that leave a customer with a working number and no way to finish the dialog. This is one of the practical differences between working with a Tech Provider rather than a BSP: the token belongs to the customer's own WhatsApp Business Account either way, so restoring it is a permissions fix, never a billing one. See WhatsApp coexistence and its actual limits for a related case where a number is deliberately connected two ways at once for exactly this kind of resilience.

Messages queued before the failure will sit as failed or pending sends, not silently vanish — but "not silent" only helps if something is watching the queue, which is the subject of the next section.

How should health checks be built so this surfaces immediately?

The failure this article describes isn't really about tokens. It's about a health check design that treats "last known good" as equivalent to "currently good," with nothing to tell the two apart on the screen a human actually looks at.

A few things follow directly from that. First, the "last checked" timestamp has to be visible next to the status it qualifies, not buried in an API response — a badge with no age attached invites exactly the trust it shouldn't get. Second, a manual, on-demand recheck has to exist and has to make the same live calls a scheduled job makes, not a lighter version of them; a Refresh button that only re-reads the cache proves nothing. Third, a scheduled check running hourly is a reasonable default, but the interval matters far less than whether the person looking at the screen can force a fresh answer whenever they actually need one — waiting for the next hourly tick is a bad experience during an active outage even when the design is otherwise sound. And fourth, a token or grant that becomes invalid should be an event something reacts to — an alert, a flag, a changed status a person is likely to see — rather than a fact that sits correctly recorded in a database column nobody is currently looking at.

None of this is exotic engineering. It is the same principle as checking a smoke alarm's battery instead of trusting that it hasn't gone off: the absence of a bad signal is not the presence of a good one. A shared team inbox built around one number's connection state has more riding on that state than a single agent watching one thread does, which is a reason to build the forced-check path early rather than after the first customer notices before the platform does.

What should an operator check before telling customers anything?

Before any message goes out about tokens, expiry, or reconnection, run the check in this order — it's the order that actually distinguishes the failure modes above rather than assuming the first plausible one.

  1. Force a live call. Do not read the stored status. Hit the number's own Graph endpoint and debug_token directly.
  2. Read is_valid and expires_at together. A false is_valid with a past expires_at is a natural expiry. A false is_valid with no useful expiry, or on a token type built to be long-lived, points at revocation instead.
  3. Check scope, not just validity. granular_scopes shows what the token can still do — a token can be valid and still lack a permission a specific call needs, which produces a different error than 190 and a different fix.
  4. Ask what changed at the app or business level, not the customer's. A portfolio move, a shared system-user token being rotated, or an app losing a use-case grant explains an outage affecting many numbers at once; a single customer revoking access explains one number failing alone.
  5. Only then write the customer-facing message. "Your token expired, please reconnect" is accurate for exactly one of these causes and vague enough to sound right for all of them — which is precisely how a revoked grant gets misdiagnosed as routine expiry in the first place.

None of this requires guessing. Every field involved is one Graph call away; the only discipline required is making the call before trusting the screen.

Every Meta quotation on this page was read from Meta's own documentation on 4 September 2026. Meta changes that documentation without notice; the linked pages are authoritative and this one is not.

Questions people also ask

Does a green quality rating mean my number is still connected?

No. Quality rating and connection status are usually two separate cached fields, both written the last time a background job asked Meta and not refreshed on every page view. A number can carry a GREEN rating from yesterday's check while the token that check used has since been revoked, so today's sends fail with error 190 or a permission error even though the badge next to it still reads GREEN. Treat quality rating as a description of message performance during the window it was measured, not as proof that the connection is currently live. If sending has stopped, force a live check against the number rather than trusting any field on the screen, however green it looks.

How often should a platform re-check a connection with Meta?

Meta's documentation does not prescribe an interval, so this is a design decision rather than a compliance requirement. An hourly check catches most failures within an hour, which is tolerable for a quality-rating dip but is a long time to keep telling a customer their number is fine while nothing is sending. The fix is not necessarily a faster schedule — polling more often mostly wastes calls on numbers that haven't changed — it is giving the person looking at the screen a way to force a live check on demand, and making the "last checked" timestamp visible enough that a stale one is itself a signal.

Will my customers' conversations survive a reconnection?

Reconnecting replaces the credential a platform uses to talk to Meta on the number's behalf; it does not touch the message history already stored in the platform's own database. Conversations, contacts, templates and past messages are unaffected by re-running Embedded Signup or re-entering a WABA id, phone number id and token. What resumes only after reconnection is the ability to send and receive new messages — anything that arrived while the connection was down should still be delivered once the webhook subscription is confirmed live again, though a long enough outage can mean Meta itself stopped attempting delivery on its end.

Can I test whether an access token is still valid?

Yes. Meta's Graph API `debug_token` endpoint takes the token as `input_token` and returns, among other fields, `is_valid` — described in Meta's reference as "Whether the access token is still valid or not" — plus an `expires_at` timestamp and the `granular_scopes` the token actually carries. Calling this directly is more informative than reading a send failure, because it separates "the token is invalid" from "the token is valid but lacks a scope this call needs," which read identically from the outside otherwise.

Does reconnecting a number use up a registration attempt?

Only if the reconnection path calls the registration endpoint, and it shouldn't for a number that is already registered on the Cloud API. Meta's documentation states registration requests are "limited to 10 requests per business number in a 72-hour moving window," so an automated reconnection flow that re-registers on every retry can burn through that budget on a number that never needed registering at all. Re-attaching credentials, refreshing a token, or re-running the OAuth grant are all separate from registration; only treat registration as necessary when the number has never been registered on this platform's app before.

Why did only some of my numbers stop working?

Because grants and tokens are not all scoped the same way. A customer revoking access on one WhatsApp Business Account, disconnecting one number from their end, or a per-number token expiring naturally affects only that number. A change at the app or business level — the app moving to a different portfolio, a platform-wide system user token being regenerated, or a shared credential being revoked — affects every number that depended on it at once. If the outage is partial, look at what the affected numbers have in common; if it's total, look at what changed for the app or the business itself, not for any single customer.

Keep reading

Your customers are already on WhatsApp

Free for your first 1,000 contacts, with no time limit and no card. Setting up the workspace takes minutes; connecting a number takes as long as Meta's own review of it.

Sign up with your company email address. No sales call, no onboarding fee, nothing to schedule.

Why this is safe to point your customer list at

Payments are processed by Razorpay on their own checkout — your card details are never entered on, or stored by, WabaCRM. Every inbound WhatsApp webhook is checked against its signature before it is trusted.

Tech Provider is a Meta platform access tier — not a partnership, a reseller agreement or an endorsement.