DEV Community

Cover image for Meta access token expired? Why it lasts 60 days and how to refresh it
Francisco José Fernández
Francisco José Fernández

Posted on Originally published at planvortex.com

Meta access token expired? Why it lasts 60 days and how to refresh it

An Instagram access token issued by Meta lasts 60 days. PlanVortex renews it automatically a day before it expires, so in normal use you never notice. You notice when that renewal fails: the account is flagged as down, stops retrying on its own, and your product gets a notification so it can ask the user to reconnect it.

Not every Meta token works like that. A Facebook Page token has no expiry date, a system user token lasts as long as you choose when you create it, and WhatsApp has a token of its own.

How long does a Meta access token last?

It depends on which token. These are the ones an integration meets:

Token How long it lasts How it is renewed
Instagram user token (Instagram Login) 60 days ig_refresh_token, once it is 24 hours old
Facebook user token, long-lived About 60 days Through the login flow again
Facebook Page token, from a long-lived user token No expiration date It does not need renewing
System user token (Business Manager) 60 days or never, chosen at creation fb_exchange_token with set_token_expires_in_60_days=true
WhatsApp token from the App Dashboard A few hours Generate a new one

For an Instagram account, 60 days is the window Meta documents for the long-lived token you get when connecting it, and it is fixed: you cannot request a longer one and there is no paid plan that extends it.

// src/orm/social_apis/instagram/index.ts
const expiresDate = new Date();
expiresDate.setSeconds(expiresDate.getSeconds() + (60 * 60 * 24 * 60)); // 60 days
Enter fullscreen mode Exit fullscreen mode

That expiresDate is the only thing deciding whether an account needs renewing. There is no heuristic and no safety margin in that calculation: it is exactly 60 days from the instant Meta handed the token over, counted in seconds.

Why does the Instagram token expire when a Page token does not?

Because they stand for different things. The Instagram token is a user token: it represents the person who authorized the app, and Meta makes the integration come back every 60 days to show that person is still behind the permission. An expiry also limits the damage of a token that leaks (it stops working on its own, without anyone having to revoke it by hand).

A Page token generated from a long-lived user token represents the Page, and Meta says it has no expiration date and is only invalidated "under certain conditions". Meta's debugging guide lists what invalidates a token before its time: the person logging out of the app, changing their password or revoking the app's authorization. That is why PlanVortex renews Instagram accounts on a cycle and leaves Facebook Pages out of it.

Either way, whoever integrates the API has to treat a dead token as a normal part of the account lifecycle, whether it died on a date or because something invalidated it.

How is a token renewed before it expires?

With a job that runs once an hour and renews any account whose token is going to expire in less than 24 hours. The hourly cadence exists precisely so there is room to spare against that one-day window:

// src/domain/repositories/account/index.ts: "Refrescamos con 1 dia de diferencia (requerido por instagram)"
const now = new Date();
now.setDate(new Date().getDate() + 1);
const account_match = {
    deleted: false,
    error_code: 0,
    "access_data.expires_in": { $lte: now },
};
Enter fullscreen mode Exit fullscreen mode
// src/jobs/index.ts
"refresh-tokens": {
    cron: "7 * * * *", // every hour, on minute 7
    run: refreshTokens,
},
Enter fullscreen mode Exit fullscreen mode

The renewal itself is a call to Meta's refresh_access_token with grant_type=ig_refresh_token, which returns a new token with another 60 days ahead of it. As long as that call succeeds, the cycle repeats on its own and the account never learns its token changed.

How do you get a Meta access token that never expires?

There are two ways, and Meta is closing the second one.

A Page access token. Exchange the short-lived user token from the login dialog for a long-lived one, then ask for the Page tokens with it:

GET /oauth/access_token?grant_type=fb_exchange_token&client_id={app-id}&client_secret={app-secret}&fb_exchange_token={short-lived-user-token}
GET /{user-id}/accounts?access_token={long-lived-user-token}
Enter fullscreen mode Exit fullscreen mode

Each Page in the second response carries its own access_token, and that one has no expiration date. Two conditions: the person asking must have a role on the Page, and the user token has to be the long-lived one, which is the input Meta asks for.

A system user token. In Business Manager (Business settings, then Users, then System users) you create a system user, give it the app and the assets, and generate a token. When you generate it you choose the expiry: never, or 60 days. Meta recommends the 60-day one as "a security best practice" and says that "all integrations should adopt expiring tokens". For businesses the requirement already applies to, asking for a non-expiring token returns an error. So the permanent system user token that many tutorials recommend may not be available to you.

A 60-day system user token is refreshed with the same exchange call plus one parameter:

GET /oauth/access_token?grant_type=fb_exchange_token&client_id={app-id}&client_secret={app-secret}&set_token_expires_in_60_days=true&fb_exchange_token={current-token}
Enter fullscreen mode Exit fullscreen mode

The response brings a new token and its expires_in. Meta's advice is to refresh within the 60 days; once it lapses, you have to generate a new one.

Which token should you use for the WhatsApp Cloud API?

