# OAuth for Social Media APIs: The Developer's Guide > Token lifetimes are where social publishing breaks. Authorization code flow, PKCE, refresh tokens and the specific reason you cannot store an access token for a post scheduled next week. **Source:** https://posteverywhere.ai/blog/social-media-oauth-guide **Author:** Jamie Partridge **Published:** 2026-09-04 --- _Last updated: September 2026._ Social media OAuth is not conceptually hard. Nearly every network implements the same authorization code flow, and once you have written it once the second is largely copy and paste. What breaks projects is what happens afterwards. Access tokens expire on wildly different schedules, refresh tokens behave inconsistently, and one design decision that seems obvious will silently destroy your scheduling feature: storing the access token alongside the scheduled post. This covers the flow, the token lifetimes that differ per platform, and the patterns that keep a publishing queue working past the first hour. ## Table of Contents 1. [The Flow, Once](#the-flow-once) 2. [PKCE and Why It Is Not Optional Any More](#pkce-and-why-it-is-not-optional-any-more) 3. [Scopes Are Not Interchangeable](#scopes-are-not-interchangeable) 4. [Token Lifetimes by Platform](#token-lifetimes-by-platform) 5. [The Scheduling Trap](#the-scheduling-trap) 6. [Refresh Token Patterns That Work](#refresh-token-patterns-that-work) 7. [When Tokens Die Anyway](#when-tokens-die-anyway) 8. [Storing Credentials Safely](#storing-credentials-safely) 9. [FAQ: Social Media OAuth](#faq-social-media-oauth) ## The Flow, Once Every network here implements the authorization code flow from [RFC 6749](https://datatracker.ietf.org/doc/html/rfc6749). Four steps. **One.** Redirect the user to the platform's authorize endpoint with your client ID, redirect URI, requested scopes and a `state` value. **Two.** They approve, and the platform redirects back to your URI with a short-lived `code` and your `state` echoed back. Verify the `state` matches what you sent. This is not optional decoration, it is your CSRF defence. **Three.** Exchange the code server-side for tokens, sending your client secret. You get an access token, usually a refresh token, an `expires_in`, and the scopes actually granted. **Four.** Call the API with `Authorization: Bearer `. The shape is identical on [X](https://docs.x.com/resources/fundamentals/authentication/oauth-2-0/authorization-code), [LinkedIn](https://learn.microsoft.com/en-us/linkedin/shared/authentication/authorization-code-flow), [Google](https://developers.google.com/identity/protocols/oauth2) and [Discord](https://docs.discord.com/developers/topics/oauth2). What differs is everything after step four. Getting to the point where you can run this flow at all is a separate problem, covered in [how to get social media API access](/blog/how-to-get-social-media-api-access). One detail people skip: **check the scopes actually granted**, not the ones you asked for. Users can decline individual permissions on several platforms, so the token you receive may not do what your code assumes. Read the granted scopes from the token response and fail loudly rather than discovering it at publish time. ## PKCE and Why It Is Not Optional Any More PKCE, from [RFC 7636](https://datatracker.ietf.org/doc/html/rfc7636), adds a one-time secret to the exchange. You generate a random `code_verifier`, send its SHA-256 hash as `code_challenge` on the authorize request, and send the original verifier when exchanging the code. It exists because an intercepted authorization code is useless without the verifier. Originally aimed at mobile and single-page apps that cannot hold a client secret, it is now required or strongly recommended nearly everywhere, and X requires it for OAuth 2.0. Implement it even on a server-side app where you technically have a secret. It costs a few lines and closes a real class of interception attack, and it is table stakes for any [social media API](/social-media-api) integration handling other people's accounts. ## Scopes Are Not Interchangeable Requesting the wrong scope is the most common cause of a 403 that looks like a bug in your code. Publishing scopes are consistently separate from read scopes, and on several platforms writing on behalf of an organization is separate again from writing as a person. LinkedIn distinguishes `w_member_social` from `w_organization_social`, and the organization one additionally requires the authenticated user to hold a specific role on the company page. Ask for the narrowest set that does the job. Over-broad scope requests get rejected in app review, and they make the consent screen more alarming, which costs you conversions at exactly the wrong moment. The per-platform requirements are mapped in our guide to [getting social media API access](/blog/how-to-get-social-media-api-access). > **Not the problem you want to own.** PostEverywhere handles OAuth, refresh and revocation across all top platforms behind one API key. [See the API docs](/developers). ## Token Lifetimes by Platform This is where the platforms diverge sharply, and where assumptions carried from one network break on the next. | Platform | Access token | Refresh | |---|---|---| | **X (Twitter)** | ~2 hours | Refresh token, rotates on use | | **Meta** | ~1 hour, exchangeable for ~60 days | Re-exchange long-lived token | | **LinkedIn** | ~60 days | Refresh token, partner-dependent | | **Google / YouTube** | ~1 hour | Refresh token, long-lived | | **TikTok** | ~24 hours | Refresh token | | **Pinterest** | ~30 days | Refresh token | | **Telegram** | Never expires | Static bot token | | **Discord webhook** | Never expires | Static URL | Two things follow. **There is no safe universal assumption.** Code that works against Pinterest's 30-day token will fail against X's two hours. And **the easy platforms are easy here too**: [Telegram](/telegram-scheduler) and Discord webhooks hold static credentials that never expire, which is why they are the fastest to prototype against, as our [Telegram](/blog/post-to-telegram-api) and [Discord](/blog/post-to-discord-api) guides show. Meta's model deserves a note because it is genuinely different: you exchange a short-lived token for a long-lived one rather than using a conventional refresh token, and the long-lived token is itself re-exchanged before expiry. ## The Scheduling Trap Here is the design mistake that costs people a weekend. You build a scheduler. A user connects their account, you receive an access token, and you store it on the scheduled post so the worker can publish later. It works perfectly in testing, because you always test by scheduling something a few minutes out. Then a user schedules a post for next Tuesday. Tuesday arrives, the worker fires, and the token expired six days ago. **Never store an access token with a scheduled job.** Store the *refresh* token against the account, and mint a fresh access token at publish time. On a platform with a two-hour token like X, anything scheduled more than two hours ahead is otherwise guaranteed to fail. The same applies to retries. A post that fails and retries four hours later needs a fresh token, not the one captured when the job was created. Our [scheduling API guide](/blog/social-media-scheduling-api-guide) covers the surrounding queue design, and [automating posting](/blog/automate-social-media-posting-api) covers the worker side. ## Refresh Token Patterns That Work **Refresh at publish time, not on a timer.** A cron job refreshing every token every hour wastes calls and multiplies failure modes. Mint on demand, immediately before you need it. **Handle rotation.** Several platforms, X included, issue a new refresh token each time you use the old one and invalidate the previous. If you do not persist the new one atomically, the next refresh fails and the account disconnects. This is a genuinely common bug and its symptom is "it worked for a while and then stopped". **Serialise refreshes per account.** Two workers refreshing the same account concurrently will race, and with rotation one of them writes a refresh token the platform has already invalidated. Take a lock per account. **Never refresh in a loop on failure.** A rejected refresh token will not become valid on the third attempt. Mark the account as needing reconnection and stop, or you will hit [rate limits](/blog/social-media-api-rate-limits) on top of an auth failure. ## When Tokens Die Anyway Even correct refresh logic loses tokens, because users do things. Changing a password invalidates tokens on several platforms. So does revoking your app in account settings, losing the Page or organization role your permissions depended on, or a platform-side security action. Meta permissions can also require periodic re-review, and LinkedIn's versioned APIs sunset on a schedule, so an untouched integration eventually breaks by itself. Design for it rather than treating it as exceptional. Detect the auth failure, mark the connection as broken, stop retrying, and tell the user which account needs reconnecting and why. The worst outcome is silently failing to publish while appearing healthy, which is what a naive retry loop produces. Our [multi-account view](/multi-account-management) exists largely to surface exactly this state. ## Storing Credentials Safely Refresh tokens are long-lived credentials to somebody else's account. Treat them accordingly. Encrypt at rest with a key that is not in the same store as the data. Never log them, including in error handlers, which is where they most often leak. Never expose them to the browser: the exchange and every refresh belong server-side. Scope database access so the publishing worker can read them and nothing else can. A [unified API](/blog/best-social-media-apis) removes most of this surface, but if you are building it yourself, revoke properly on disconnect. Deleting your row leaves a live token on the platform side, which is a bad answer to give a user asking whether you still have access. Call the platform's revocation endpoint. If none of this is your product's core value, it is reasonable not to build it. A [unified publishing API](/social-media-api) holds one credential per customer and handles every platform's token behaviour behind it. Teams building agents typically use that or [MCP](/blog/social-media-api-vs-mcp) rather than implementing eight refresh strategies, which our [agent framework comparison](/blog/ai-agent-frameworks-for-social-media) goes into. > **One key instead of eight token flows.** Publishing across all top platforms, auth handled. From $9/mo with a 7-day trial and card required. [See pricing](/pricing) or [browse connectors](/connectors). ## FAQ: Social Media OAuth ### Why do my scheduled posts fail with an auth error? Almost always because an access token was stored alongside the scheduled job and expired before the job ran. Store the refresh token against the account instead and mint a fresh access token at publish time. On X, where access tokens last around two hours, anything scheduled further ahead than that will otherwise fail. ### How long do social media access tokens last? It varies enormously. X is around two hours, Google and Meta around one hour, TikTok around 24 hours, Pinterest around 30 days, and LinkedIn around 60 days. Telegram bot tokens and Discord webhook URLs never expire. There is no safe universal assumption, so handle each platform's lifetime explicitly. ### What is PKCE and do I need it? PKCE, defined in RFC 7636, adds a one-time `code_verifier` to the authorization code exchange so an intercepted code cannot be redeemed alone. It was designed for apps that cannot hold a client secret, but it is now required or strongly recommended nearly everywhere, and X requires it for OAuth 2.0. Implement it even server-side. ### What is a rotating refresh token? A refresh token that is replaced each time it is used, with the previous one invalidated. Several platforms including X do this. If your code does not persist the new refresh token atomically, the following refresh fails and the account disconnects, which typically presents as an integration that worked for a while and then stopped. ### Why did my token stop working without my code changing? Users invalidate tokens by changing passwords, revoking app access, or losing the Page or organization role your permissions relied on. Platforms also invalidate tokens for security reasons, Meta permissions can require re-review, and LinkedIn's versioned APIs sunset on a schedule. Detect it, mark the connection broken, and prompt a reconnect rather than retrying. ### Can I store an access token in the browser? No. Access and refresh tokens are credentials to a user's social account and belong server-side only. The code exchange requires your client secret, which must never reach the browser, and every refresh should happen server-side too. Use PKCE for public clients that genuinely cannot hold a secret. ### Do I need separate scopes to post as a company page? Usually yes. LinkedIn separates `w_member_social` from `w_organization_social`, and the organization scope additionally requires the authenticated user to hold an administrator or content admin role on the page. Meta similarly distinguishes Page permissions from user permissions. Request the narrowest scope that does the job. ### How should I handle a failed token refresh? Stop, do not retry in a loop. A rejected refresh token will not become valid on a later attempt, and retrying compounds an auth failure with a rate limit problem. Mark the connection as needing reconnection, surface which account and why, and stop attempting to publish for it until the user reconnects.