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:
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 body | Where it appears | Cause | First fix |
|---|---|---|---|
"reason": "client-not-enrolled", title "Client Forbidden" | Any v2 call | App on the retired Free package, standalone or outside a Project | Move 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, DMs | App-only Bearer token | Use a user-context token |
A bare "detail": "Forbidden" | GET /2/users/me | Token without tweet.read and users.read | Add both scopes, authorize again |
Any 403 on a like, follow or quote_tweet_id | /2/users/{id}/likes, /following, quote posts | Removed from self-serve on April 20, 2026 | Enterprise contract only |
| "Reply to this conversation is not allowed because you have not been mentioned..." | Replies | Summoned-reply rule since February 23, 2026 | Reply only when mentioned or quoted |
| A 403 on a post with two or more $cashtags | POST /2/tweets | Self-serve limit of 1 cashtag per post | Keep one cashtag |
| "This user is not allowed to post a video longer than N minutes." | Posts with video | The posting account's video cap | Trim the video |
| "You are not permitted to perform this action." | POST /2/tweets | X refused this post: a rule above, posting limits or a spam check | Check the content, slow down |
| "Your account is not permitted to access this feature." | POST /2/tweets | Write restriction on the posting account | Stop retries, check the account on x.com |
Type not-authorized-for-resource | Reading a post or user | Protected or private content | None: only approved followers can read it |
A dm- type, or "This operation is not permitted." | Direct messages | The recipient's settings or a safety check | Read the type; often nothing to fix |
| Code 453, "You currently have access to a subset of X API V2 endpoints..." | v1.1 endpoints | v1.1 is outside your access | Call 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.readandusers.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.
{
"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.
-
Open the Developer Console. Sign in at console.x.com and find the app whose keys your code uses.
-
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.
-
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.
-
Make new keys and tokens. Regenerate the Bearer token and the access tokens, then deploy them: old ones keep the old enrollment.
-
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:
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-enrolledwhen the app isn't on pay-per-use;not-authorized-for-resourceon 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_idneeds 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
POSTandDELETE /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
POSTandDELETE /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-verifiedand otherdm-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.Clientinstead oftweepy.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:
{
"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/mewith a bare "Forbidden". The user token lackstweet.readandusers.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 insideerrorsof 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.APIcalls such asupdate_statushit v1.1;tweepy.Clientcalls 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.
#Print the whole 403 body in tweepy and Node.js
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
detailcan mislead. Theclient-not-enrolledtext 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. Trustreasonandtypefirst, 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 reads | Sorsa API | Official X API |
|---|---|---|
| Setup | One API key | Developer app, pay-per-use package, OAuth tokens |
| Price per 1,000 posts | from $0.014 | $5.00 |
| Speed | 20 requests per second on every plan | Per-endpoint 15-minute windows |
| Monthly ceiling | Your plan, up to 10M requests, plus extra requests | 3M post reads, then Enterprise |
| Your X account | Never involved | Tokens 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
-
Get a key. Sign up and copy the key from the dashboard; 100 free requests come with it, no card needed.
-
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 } } ] } -
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 throughnext_cursoruntil 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") -
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:
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.
#How we checked
On November 2, 2026 we read X's response-code reference, pricing page, create-post reference and changelog on docs.x.com, plus the X staff answers in the developer forum threads linked above, for every error text quoted here. The Sorsa API requests follow the live API reference; the response shown is an example in its format with made-up data.
Sources
- X API: Response codes and errors (opens in a new tab)
- X API: Pay-per-usage pricing (opens in a new tab)
- X API: Changelog (opens in a new tab)
- X API: Create post reference (opens in a new tab)
- X API: Manage posts (opens in a new tab)
- X Developer Community: pricing update effective April 20, 2026 (opens in a new tab)
- X Developer Community: Free tier stuck as a standalone app (opens in a new tab)
- X Developer Community: POST /2/tweets 403 on pay-per-use (opens in a new tab)
- X Developer Community: "Your account is not permitted to access this feature" (opens in a new tab)
- X Developer Community: 403 when replying via the API (opens in a new tab)
- Sorsa API reference