Skip to content

X API 403 Forbidden: what causes it and how to fix it

A 403 Forbidden from the official X API means your keys work but X refuses this request; the body names the cause, from an app not moved to pay-per-use to likes and follows removed on April 20, 2026.

Short answer: Log the full response body and match its text: client-not-enrolled means moving the app to pay-per-use, a Read-only app or an app-only token blocks posting, and follows, likes and quote posts now need an Enterprise contract. To read public posts and profiles without these 403s, use Sorsa API: one API key, from $0.014 per 1,000 tweets, and 100 free requests.

Endpoint
POST /tweet-info-bulk
Page size
100 tweets per request
Price
from $0.014 per 1,000

#What a 403 Forbidden means on the official X API

A 403 is X saying it knows who you are and still won't do this. X's response code reference (opens in a new tab) defines it as valid authentication without permission for the resource or action. A 401 is the other case: the credentials themselves are missing or wrong.

Every cause of a 403 returns the same status, so the status alone tells you nothing. The body does. X answers in Problem Details JSON with a title, a detail and a type, and enrollment errors add a reason. Log the whole response and read the fields yourself:

Anatomy of an official X API 403 response: status line, x-transaction-id header, detail, reason and type HTTP/1.1 403 Forbidden x-transaction-id: 7f3a91c2e05b44d1 { "title": "Client Forbidden", "detail": "When authenticating requests to the X API v2 endpoints, you must use keys and tokens from a developer App that is attached to a Project. You can create a ..." "reason": "client-not-enrolled", "required_enrollment": "...", "type": ".../problems/client-forbidden" } 1 Status 403 Understood and refused. 2 x-transaction-id The ID X staff ask for when you report a 403. 3 detail X's words. It may say Project when the real fix is moving the app to pay-per-use. 4 reason and type The cause in a form your code can match. Log them.
The status says "refused"; the reason, type and detail say why. Log all of them, plus x-transaction-id.

Two habits save hours. Log the body and the x-transaction-id response header on every 403, because X staff ask for both before they look at a case on the developer forum. And don't retry: a 403 comes back the same until something changes in the app, the token, the post or the account. Retries belong to 429 and 5xx errors, which our guide to Twitter API error codes covers code by code.

#Find your 403 by its error text

Match the text in your 403 body against this table: each row names the cause and the first fix. Most rows come from writes; reads fail on enrollment, scopes, protected accounts and old v1.1 calls.

