affaan-m/ECC · error · Exception
Forbidden: {resp.json().get('detail', 'check permissions')}
Error message
Forbidden: {resp.json().get('detail', 'check permissions')} What it means
A generic Exception raised by X (Twitter) API posting code when the response status is 403 Forbidden. The message includes the API's detail string if present, otherwise a hint to check permissions. 403 from the v2 tweets endpoint means the authenticated app/user is not permitted to perform the action.
Source
Thrown at skills/x-api/SKILL.md:206
remaining = int(resp.headers.get("x-rate-limit-remaining", 0))
if remaining < 5:
reset = int(resp.headers.get("x-rate-limit-reset", 0))
wait = max(0, reset - int(time.time()))
print(f"Rate limit approaching. Resets in {wait}s")
```
## Error Handling
```python
resp = oauth.post("https://api.x.com/2/tweets", json={"text": content})
if resp.status_code == 201:
return resp.json()["data"]["id"]
elif resp.status_code == 429:
reset = int(resp.headers["x-rate-limit-reset"])
raise Exception(f"Rate limited. Resets at {reset}")
elif resp.status_code == 403:
raise Exception(f"Forbidden: {resp.json().get('detail', 'check permissions')}")
else:
raise Exception(f"X API error {resp.status_code}: {resp.text}")
```
## Security
- **Never hardcode tokens.** Use environment variables or `.env` files.
- **Never commit `.env` files.** Add to `.gitignore`.
- **Rotate tokens** if exposed. Regenerate at developer.x.com.
- **Use read-only tokens** when write access is not needed.
- **Store OAuth secrets securely** — not in source code or logs.
## Integration with Content Engine
Use `brand-voice` plus `content-engine` to generate platform-native content, then post via X API:
1. Pull recent original posts when voice matching matters
2. Build or reuse a `VOICE PROFILE`
3. Generate content with `content-engine` in X-native formatView on GitHub (pinned to 01e15490f0)
Solutions
- In the developer portal, regenerate the token/keys with the required access level (Read and Write).
- Confirm the OAuth user access token is still valid (re-auth if the user revoked the app).
- Check that the request includes the user-context credentials for endpoints that require user auth, not just app-only Bearer auth.
- Avoid posting identical text repeatedly — X returns 403 for duplicates; vary content or check response detail.
- Read resp.json().get('detail') fully; it usually names the exact permission/scope missing.
Example fix
# before
elif resp.status_code == 403:
raise Exception(f"Forbidden: {resp.json().get('detail', 'check permissions')}")
# after: distinguish common 403 causes
detail = resp.json().get('detail', '')
if 'duplicate' in detail.lower():
raise DuplicateTweetError(detail)
if 'token' in detail.lower() or 'permission' in detail.lower():
raise PermissionError(f"Re-issue token with tweet.write scope: {detail}")
raise ForbiddenError(f"X 403: {detail}") Defensive patterns
Strategy: validation
Validate before calling
def token_has_write_scope(oauth) -> bool:
# verify the credentials include write access before posting
resp = oauth.get("https://api.x.com/2/users/me")
return resp.status_code == 200 # plus check your app's permission setting Type guard
null
Try / catch
try:
post_tweet(content)
except ForbiddenError as e:
if 'duplicate' in str(e).lower():
skip_duplicate(content)
else:
raise # permissions issue needs operator attention Prevention
- Issue tokens with the minimum required scope (tweet.write).
- Avoid duplicate content — X returns 403 for repeats.
- Re-auth users when they may have revoked the app.
When it happens
Trigger: Posting with a read-only token; the OAuth user's access was revoked; the app lacks the tweet.write scope; attempting to post duplicate content (X returns 403 for 'duplicate content'); attempting a privileged action (e.g. posting on behalf of another user) without the right permissions.
Common situations: Token generated with read-only permissions in the developer portal; app suspended or rate-limited by X enforcement; user revoked access between token creation and use; OAuth 1.0a user context not attached for a user-context endpoint.
Understand the failure class
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- Rate limited. Resets at {reset}
- X API error {resp.status_code}: {resp.text}
- ${label} destination is not writable by the current user: ${
- Failed to save package manager preference: ${err.message}
- Failed to save package manager config to ${configPath}: ${er
AI-assisted analysis of affaan-m/ECC@01e15490f0 (2026-08-13).
Data as JSON: /api/errors/efdb51ef478f6a37.
Report an issue: GitHub.