The token on the API Setup screen is only good for testing. The App Dashboard gives you a user token that, in Meta's words, expires quickly: you would have to generate a new one every few hours, which is enough to send a first test message.

For anything after that, it depends on whose number it is:

  • For your own business's number, a system user token, with everything said above about choosing 60 days or never.
  • For your customers' numbers (Meta calls you a Tech Provider), a Business Integration System User access token, one per customer, which you get by implementing Embedded Signup and exchanging the code the flow returns. Meta presents it as the way to reach each customer's assets without asking them to authenticate again. That is how PlanVortex connects WhatsApp: the customer goes through Embedded Signup and the code is exchanged on the server.

How do you check when a Meta token expires?

Meta's debug_token endpoint (the one behind the Access Token Debugger in its developer tools) returns, among other fields, expires_at and data_access_expires_at:

GET /debug_token?input_token={token-to-check}&access_token={app-id}|{app-secret}
Enter fullscreen mode Exit fullscreen mode

They are two different dates. expires_at is when the token stops working. data_access_expires_at is the data access window, 90 days from the person's last activity: past it, the person can still log in, but the app loses access to their data until they re-authorize the permissions. PlanVortex calls debug_token when it connects a WhatsApp account, to read which assets the person granted and when the token expires.

Do tokens last the same on every network?

No: each network forces its own deadline, and renewal code that treats them all alike ends up with dead accounts.

Network access_token lifetime Source
Instagram (Meta) 60 days instagram/index.ts
Facebook Page (Meta) No expiration date absent from getAccountsToRefresh
LinkedIn 60 days (5,184,000 s) __tests__/social/contract/linkedin.test.ts
TikTok 24 hours __tests__/social/contract/youtube.test.ts
YouTube (Google) 1 hour youtube.types.d.ts

YouTube is the extreme case: with a token that lasts an hour, the hourly job is not enough on its own, so its SDK also renews on demand before every call. Instagram and LinkedIn, at 60 days, live comfortably inside the hourly pass with room to spare. Facebook Pages sit at the other end: their token has no date, so they never enter the pass. The practical consequence is that the renewal code cannot be generic: each SDK knows how long its own token lasts and which field of the response carries the new expiry, because they are not even named the same across networks.

What happens the day the token really expires?

The automatic renewal fails, and from that point the account stops trying on its own. This only happens if something prevented the 60-day cycle from closing in time (the user revoked the permission from Meta, the app lost its approved use case, or the job had been unable to run for more than a day), never from the mere passing of 60 days with the system working normally.

When the renewal call fails, three things happen at once:

  1. The account is flagged with an error code (error_code: 707, "Error while sincronyzing").
  2. That code takes it out of the job's next pass: the query selecting accounts to renew requires error_code: 0, so a downed account is not retried every hour against a token that no longer works.
  3. A change_state_account change is queued and notified by signed webhook to the client apps that have that client registered.

The job processes every account due for renewal in that pass in parallel, grouped by client. That matters because a client with several Instagram accounts can have one fail and the rest renew without trouble in the same pass: one failure neither blocks nor delays the others, and the webhook that reaches the client app carries that client's changes grouped together, not one message per account.

// src/jobs/RefreshTokens.ts
} catch (e) {
    await AccountModel.updateOne({ _id: account._id }, { $set: { error_code: CustomError.ERROR_CODE_707.key } });
    addAccountChange(grouped, account, {
        field: ALLOWED_WEBHOOKS_NOTIFICATIONS.change_state_account,
        id_account: account._id.toString(),
        id_organization: account.id_organization._id.toString(),
        social_network: account.social_network,
    });
}
Enter fullscreen mode Exit fullscreen mode

How does your product find out an account disconnected?

Through a webhook, as soon as the job detects the failure. If your app integrates the PlanVortex API, the change_state_account webhook reaches you with id_account, id_organization and social_network, signed like every other outbound PlanVortex webhook.

Finding out through a publishing error is too late: the user scheduled content for three days out assuming the account was still connected, and the failure does not surface until that content tries to go out. With the webhook, your product can warn the user the same day the account went down, with plenty of time to reconnect it before anything is left unpublished.

What should you have settled if you integrate Meta accounts into your product?

Four things, before assuming a connected account is still connected:

  • A disconnected account is a state, not an error. It happens in normal use (the user revokes the permission from Meta's settings, changes their password, the app loses access) and your product has to be able to show that without treating it as an exception.
  • Listen to the webhook as well as to the publishing error. It is the difference between warning on the day the account goes down and warning on the day something scheduled fails because of it. The code that receives it (signature verification over the raw body included, which is where everyone trips) is in the Node.js and Python guides.
  • Reconnection is always manual. Neither PlanVortex nor Meta can renew a permission the user has revoked; the flow to connect the account is the same one as the first time, authorization code included.
  • The message to the user cannot be generic, because networks do not fail the same way. A downed TikTok account and a downed Instagram account share neither the most likely cause nor the urgency: with a 24-hour token, a TikTok failure is almost always a one-off problem with the job; with a 60-day one, an Instagram failure almost always means the user revoked the permission.

Top comments (0)