API reference·4 min read

Error handling

Common API error codes, what they mean, and how to handle each one in your integration.

By Operelio team · Updated July 2026

On this page6
  1. 1.Error response shape
  2. 2.Status codes
  3. 3.What to retry, what not to
  4. 4.Don't retry these
  5. 5.Retry snippet
  6. 6.Frequently asked questions

Error response shape

Every error response is JSON with an "error" field explaining what went wrong, plus the HTTP status code. Some errors include extra context fields (e.g. "requiredPlan": "agency" on a 403, or a "Retry-After" header on 429).

Example 403 response
HTTP/1.1 403 Forbidden
Content-Type: application/json

{
  "error": "Upgrade to Agency to use batch processing.",
  "requiredPlan": "agency"
}

Status codes

CodeMeaningWhat to do
400Bad requestYour request body is missing required fields, has invalid values, or is too big. Common causes: malformed JSON, missing inputFileId, unknown toolType, configJson over 100 KB (on /jobs), or a body shape the route can't parse. Both /jobs and /jobs/batch return {"error": "Invalid JSON body."} for an unparseable body.
401UnauthorizedYour API key is missing, invalid, or revoked. Check the Authorization header is set to "Bearer op_…" with a valid key.
403ForbiddenYour plan doesn't allow this. Three sub-cases: (a) the endpoint or tool requires a higher plan: batch processing requires Agency, and API access itself requires Team. (b) A sub-feature inside a tool requires Pro (e.g. similarity matching, advanced patterns, inner/full joins). (c) Your monthly job quota is exhausted on /uploads or /jobs/batch. The requiredPlan field tells you which plan you'd need.
404Not foundThe job ID, file ID, or batch ID doesn't exist or doesn't belong to you. Double-check the ID and that you're calling with the same key that created the resource.
413Payload too largeYour /jobs/batch request body exceeds 100 KB, or your /uploads request declares a Content-Length over your plan's file size cap. (For /jobs, the same 100 KB configJson cap returns 400 instead. On /uploads, a file that slips past the Content-Length check but exceeds the cap after parsing returns 400.)
429Too many requestsOne of four things: (a) burst rate limit hit (60/min), error "Rate limit exceeded", Retry-After counts down the current window. Wait that many seconds and retry. (b) Monthly API call cap spent (Team 1,000, Agency 5,000), code "api_monthly_limit_reached", with limit, used, and resetAt in the body. Has a Retry-After too, but it counts down to your cycle reset. Don't sleep on it. (c) Monthly job quota exhausted on /jobs, error starts "Monthly job limit reached". No Retry-After. Wait for the next billing cycle or upgrade. (d) Concurrent job limit hit (1 Free / 2 Pro / 3 Team / 5 Agency), error mentions "jobs running". No Retry-After. Wait for a running job to finish. Switch on the error string and the code field to tell them apart.
5xxServer error500/502/503/504 mean something went wrong on our end. Retry with exponential backoff. If it persists, contact support with the timestamp and the request you sent.

What to retry, what not to

Retry strategy depends on which kind of 429 you got and on whether the response is server-side. Switch on the response shape, not just the status code:

ResponseRetry strategy
429, error "Rate limit exceeded"Burst limit. Wait the number of seconds in the Retry-After header, then retry.
429, code "api_monthly_limit_reached"Monthly API call cap spent. Don't retry: the Retry-After on this one counts down to your cycle reset (the resetAt field), which can be days away. Wait for the cycle to roll or upgrade.
429 without Retry-After, error "Monthly job limit reached"Job quota exhausted. Don't retry. Wait for the next billing cycle or upgrade your plan.
429 without Retry-After, error mentions "jobs running"Concurrent job limit hit (1 Free / 2 Pro / 3 Team / 5 Agency). Wait for a running job to finish, then retry. A short polling delay (a few seconds) usually clears it.
5xxServer error. Exponential backoff: 1s, 2s, 4s, 8s, 16s. Give up after 5 attempts. Covers 500, 502, 503, and 504.

Don't retry these

These responses mean your request itself is the problem. Retrying without changing it fails the same way. Fix the request, the plan, or wait for next month.

