Twitter API Errors | Status Codes and JSON Shape
HTTP status codes TwitterAPIs returns, the JSON error shape, and how to handle each one in your client.
TwitterAPIs uses standard HTTP status codes. A 2xx means the call worked. Anything else carries a JSON error body that tells you what went wrong. Read the status code first, then the body.
Status codes
| Code | error | Meaning | What to do |
|---|---|---|---|
200 | none | Success | Parse the JSON body. The data is ready. |
400 | bad_request | A parameter is missing or malformed. | Fix the request before retrying. |
401 | unauthorized | The API key is missing, malformed, or revoked. | Check the Authorization (or x-api-key) header. |
402 | insufficient_credits | Your balance is exhausted. | Top up in the dashboard, then retry the same request. |
404 | not_found | The resource does not exist, for example a deleted tweet, a suspended/unknown handle, or a private account. | Do not retry. |
429 | rate_limited | Upstream rate limit hit; the pool account was cooled down. | Back off, then retry. See Rate limits. |
502 | bad_gateway | The upstream X request failed. | Retry with backoff. |
503 | no_capacity / billing_unavailable | The account pool is momentarily saturated, or billing was unreachable (we fail closed). | Retry shortly. |
Error shape
Every non-2xx response uses the same JSON envelope, so you can parse errors with one code path:
{
"error": "insufficient_credits",
"message": "Not enough credits for this request. Top up to continue."
}| Field | Type | Description |
|---|---|---|
error | string | A stable, machine-readable code (for example not_found, rate_limited, insufficient_credits). Branch on this, not on the message text. |
message | string | A human-readable explanation of the failure. Safe to surface to a user; do not parse it programmatically. |
Handling errors
Branch on the status code. Retry the transient classes (429 and 5xx) with exponential backoff, and surface the rest to the caller because a human needs to act.
async function call(url) {
for (let attempt = 0; attempt < 4; attempt++) {
const res = await fetch(url, {
headers: { Authorization: `Bearer ${process.env.TWITTERAPIS_KEY}` },
});
if (res.ok) return res.json();
// Retry only the transient classes.
if (res.status === 429 || res.status >= 500) {
await new Promise((r) => setTimeout(r, 2 ** attempt * 500));
continue;
}
// 400, 401, 402, 404 need a human, not a retry.
const err = await res.json();
throw new Error(`${err.error}: ${err.message}`);
}
throw new Error("Exhausted retries");
}import os
import time
import requests
def call(url):
for attempt in range(4):
res = requests.get(
url,
headers={"Authorization": f"Bearer {os.environ['TWITTERAPIS_KEY']}"},
)
if res.ok:
return res.json()
# Retry only the transient classes.
if res.status_code == 429 or res.status_code >= 500:
time.sleep(2 ** attempt * 0.5)
continue
# 400, 401, 402, 404 need a human, not a retry.
err = res.json()
raise RuntimeError(f"{err['error']}: {err['message']}")
raise RuntimeError("Exhausted retries")A 402 is not a bug
402 insufficient_credits means the request was valid but your balance ran out. Add credits in the dashboard and the same request will succeed. Rate-limited (429) and upstream (502) calls are never billed, so a retry costs you nothing.
FAQ
Which errors should I retry?
Retry the transient classes, 429 and 5xx, with exponential backoff. Surface 400, 401, 402, and 404 to the caller, since they need a human to act.
Should I branch on the message text?
No. Branch on the stable error code field, not the human-readable message. The message is safe to show a user but not to parse.
Is a 402 a bug?
No. A 402 insufficient_credits means the request was valid but your balance ran out. Top up in the dashboard and retry the same call.