How to Handle API Errors and Rate Limits (401, 403, 413, 429, 500)
By VeloForge Labs · · 3 min read
Every API call can fail, and a good client treats failure as a normal outcome. This guide explains what each error from the TexToolsAI conversion endpoint means and what your code should do about it.
Read the body, not only the status
Error responses come back as JSON with success: false and a human-readable error message. Log that message. It is more specific than the status code.
{
"success": false,
"error": "Invalid API key. Please check your key at textoolsai.org/pricing."
}
The status codes
400 Bad Request. The body was not valid JSON, or markdown was missing or not a string. Retrying the same request will fail the same way. Fix the payload.
401 Unauthorized. The Authorization header is missing, malformed, or the key is not recognized. The expected format is Authorization: Bearer <API_KEY>. Do not retry. Check that the environment variable is actually set in the process making the call.
403 Forbidden. The key exists but cannot be used, for example because it has expired (Pro keys last 30 days) or its status is not active. Do not retry. Buy or issue a new key.
413 Payload Too Large. The playground demo accepts up to 25 KB of Markdown per request. Pro keys are not subject to that playground limit. Split the document or use a key.
429 Too Many Requests. You reached a limit. What that means depends on the plan. In the playground, the daily allowance of 100 requests resets after 24 hours. For a Pro key, a 429 means the 50,000-request allowance is used up, and retrying will not help until you get a new key.
500 Internal Server Error. Something went wrong on the server. This is the one class where a retry is reasonable.
What to retry
| Status | Retry? | Why |
|---|---|---|
| 400 | No | Your request is wrong |
| 401 | No | Credentials are wrong |
| 403 | No | Key is expired or disabled |
| 413 | No | Request is too big |
| 429 | Usually no | Quota is used up, not a short burst |
| 500 | Yes, with backoff | Possibly temporary |
| Network error or timeout | Yes, with backoff | Possibly temporary |
Retry with exponential backoff
Retry only temporary failures, wait longer each time, and give up after a few attempts.
async function convertWithRetry(markdown, attempts = 3) {
for (let i = 0; i < attempts; i++) {
try {
const res = await fetch("https://texttoolsai.org/api/convert", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + process.env.TEXTOOLSAI_API_KEY,
},
body: JSON.stringify({ markdown }),
signal: AbortSignal.timeout(10_000),
});
const data = await res.json();
if (res.ok && data.success) return data.html;
if (res.status < 500) throw new Error(data.error || "Request rejected");
} catch (err) {
if (i === attempts - 1) throw err;
}
await new Promise((r) => setTimeout(r, 2 ** i * 500));
}
throw new Error("Conversion failed after retries");
}
Note the res.status < 500 check: client errors throw immediately instead of wasting retries. In production, add a little random jitter to the delay so many clients do not retry at the same instant.
Rate-limit headers
Successful responses include headers that tell you where you stand:
X-RateLimit-Limitis the allowance for your plan.X-RateLimit-Remainingis how many requests are left.X-RateLimit-Resetis a timestamp for when the playground allowance resets.
Read X-RateLimit-Remaining and warn yourself in monitoring well before it reaches zero. Surprise quota exhaustion is far worse than an alert that fires early.
Fail gracefully
If conversion fails when a user submits content, do not lose their text. Save the Markdown, show a friendly message, and queue the conversion for later. Showing an unsanitized fallback is never the answer, because it reopens the XSS problem. Show the plain text escaped instead.
The complete list of codes is in the API docs.