Text in the 403 bodyWhere it appearsCauseFirst fix
"reason": "client-not-enrolled", title "Client Forbidden"Any v2 callApp on the retired Free package, standalone or outside a ProjectMove the app to pay-per-use, make new keys
"Authenticating with OAuth 2.0 Application-Only is forbidden for this endpoint"Posting, /2/users/me, bookmarks, DMsApp-only Bearer tokenUse a user-context token
A bare "detail": "Forbidden"GET /2/users/meToken without tweet.read and users.readAdd both scopes, authorize again
Any 403 on a like, follow or quote_tweet_id/2/users/{id}/likes, /following, quote postsRemoved from self-serve on April 20, 2026Enterprise contract only
"Reply to this conversation is not allowed because you have not been mentioned..."RepliesSummoned-reply rule since February 23, 2026Reply only when mentioned or quoted
A 403 on a post with two or more $cashtagsPOST /2/tweetsSelf-serve limit of 1 cashtag per postKeep one cashtag
"This user is not allowed to post a video longer than N minutes."Posts with videoThe posting account's video capTrim the video
"You are not permitted to perform this action."POST /2/tweetsX refused this post: a rule above, posting limits or a spam checkCheck the content, slow down
"Your account is not permitted to access this feature."POST /2/tweetsWrite restriction on the posting accountStop retries, check the account on x.com
Type not-authorized-for-resourceReading a post or userProtected or private contentNone: only approved followers can read it
A dm- type, or "This operation is not permitted."Direct messagesThe recipient's settings or a safety checkRead the type; often nothing to fix
Code 453, "You currently have access to a subset of X API V2 endpoints..."v1.1 endpointsv1.1 is outside your accessCall the v2 endpoint
  • "reason": "client-not-enrolled", title "Client Forbidden"

    Where it appears
    Any v2 call
    Cause
    App on the retired Free package, standalone or outside a Project
    First fix
    Move the app to pay-per-use, make new keys
  • "Authenticating with OAuth 2.0 Application-Only is forbidden for this endpoint"

    Where it appears
    Posting, /2/users/me, bookmarks, DMs
    Cause
    App-only Bearer token
    First fix
    Use a user-context token
  • A bare "detail": "Forbidden"

    Where it appears
    GET /2/users/me
    Cause
    Token without tweet.read and users.read
    First fix
    Add both scopes, authorize again
  • Any 403 on a like, follow or quote_tweet_id

    Where it appears
    /2/users/{id}/likes, /following, quote posts
    Cause
    Removed from self-serve on April 20, 2026
    First fix
    Enterprise contract only
  • "Reply to this conversation is not allowed because you have not been mentioned..."

    Where it appears
    Replies
    Cause
    Summoned-reply rule since February 23, 2026
    First fix
    Reply only when mentioned or quoted
  • A 403 on a post with two or more $cashtags

    Where it appears
    POST /2/tweets
    Cause
    Self-serve limit of 1 cashtag per post
    First fix
    Keep one cashtag
  • "This user is not allowed to post a video longer than N minutes."

    Where it appears
    Posts with video
    Cause
    The posting account's video cap
    First fix
    Trim the video
  • "You are not permitted to perform this action."

    Where it appears
    POST /2/tweets
    Cause
    X refused this post: a rule above, posting limits or a spam check
    First fix
    Check the content, slow down
  • "Your account is not permitted to access this feature."

    Where it appears
    POST /2/tweets
    Cause
    Write restriction on the posting account
    First fix
    Stop retries, check the account on x.com
  • Type not-authorized-for-resource

    Where it appears
    Reading a post or user
    Cause
    Protected or private content
    First fix
    None: only approved followers can read it
  • A dm- type, or "This operation is not permitted."

    Where it appears
    Direct messages
    Cause
    The recipient's settings or a safety check
    First fix
    Read the type; often nothing to fix
  • Code 453, "You currently have access to a subset of X API V2 endpoints..."

    Where it appears
    v1.1 endpoints
    Cause
    v1.1 is outside your access
    First fix
    Call the v2 endpoint

#client-not-enrolled: move the app to pay-per-use

client-not-enrolled means the app behind your keys isn't enrolled in the access the endpoint needs, so every v2 call fails, reads and writes alike. In 2026 the cause is nearly always an app left on the legacy Free package, shown as a standalone app or sitting outside a Project.

403 body
{
  "detail": "When authenticating requests to the X API v2 endpoints, ...",
  "reason": "client-not-enrolled",
  "required_enrollment": "Appropriate Level of API Access",
  "title": "Client Forbidden",
  "type": "https://api.x.com/2/problems/client-forbidden"
}

The detail talks about Projects, but X staff describe the 2026 cause plainly: the Free tier is deprecated, and an app left on it gets this error until it moves to pay-per-use, as one forum thread on stuck standalone apps (opens in a new tab) shows. The same answer notes that the sign-in endpoints stay free under pay-per-use.

If the body has no reason field, enrollment is not your problem. X staff have said outright that a plain 403 on posting is a different error, so moving projects or regenerating keys won't touch it.

  1. Open the Developer Console. Sign in at console.x.com and find the app whose keys your code uses.

  2. Check its package. It needs pay-per-use and the Production environment; a standalone app or one under a legacy Free, Basic or Pro project fails.

  3. Move it to pay-per-use. Add a payment method and credits; reads are billed per item, $0.005 per post and $0.010 per user, as our breakdown of X API pricing shows.

  4. Make new keys and tokens. Regenerate the Bearer token and the access tokens, then deploy them: old ones keep the old enrollment.

  5. Ask on the forum if the console can't move it. Post the numeric App ID, the endpoint and the full body; X staff have moved stuck apps by hand.

