Operelio
Docs10 min read

Error handling

Every API error code, what it means, and how to handle it: the JSON error shapes, codes such as read_only_key and plan_required, and when to retry.

By Operelio team · Updated October 30, 2026

On this page7
  1. 1.Error response shape
  2. 2.Status codes
  3. 3.Codes on the newer endpoints
  4. 4.What to retry, what not to
  5. 5.Don't retry these
  6. 6.Retry snippet
  7. 7.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).

The endpoints for trails, leads, Do Not Contact, workflows, CRM and reports always add a "code" next to "error": a short, stable word to switch on, while "error" is a sentence to show a person. The file and job endpoints (/uploads, /jobs, /jobs/batch and /files) keep the shapes below.

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, 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. Two sub-cases: (a) the endpoint or tool requires a higher plan, for example batch processing requires Agency (a key whose workspace has lost API access altogether gets 401, not 403). (b) A feature that saves or sends requires Starter (e.g. saved mappings, custom CRM schemas, CRM push). The requiredPlan field tells you which plan you'd need. Running out of monthly jobs is a 429, not a 403.
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 4.5 MB (the most one request can carry, whatever your plan's cap). Our error body names maxRequestBytes; the host may send its own plain 413 instead. (For /jobs, the same 100 KB configJson cap returns 400 instead. On /uploads, a file that slips past the Content-Length check but exceeds your plan's file size cap after parsing returns 400.)
429Too many requestsOne of four things: (a) burst rate limit hit (60/min), error "Rate limit exceeded" or, on the newer endpoints, code "rate_limited", Retry-After counts down the current window. Wait that many seconds and retry. (b) Monthly API call cap spent (Pro 1,000, Agency 5,000), on any request that counts (uploads, jobs, batches, pushes, trail and routing changes, Do Not Contact changes, saved workflow runs, Bridgeant questions that use a message and confirms that run, never a read), 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 jobs used up, on /uploads, /jobs or /jobs/batch: code "job_monthly_limit_reached", with limit, used, and resetAt in the body (a batch adds requested and remaining). No Retry-After. Wait until resetAt, upgrade to the plan in requiredPlan, or send a smaller batch. (d) Concurrent job limit hit (1 Free / 2 Starter / 3 Pro / 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.

Codes on the newer endpoints

Every response from the trails, leads, Do Not Contact, workflows, CRM, reports and Bridgeant endpoints that isn't a success carries an error sentence and a code, except Bridgeant's 401 and its burst-limit 429, which keep the /jobs shape. These are the codes to handle:

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

{
  "error": "This API key is read only. Make a key with read and write access for this.",
  "code": "read_only_key"
}
StatusCodeWhat it means
403read_only_keyA write with a read-only key. Make a key with read and write access.
403admin_key_requiredA change to a trail, a routing team, the Do Not Contact list or a CRM push, with a key whose maker isn't an admin of the workspace any more, or has no maker. Have an admin make the key.
403plan_requiredThe feature needs a higher plan. The error sentence names it.
403plan_trail_capResuming would pass your plan's limit on live trails. upgradeRequired is true when a higher plan allows more.
404trail_not_found, run_not_found, lead_not_foundNo such id in this workspace.
409status_changedThe trail's state changed at the same moment. Read it again, then decide.
409ambiguous_workflow_nameTwo saved workflows share that name. Run the one you mean by its id.
409trail_live_change_needs_testThe trail is on. Switch it off first, or change the team in the app.
409trail_draft_pendingThe trail has routing changes on its canvas that aren't published. Publish or discard them in the app, then try again.
409no_crmThe trail sends to no CRM, so its teams have no CRM users to name.
409routing_refusedA rule names a territory that was deleted, or territories were being changed at that moment. The error sentence says which.
422invalid_teamThe team you sent breaks the routing rules' own limits. The error sentence says what to fix.
403workspace_archivedThe workspace is archived, so its trails can't be changed.
4xxjob_refusedPOST /workflows/run couldn't start the job and the job gave no code of its own. The status and the error sentence say why.
409version_conflictThe trail changed since the ifVersion you sent. Read its routing again and send the new version. currentVersion has it.
422no_folder, no_destination, mapping_missing, draft_pending, builder_setup_needed, schedule_invalid and the other switch-on checksResume failed one of the checks the Switch on button runs. The error sentence says what to fix.
422owner_not_activeA routing team names someone who isn't an active user of the trail's CRM. ownerIds lists them.
422cap_exceededThe Do Not Contact entries would take the list past your plan's cap. Nothing was added.
400invalid_inputThe Bridgeant ask has no message, or its conversationId or fileId isn't text.
400message_too_longThe message to Bridgeant is over 8,000 characters. Upload a file instead of pasting it.
403key_has_no_creatorBridgeant answers as the person who made the key, and this key has no maker on record. Make a new key.
403creator_not_memberBridgeant answers as the person who made the key, and they aren't in the workspace now.
403bridgeant_offBridgeant is switched off for the key's maker or for the workspace.
404conversation_not_foundNo conversation with that id for the key's maker in this workspace.
404action_not_foundNo action with that id waiting for the key's maker. Ask Bridgeant again.
409already_runThat action has already run. Ask Bridgeant again to run it once more.
409skipped_quotaThe trail run was skipped because the month's jobs are used up. Nothing ran, and the call didn't count.
409plan_changedThe plan changed after Bridgeant offered to save it as a trail. Ask again.
409not_run, or the plan's own reasonThe plan didn't run. Nothing was used, and the call didn't count.
429slow_downThe key's maker sent Bridgeant more than 30 messages in a minute, counting the chat in the app. Wait for Retry-After.
400 or 409job_not_pushable, already_pushed, held, connection_inactive, provider_mismatchA push couldn't start: the job isn't a finished file that can be pushed, it was pushed already, it is held, the CRM connection is off, or it's for another CRM.
409push_in_progressA push of this job is already running. Read its status with GET /jobs/:id/push.
401 or 409crm_reconnect_neededThe CRM refused Operelio's sign-in: 401 on a push, 409 when reading CRM owners or changing a team. Reconnect it in the app. Your API key is fine.
503crm_unreachableThe CRM couldn't be reached. Try again in a minute.

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" or code "rate_limited"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, code "job_monthly_limit_reached"Monthly jobs used up. Don't retry. Wait until resetAt, upgrade your plan, or for a batch, send no more files than remaining.
429 without Retry-After, error mentions "jobs running"Concurrent job limit hit (1 Free / 2 Starter / 3 Pro / 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 or plan field.
403 (read_only_key, admin_key_required)The key can't write. Use a read-and-write key made by an admin.
409 (status_changed)Something changed between your read and your write. Read again, then decide whether to send it again.
422A check failed or a cap was reached. Fix what the error sentence names first.
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, or the upload is over 4.5 MB. Trim configJson, split the batch, or send a smaller file before retrying.
429 (quota or concurrent limit)A 429 with code "api_monthly_limit_reached" or "job_monthly_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 burst-limit 429 ("Rate limit exceeded" or code "rate_limited") 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 (monthly jobs 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) {
    // Three 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), the monthly jobs
    // limit (code "job_monthly_limit_reached"), and the concurrent limit
    // (no Retry-After at all).
    const body = await res.clone().json().catch(() => null);
    if (body && body.code === "api_monthly_limit_reached") return res;
    if (body && body.code === "job_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" or code "rate_limited") 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 cap of 3 apply_fixes rounds per Data Health Check, 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. Other errors carry a machine-readable code in a separate "code" field alongside a human-readable "error": "api_monthly_limit_reached", which any write returns when the monthly API call cap is spent, "job_monthly_limit_reached", which /uploads, /jobs and /jobs/batch return when your monthly jobs are used up, and every error from the trails, leads, Do Not Contact, workflows, CRM and reports endpoints. The other file and job errors use 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 deliberately return 401 with the same "Unauthorized" message. Telling them apart would leak information about which keys exist. Check the API keys tab (open Integrations in the rail and choose API keys) to see your active and revoked keys.

Connect Operelio to your code or your assistant

Start free and add Operelio to Claude or ChatGPT today. The API and webhooks come with Pro.