{"record":{"id":"efdb51ef478f6a37","repo":"affaan-m/ECC","slug":"forbidden-resp-json-get-detail-check-permi","errorCode":null,"errorMessage":"Forbidden: {resp.json().get('detail', 'check permissions')}","messagePattern":"Forbidden: (.+?)","errorType":"exception","errorClass":"Exception","httpStatus":null,"severity":"error","filePath":"skills/x-api/SKILL.md","lineNumber":206,"sourceCode":"\nremaining = int(resp.headers.get(\"x-rate-limit-remaining\", 0))\nif remaining < 5:\n    reset = int(resp.headers.get(\"x-rate-limit-reset\", 0))\n    wait = max(0, reset - int(time.time()))\n    print(f\"Rate limit approaching. Resets in {wait}s\")\n```\n\n## Error Handling\n\n```python\nresp = oauth.post(\"https://api.x.com/2/tweets\", json={\"text\": content})\nif resp.status_code == 201:\n    return resp.json()[\"data\"][\"id\"]\nelif resp.status_code == 429:\n    reset = int(resp.headers[\"x-rate-limit-reset\"])\n    raise Exception(f\"Rate limited. Resets at {reset}\")\nelif resp.status_code == 403:\n    raise Exception(f\"Forbidden: {resp.json().get('detail', 'check permissions')}\")\nelse:\n    raise Exception(f\"X API error {resp.status_code}: {resp.text}\")\n```\n\n## Security\n\n- **Never hardcode tokens.** Use environment variables or `.env` files.\n- **Never commit `.env` files.** Add to `.gitignore`.\n- **Rotate tokens** if exposed. Regenerate at developer.x.com.\n- **Use read-only tokens** when write access is not needed.\n- **Store OAuth secrets securely** — not in source code or logs.\n\n## Integration with Content Engine\n\nUse `brand-voice` plus `content-engine` to generate platform-native content, then post via X API:\n1. Pull recent original posts when voice matching matters\n2. Build or reuse a `VOICE PROFILE`\n3. Generate content with `content-engine` in X-native format","sourceCodeStart":188,"sourceCodeEnd":224,"githubUrl":"https://github.com/affaan-m/ECC/blob/01e15490f04e29cfefe3896951f43db46994d8ee/skills/x-api/SKILL.md#L188-L224","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"# before\nelif resp.status_code == 403:\n    raise Exception(f\"Forbidden: {resp.json().get('detail', 'check permissions')}\")\n\n# after: distinguish common 403 causes\ndetail = resp.json().get('detail', '')\nif 'duplicate' in detail.lower():\n    raise DuplicateTweetError(detail)\nif 'token' in detail.lower() or 'permission' in detail.lower():\n    raise PermissionError(f\"Re-issue token with tweet.write scope: {detail}\")\nraise ForbiddenError(f\"X 403: {detail}\")","handlingStrategy":"validation","validationCode":"def token_has_write_scope(oauth) -> bool:\n    # verify the credentials include write access before posting\n    resp = oauth.get(\"https://api.x.com/2/users/me\")\n    return resp.status_code == 200  # plus check your app's permission setting","typeGuard":"null","tryCatchPattern":"try:\n    post_tweet(content)\nexcept ForbiddenError as e:\n    if 'duplicate' in str(e).lower():\n        skip_duplicate(content)\n    else:\n        raise  # permissions issue needs operator attention","preventionTips":["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."],"tags":["python","x-api","oauth","permissions","forbidden"],"backgroundTag":null,"analyzedSha":"01e15490f04e29cfefe3896951f43db46994d8ee","analyzedAt":"2026-08-13T00:31:08.655Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}