#Likes, follows and quote posts: removed from self-serve

Since April 20, 2026, the official X API no longer likes, follows or quote-posts for any self-serve app, so these calls return 403 however the app is set up. X's pricing update (opens in a new tab) lists POST and DELETE /2/users/{id}/likes, POST and DELETE /2/users/{id}/following, and any post sent with quote_tweet_id, and the create-post reference (opens in a new tab) now says quote-posting needs an Enterprise plan.

Self-serve covers pay-per-use and the legacy Free, Basic and Pro plans. Likes and follows had already left the Free tier on August 22, 2025, so a bot built before then may have been failing for more than a year.

No setting brings them back. New tokens, the like.write or follows.write scopes and more credits change nothing; only an Enterprise contract, from about $42,000 a month, still writes them. The timeline shows when each of these 403s appeared:

Six official X API changes from August 2025 to October 2026 that created new 403 errors Aug 22, 2025 Likes and follows leave the Free tier Feb 23, 2026 Replies only when the author summoned you Sep 1, 2026 Video length capped by the posting account Feb 6, 2026 Pay-per-use opens; Free closes to new apps Apr 20, 2026 Follows, likes and quote posts leave every self-serve tier Oct 26, 2026 DM refusals get their own types
Six changes in 14 months created most of the 403s developers hit today. April 20, 2026 removed three actions outright.

Pick what your code does to see whether self-serve access still allows it, the 403 it returns and the fix:

What does your code do on the official X API?

  • Read posts, profiles or search

    Open on pay-per-use

    Cost
    $0.005 per post, $0.010 per user, up to 3M post reads a month
    The 403
    client-not-enrolled when the app isn't on pay-per-use; not-authorized-for-resource on protected accounts
    Fix
    Move the app to pay-per-use in console.x.com and make new keys. Sorsa API reads the same public data with one API key and no app.
  • Read your own bookmarks, likes or followers

    Open, user token only

    Cost
    $0.001 per item as Owned Reads, since April 20, 2026
    The 403
    "Unsupported Authentication" with an app-only Bearer token
    Fix
    Call it with a user-context token: OAuth 1.0a or OAuth 2.0 with PKCE.
  • Publish a post

    Open on pay-per-use

    Cost
    $0.015 per post, $0.20 with a URL
    The 403
    "You are not permitted to perform this action." or a Read-only app
    Fix
    Set the app to Read and write, make new access tokens, post with a user token.
  • Reply to a post

    Only when summoned

    Since
    February 23, 2026, self-serve only
    The 403
    "Reply to this conversation is not allowed because you have not been mentioned or otherwise engaged by the author"
    Fix
    Reply only to posts that @mention your account or quote one of its posts.
  • Quote a post

    Removed from self-serve

    Since
    April 20, 2026: quote_tweet_id needs Enterprise
    The 403
    Any post that carries quote_tweet_id
    Fix
    Post a link to the post instead, or move to an Enterprise contract.
  • Like or unlike

    Removed from self-serve

    Since
    April 20, 2026; off the Free tier since August 22, 2025
    The 403
    POST and DELETE /2/users/{id}/likes
    Fix
    None on self-serve. Only an Enterprise contract still likes through the API.
  • Follow or unfollow

    Removed from self-serve

    Since
    April 20, 2026; off the Free tier since August 22, 2025
    The 403
    POST and DELETE /2/users/{id}/following
    Fix
    None on self-serve. Only an Enterprise contract still follows through the API.
  • Post with $cashtags

    One per post

    Rule
    Self-serve posts carry at most 1 cashtag
    The 403
    A post with a second $TICKER
    Fix
    Keep one cashtag and write the other tickers without the $ sign.
  • Post a long video

    Capped by the account

    Rule
    20 minutes by default, 125 minutes on X Premium, documented September 1, 2026
    The 403
    "This user is not allowed to post a video longer than N minutes."
    Fix
    Trim the video. A bigger API budget doesn't raise the cap.
  • Send a direct message

    Refused per recipient

    Since
    October 26, 2026: each refusal has its own type
    The 403
    dm-recipient-not-accepting, dm-sender-not-verified and other dm- types, or "This operation is not permitted."
    Fix
    Read the type. Most refusals come from the recipient's settings and can't be fixed in code.
  • Call a v1.1 endpoint

    Mostly closed

    Rule
    Self-serve access covers v2 and a few v1.1 endpoints, such as media upload
    The 403
    Code 453: "You currently have access to a subset of X API V2 endpoints and limited v1.1 endpoints only"
    Fix
    Call the v2 endpoint: in tweepy, tweepy.Client instead of tweepy.API.