ResponseWhy retry won't help
400The body is malformed or missing required fields. Check the error message and fix the request.
401The key is invalid or revoked. Generate a new key and update your integration.
403 (plan or feature gate)The endpoint or sub-feature isn't on your plan. Upgrade to the plan in the requiredPlan field.
403 (quota exhausted)Your monthly job quota is gone for this month. Wait for the next billing cycle, or upgrade to a plan with a higher cap.
404The ID doesn't exist. Verify you're sending the right ID and using the same key that owns the resource.
413The batch request body is over 100 KB. Trim configJson or split into smaller batches before retrying.
429 (quota or concurrent limit)A 429 with code "api_monthly_limit_reached" or the error "Monthly job limit reached" is a spent monthly cap: wait for next cycle or upgrade. A 429 mentioning "jobs running" is the concurrent limit: wait for one of your running jobs to finish before retrying. Only the "Rate limit exceeded" 429 is worth an automatic retry.

Retry snippet

A minimal retry helper in JavaScript that handles every retryable case correctly. It retries burst-limit 429s (honoring Retry-After) and 5xx server errors (exponential backoff with cap). It does not retry other 4xx errors, 429s without Retry-After (job quota or concurrent limit), or the monthly API cap 429 (code "api_monthly_limit_reached", whose Retry-After counts down to your cycle reset and can be days long).

Retry helper
async function callApi(url, options, attempt = 1) {
  const res = await fetch(url, options);

  // Success or non-retryable client error: return as-is.
  if (res.status < 500 && res.status !== 429) return res;

  if (res.status === 429) {
    // Two kinds of 429 aren't worth an automatic retry: the monthly API
    // cap (code "api_monthly_limit_reached", whose Retry-After counts
    // down to your cycle reset, which can be days), and the job-quota
    // and concurrent-limit 429s (no Retry-After at all).
    const body = await res.clone().json().catch(() => null);
    if (body && body.code === "api_monthly_limit_reached") return res;
    if (!res.headers.get("Retry-After")) return res;
  }

  // Give up after 5 attempts.
  if (attempt >= 5) return res;

  // Burst 429: honor Retry-After. 5xx: exponential backoff.
  const delay =
    res.status === 429
      ? Number(res.headers.get("Retry-After")) * 1000
      : Math.min(1000 * 2 ** (attempt - 1), 16_000);

  await new Promise((r) => setTimeout(r, delay));
  return callApi(url, options, attempt + 1);
}

The helper retries burst-limit 429s (error "Rate limit exceeded") and 5xx errors. It does not retry 400/401/403/404, 429s without Retry-After, or the monthly API cap 429, all of which need outside intervention.

Frequently asked questions

Does the API ever return errors that aren't in this list?

The standard codes above cover almost everything. Some endpoints include extra context fields alongside the error (e.g. "requiredPlan" on plan-gated 403s, "totalRows" and "maxTotalRows" on a batch row-cap rejection). Always parse the JSON body for details.

Why is the apply_fixes error a code instead of a sentence?

When you exceed the 3-rounds-per-Health-Check apply_fixes cap, the response is { "error": "apply_fixes_limit_reached", "limit": 3, "used": N }. The "error" field is a machine-readable code so your integration can switch on it directly without parsing prose. The only other machine-readable code is "api_monthly_limit_reached", which every endpoint returns (in a separate "code" field, alongside a human-readable "error") when the monthly API call cap is spent. Everything else uses a human-readable string.

What does a 200 response with status "failed" mean?

GET /jobs/:id always returns 200 if the job exists, with the job's status in the body. "failed" means the worker tried to process the job and ran into an error, like a corrupt file or a parsing failure. The errorMessage field explains what went wrong. This is different from a 4xx or 5xx, which means the API call itself didn't reach the worker.

How do I tell if my key was revoked vs never existed?

Both return 401 with the same "Unauthorized" message, by design. Telling them apart would leak information about which keys exist. Check the API Keys page in the dashboard to see your active and revoked keys.

Ready to get started?

Upload a file and run your first transformation. Free, no credit card required.