X API 403 Forbidden When Posting: The 4 Real Causes and Fixes
Last updated: July 9, 2026
A 403 Forbidden from the X API when posting a tweet is not a rate limit and not an expired token. It means X understood your request, checked your credentials, and decided you are not allowed to do this specific action. In practice, almost every 403 on POST /2/tweets comes down to one of four causes. Work through them in order.
Cause 1: App-only auth instead of user-context auth
This is the most common cause, and the error message is unhelpful: "You are not permitted to perform this action."
The X API has two authentication modes. App-only auth (an OAuth 2.0 Bearer token from your app's keys) can read public data but can never post, because a tweet has to come from a user. Posting requires user context: either OAuth 1.0a with an access token and secret, or an OAuth 2.0 user token obtained through the Authorization Code with PKCE flow.
The fix in Tweepy: do not pass only bearer_token to tweepy.Client. Pass all four user-context credentials:
import tweepy
client = tweepy.Client(
consumer_key="YOUR_API_KEY",
consumer_secret="YOUR_API_KEY_SECRET",
access_token="YOUR_ACCESS_TOKEN",
access_token_secret="YOUR_ACCESS_TOKEN_SECRET",
)
client.create_tweet(text="Hello from user context")
If you initialize tweepy.Client(bearer_token="...") and call create_tweet, you get 403 every time. The same rule applies in every language: raw curl with Authorization: Bearer <app bearer token> against POST /2/tweets is always forbidden.
Cause 2: App is not attached to a Project
This cause announces itself with a distinctive message: "When authenticating requests to the Twitter API v2 endpoints, you must use keys and tokens from a Twitter developer App that is attached to a Project."
X API v2 endpoints only work for apps that live inside a Project in the developer portal. Standalone apps (common for accounts created years ago, or apps created outside the Projects flow) get 403 on every v2 call regardless of credentials.
The fix: open the developer portal, create a Project if you do not have one, and attach your app to it (or create a new app inside the Project). Then regenerate your keys and tokens, because credentials issued to the standalone app do not carry over. On newer pay-per-use accounts, also confirm the app is linked to your pay-per-use package. Adding credit does not automatically link the app.
Cause 3: Missing tweet.write scope or stale token permissions
Two variants of the same problem:
- OAuth 2.0 user context: your access token must include the
tweet.writescope (plustweet.readandusers.read, which most libraries require). If your authorization URL only requested read scopes, posting returns 403. Re-run the authorization flow requestingtweet.read tweet.write users.read offline.access. - OAuth 1.0a: your app's permissions must be set to "Read and Write" in the developer portal, under User authentication settings. The gotcha: access tokens keep the permission level they were issued with. If you generated tokens while the app was Read-only and then flipped it to Read and Write, the old tokens still cannot post. Regenerate the access token and secret after changing permissions.
You can verify what a token can do by checking the developer portal's authentication settings and, for OAuth 2.0, inspecting the scopes you requested during authorization. If tweet.write is not in that list, no amount of retrying will help.
Cause 4: Zero credit balance on pay-per-use
Since February 6, 2026, new X API signups are on pay-per-use billing: you buy credits and every request deducts from them (roughly $0.015 per post, $0.005 per read, and $0.20 per post containing a link). When the balance runs out, X typically returns a 402 with a CreditsDepleted message, but developers also report 403 responses when the balance has gone negative, when the app was never enrolled in the pay-per-use package, or during account migration.
The fix: check the credit balance in the developer portal. Top it up if it is at or below zero, and confirm your app appears under the pay-per-use package rather than as an unlinked standalone app. If credits exist and the app is linked but 403 persists on every endpoint, it is usually an enrollment glitch on X's side, and the developer community forum is the escalation path.
How do I tell which cause I have?
Read the error body, not just the status code. The X API returns a JSON body with title and detail fields. "You are not permitted to perform this action" points to cause 1 or 4. The "attached to a Project" message is cause 2. "Your client app is not configured with the appropriate oauth1 app permissions for this endpoint" is cause 3. A 402 or any mention of credits is cause 4.
If you would rather not manage any of this, OpenTweet handles it for you: it queues your posts, retries transient failures, and manages X OAuth tokens and refresh on its own developer infrastructure, so a single OpenTweet API key (or the hosted MCP endpoint) is all your code needs. See the developer page for the API and MCP setup.