Self-serve means pay-per-use and the legacy Free, Basic and Pro plans. From X's changelog and docs, checked November 2, 2026.

What self-serve access on the official X API allows in November 2026, action by action.

A quote post has a plain workaround: post the link to the post instead of quote_tweet_id. X bills a post with a URL at $0.20, against $0.015 for a plain post.

#403 on POST /2/tweets: tokens, replies, accounts

Posting is open on pay-per-use at $0.015 per post, so a 403 on POST /2/tweets comes from the token, the post's content or the account behind it. Check them in that order.

A Read-only app or an app-only token

A post needs a user-context token: OAuth 1.0a with the app's consumer keys plus the account's access token and secret, or an OAuth 2.0 token from the PKCE flow with the tweet.write scope. An app-only Bearer token gets this answer:

403 body
{
  "title": "Unsupported Authentication",
  "detail": "Authenticating with OAuth 2.0 Application-Only is forbidden ...",
  "status": 403
}

New apps start with Read permission. Switch the app's user authentication settings to Read and write, then make a new access token and secret: tokens issued before the change keep Read only.

"Reply to this conversation is not allowed"

Since February 23, 2026, self-serve apps may reply only when the original post's author summoned the replying account, by mentioning it or quoting one of its posts, according to X's changelog (opens in a new tab). Any other reply returns 403 with "Reply to this conversation is not allowed because you have not been mentioned or otherwise engaged by the author", even though the same reply goes through on x.com. Build reply bots on mentions: read the posts that mention your account and answer those. A summoned reply also costs less on X, $0.010 against $0.015.

One cashtag per post, and video length

Self-serve posts may carry one $cashtag, and a second one makes X refuse the post; in one forum case, removing the extra cashtag was the whole fix. Video caps follow the posting account rather than your API spend: 20 minutes by default and 125 minutes on X Premium. A longer video returns "This user is not allowed to post a video longer than 20 minutes."

"You are not permitted to perform this action"

This about:blank 403 is X's catch-all refusal for one post. Go through the rules above first: a reply nobody summoned, a second cashtag, a quote. If none applies, the account ran into posting limits or a spam check: slow down, stop posting duplicate text and try one post by hand on x.com.

"Your account is not permitted to access this feature"

This text points at the posting account, not your app. In a September 2026 forum case it began right after a batch of automated posts, reads kept working, and X staff described an account-level write restriction rather than a missing permission or credits. Stop the automation, check whether the account can post on x.com, and post the x-transaction-id on the developer forum if only the API fails.

#403 on reads: scopes, protected accounts, v1.1

A read returns 403 when the app, the token or the target account is the problem, and only protected accounts are truly off limits. client-not-enrolled blocks reads as well; the other read causes are these:

  • GET /2/users/me with a bare "Forbidden". The user token lacks tweet.read and users.read. Add both to the authorize URL and send the user through it again: existing tokens keep their old scopes.
  • Your own bookmarks, likes and followers. These Owned Reads cost $0.001 per item and need a user-context token; an app-only token gets "Unsupported Authentication".
  • Protected posts and accounts. They come back with the type not-authorized-for-resource, which X's reference describes as no access to private or protected content. In a batch lookup the same type arrives inside errors of a 200 response, next to the posts you can read.
  • v1.1 endpoints. Self-serve access covers v2 and a few v1.1 endpoints such as media upload, and the rest return code 453. In tweepy, tweepy.API calls such as update_status hit v1.1; tweepy.Client calls v2.

Apart from protected accounts, none of these causes depends on the data itself: they come from the app, the package and the token, which is why reading public data through another route avoids them.

Libraries hide the body by default: tweepy raises tweepy.errors.Forbidden: 403 Forbidden, and twitter-api-v2 throws "Request failed with code 403". Both keep the response, so print it before you guess:

import tweepy

client = tweepy.Client(
    consumer_key="API_KEY", consumer_secret="API_SECRET",
    access_token="ACCESS_TOKEN", access_token_secret="ACCESS_SECRET",
)

try:
    client.create_tweet(text="Testing the posting path")
except tweepy.errors.Forbidden as e:
    print(e.api_messages)  # X's detail text
    print(e.response.headers.get("x-transaction-id"))
    print(e.response.text)  # the whole body: reason, type, title

Then match the text against the table above. A loop that retries a 403 only burns time: retry 429 and 5xx responses with backoff, and fix a 403 before you send the request again.

Good to know

X's detail can mislead. The client-not-enrolled text asks for a Project even when the app already sits in one and the real fix is the move to pay-per-use, and "You are not permitted to perform this action" covers several different refusals. Trust reason and type first, and the request you sent second.

#Read public X data without 403 errors: use Sorsa API

Most read 403s come from the official X API's setup: the app, its package, its tokens and their scopes. To read public posts, profiles, followers and search results without that setup, use Sorsa API, a Twitter (X) API and scraper: one API key in an ApiKey header, with no developer app, enrollment or OAuth, so none of the read causes above can happen.

Here is what a reader of public data gets on each side:

What matters for readsSorsa APIOfficial X API
SetupOne API keyDeveloper app, pay-per-use package, OAuth tokens
Price per 1,000 postsfrom $0.014$5.00
Speed20 requests per second on every planPer-endpoint 15-minute windows
Monthly ceilingYour plan, up to 10M requests, plus extra requests3M post reads, then Enterprise
Your X accountNever involvedTokens tied to an account
  • Setup

    Sorsa API
    One API key
    Official X API
    Developer app, pay-per-use package, OAuth tokens
  • Price per 1,000 posts

    Sorsa API
    from $0.014
    Official X API
    $5.00
  • Speed

    Sorsa API
    20 requests per second on every plan
    Official X API
    Per-endpoint 15-minute windows
  • Monthly ceiling

    Sorsa API
    Your plan, up to 10M requests, plus extra requests
    Official X API
    3M post reads, then Enterprise
  • Your X account

    Sorsa API
    Never involved
    Official X API
    Tokens tied to an account
  1. Get a key. Sign up and copy the key from the dashboard; 100 free requests come with it, no card needed.

  2. Read posts by ID. POST /tweet-info-bulk (opens in a new tab) takes up to 100 post IDs or links per request and returns each post with its full author profile:

    Terminal
    curl -X POST "https://api.sorsa.io/v3/tweet-info-bulk" \
      -H "ApiKey: YOUR_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{"tweet_links": ["1853412966240727289", "1853398071322091822"]}'
    
    Response200 OK
    {
      "tweets": [
        {
          "id": "1853412966240727289",
          "full_text": "Shipped the new export page today.",
          "created_at": "2026-10-30T14:02:11Z",
          "lang": "en",
          "likes_count": 184,
          "retweet_count": 22,
          "reply_count": 17,
          "quote_count": 3,
          "view_count": 9120,
          "is_reply": false,
          "user": {
            "id": "1592041833471827968",
            "username": "lena_ships_daily",
            "display_name": "Lena Ships",
            "followers_count": 4210,
            "verified": false,
            "protected": false
          }
        }
      ]
    }
    
  3. Search instead of reading timelines. POST /search-tweets (opens in a new tab) takes X's search operators, returns about 20 posts per page and pages through next_cursor until it comes back empty:

    Python
    import requests
    
    URL = "https://api.sorsa.io/v3/search-tweets"
    HEADERS = {"ApiKey": "YOUR_API_KEY"}
    
    body = {"query": "from:lena_ships_daily", "order": "latest"}
    posts = []
    while True:
        r = requests.post(URL, headers=HEADERS, json=body, timeout=30)
        r.raise_for_status()  # 403: missing key or no requests left
        data = r.json()
        posts += data.get("tweets", [])
        if not data.get("next_cursor"):
            break
        body["next_cursor"] = data["next_cursor"]
    print(len(posts), "posts")
    
  4. Watch your own balance. GET /key-usage-info (opens in a new tab) shows the requests left on the key and never counts against them.

Sorsa API has a short error list of its own. It returns 403 when the ApiKey header is missing ("missing key in request header") or when the key's requests are used up, and 401 when the key is wrong. A failed request is never charged.

Once the 403s are gone, price decides the rest:

Posts read

Official X API

$5,000

for 1M posts

$0.005 per post

Sorsa API

from$14

for 1M posts

from $0.014 per 1,000

Sorsa API reads public data only and never posts, likes or follows, so your X account is never at risk. To publish, keep a user-context token on the official X API for the writes and read everything through Sorsa API; our guide to the Twitter API without a developer account walks through that setup.

#Fix the write, move the reads

A 403 from the official X API names its cause in the body. Fix enrollment, tokens or the post's content where X still sells the action, and stop retrying likes, follows and quote posts, which left self-serve on April 20, 2026. For reading public posts and profiles, Sorsa API needs one API key and no app setup, costs from $0.014 per 1,000 tweets, and the first 10K tweets fit in the 100 free requests every account gets.

#FAQ

What is the difference between 401 and 403 on the official X API?

A 401 means X didn't accept your credentials: the keys are missing, mistyped, revoked or signed wrong. A 403 means X accepted them and still refuses the request, because of the app's enrollment, the token's permissions, the post's content or the account. Regenerating keys fixes many 401 errors and almost no 403s.

Should I retry a 403 from the official X API?

No. A 403 repeats until something changes: the app's package, the token, the post or the account's standing. Retry only 429 and 5xx responses, with backoff. For a 403, log the body and the x-transaction-id header, match the text, fix the cause, then send the request once.

Is the official X API free in 2026?

No. The Free tier closed to new apps on February 6, 2026, X staff now call it deprecated, and pay-per-use bills every read: $0.005 per post and $0.010 per user. Only the sign-in endpoints stay free. Our answer to is the Twitter API free covers what is left.

Which API should I use to read X data without 403 errors?

Use Sorsa API to read public X data without 403 errors: it takes one API key and no developer app, enrollment or OAuth, so the setup behind most read 403s doesn't exist. It returns 100 posts per batch request from $0.014 per 1,000, at 20 requests per second on every plan, with 100 free requests to start.

Does Sorsa API return 403 errors?

Yes, in two cases. Sorsa API returns 403 when the ApiKey header is missing, with the message "missing key in request header", and when the key's requests are used up; a wrong key gets 401 instead. An upgrade or extra requests restore access at once, and failed requests are never charged.

By the Sorsa API team. We build and run Sorsa API, the read-only Twitter/X API and scraper, in production since 2022 (formerly TweetScout API) with 6B+ requests served. About Sorsa API

What changed
  • First published.