Building blocks
Errors
Status codes, the JSON error envelope, the stable error-code catalogue and the request id to quote to support. Branch on error.code, never on the message.
Every ARCNM error returns the same JSON envelope — a stable
error.code, a human-readable message, and a request_id for
support. Branch on code, never the message:
{
"error": {
"code": "insufficient_scope",
"message": "API key / token missing required scope",
"details": {},
"doc_url": "https://api.arcnm.io/…"
},
"request_id": "req_9c5f-…"
}
code— stable machine-readable identifier. Switch on this, not onmessage.message— human-readable, English. Safe to log; translate before showing end-users.details— endpoint-specific context (offending field, valid values, limits). Always an object.doc_url— a link to the relevant docs for this code.request_id— a top-level sibling oferror, and also theX-Request-IDresponse header. Quote it in support tickets.
Status codes
| Code | Meaning |
|---|---|
200 OK |
Success, body returned |
201 Created |
Resource created |
202 Accepted |
Async work enqueued — poll for the result |
204 No Content |
Success, no body |
400 Bad Request |
Payload doesn't match the schema |
401 Unauthorized |
Missing / invalid / expired credential |
402 Payment Required |
Wallet can't cover the call, or plan entitlement exceeded |
403 Forbidden |
Authenticated but lacks scope / role / verification |
404 Not Found |
Resource doesn't exist in this tenant |
405 Method Not Allowed |
Wrong method for the route |
409 Conflict |
Uniqueness or state conflict (e.g. duplicate part_number) |
410 Gone |
Resource permanently removed |
412 Precondition Failed |
Precondition (e.g. If-Match) not met |
413 Payload Too Large |
Request body exceeds the size cap (details.observed_bytes, details.limit_bytes) |
415 Unsupported Media Type |
Wrong Content-Type (e.g. JSON for a CAD upload) |
422 Unprocessable Entity |
Schema valid but a value was rejected |
423 Locked |
Wallet administratively paused |
429 Too Many Requests |
Request rate limit hit — see Rate limits. Plan-quota exhaustion is 402, not 429 |
503 Service Unavailable |
Transient overload or deploy window — honour Retry-After, then retry |
5xx (other) |
Server / upstream error — retry with backoff |
Billed on success only: non-2xx responses cost €0, including 429s.
Error code catalog
These code values are emitted by the platform's typed errors and are
stable. Route-level HTTPExceptions that don't map to a typed error get
a code derived from the status (400→bad_request, 401→unauthorized,
403→forbidden, 404→not_found, 405→method_not_allowed,
409→conflict, 410→gone, 415→unsupported_media_type,
422→unprocessable_entity, 429→too_many_requests) — with the specific
reason in message (e.g. calculation_not_found,
max_attempts_exceeded).
Authentication & authorization
| Code | Status | Meaning |
|---|---|---|
unauthorized |
401 | Missing or invalid credential |
invalid_api_key |
401 | API key not found or revoked |
mfa_required |
401 | Session needs an MFA step |
replay_detected |
401 | Token / key replay — session revoked |
forbidden |
403 | Authenticated but not allowed |
insufficient_scope |
403 | Key/token lacks the required scope |
role_read_only |
403 | The signed-in user's role in this organization is read-only (details.role, e.g. viewer): it can read everything a member reads, but every change needs a member, admin or owner |
tenant_isolation_violation |
403 | Tenant context required / mismatch |
email_verification_required |
403 | Caller's email isn't verified |
recent_auth_required |
403 | The action needs a step-up confirmation made within the last few minutes. details.reason is no_recent_auth (none on file), factor_not_satisfied (you confirmed with a factor this action does not accept) or verification_unavailable (we could not check — retry). details.factors_accepted lists what satisfies this action; details.factors_available lists what the caller has enrolled (null when it cannot be resolved); details.step_up names where to go — e.g. {"factor": "passkey", "endpoint": "/api/v1/sessions/sudo/passkey/options"}. Confirm there, then retry the original request within details.window_seconds. There is no password prompt: the factors are a passkey, an authenticator or recovery code, or a code emailed to you. Once an account uses an authenticator app, an emailed code is no longer accepted for step-up; details.factors_accepted then lists passkey and totp only |
Billing (402 / 423)
| Code | Status | Meaning |
|---|---|---|
insufficient_funds |
402 | Wallet can't cover the request |
entitlement_exceeded |
402 | Plan limit exceeded. With details.feature nesting_runs it is this month's job-nest runs (details.used, details.limit): a new start is kept as a draft instead, and POST /nest-runs/{run_id}/start answers this until a run is free |
addon_required |
402 | The request needs the Nesting add-on: job nests (start, preview, revisions, release), a calculation's cutting file, and the nesting potential of a selection. Pricing never needs it — every sheet quote is nested true-shape on every plan. details.feature is nesting, details.required_plan scale, details.upgrade_url where to book it. Not a plan limit — book the add-on on Scale or Enterprise (or start its trial), then retry. Stored quotes and job nests stay readable without it — except the job nest an assembly calculation runs for its own sheet components, which is read or changed with the add-on only |
quota_exceeded |
402 | Plan quota for a feature exhausted. Calculation submits always carry details.cells_requested / details.cells_available (a single run asks for 1). A grid the remaining quota can't cover never runs in part: /calculations/batch rejects the whole request with nothing persisted, while /quote and /upload-and-quote save the grid's cells parked blocked first — their details then also carry batch_id and parked_cells, so once unblocked you resume that batch instead of re-submitting it. Shrink the grid, upgrade the plan, or top up your balance — the coverage block on the parked calculations says which |
wallet_locked |
423 | Wallet is paused |
State & validation
| Code | Status | Meaning |
|---|---|---|
bad_request |
400 | Malformed request |
invalid_cursor |
400 | Pagination cursor is malformed, was issued for a different query (filters or order changed mid-walk), or was sent together with offset — see Pagination |
not_found |
404 | Resource not in tenant |
method_not_allowed |
405 | Wrong HTTP method for the route |
conflict |
409 | Uniqueness or state conflict (e.g. a duplicate unique key, or a referenced resource that does not exist / is still in use). When a database constraint raises it, details.constraint names the violated constraint |
no_organization_context |
409 | User must create/join an org first |
idempotency_in_flight |
409 | Same Idempotency-Key still processing |
idempotency_conflict |
409 | Same key reused with a different body |
subscription_not_provisioned |
409 | Subscription not yet linked to billing |
gone |
410 | Resource permanently removed — don't retry, and drop the id |
precondition_failed |
412 | Precondition not met |
payload_too_large |
413 | Request body exceeds the size cap (details.observed_bytes, details.limit_bytes) |
unsupported_media_type |
415 | Wrong Content-Type for the route — e.g. JSON sent to a multipart CAD upload |
unprocessable_entity |
422 | Value rejected by a business rule |
value_too_long |
422 | A string field exceeds the column it is stored in. details.max_length is the limit; details.field names the field when it can be identified (e.g. part_number, description). Send a shorter value — retrying the same one always fails |
material_unresolved |
422 | The material_ref you sent matches no material in the catalogue, or the material_grade_id is not a known grade. details.candidates carries ranked near matches (same shape as POST /materials/lookup) — pick one and resend its material_grade_id. Raised identically by every calculation entry point: POST /calculations, /quote, /batch, /upload-and-quote(-json) and PATCH /calculations/{id}/material. Omit the material entirely to price what the part's own drawing declares |
environment_not_found |
404 | The costing_environment_id you sent is not a costing environment of your organization — another organization's id answers exactly like one that never existed. Raised identically by POST /calculations, /quote, /batch and /upload-and-quote(-json), and by GET /environments/{env_id} |
cad_file_not_step |
422 | The 3D model is not a STEP file (no ISO-10303-21 header) — STEP is the only solid-model format a part is priced from. Export the part as STEP AP203, AP214 or AP242 and re-upload; a mesh (STL, OBJ) goes in as role=mesh and is priced on its envelope only |
DXF flat patterns
Raised by the upload routes when the CAD file is a DXF flat pattern (POST /calculations/upload-and-quote(-json), POST /parts/{part_id}/revisions/{revision_id}/datasets?role=cad_3d) — at upload, before anything is stored. See Uploading files → DXF flat patterns.
| Code | Status | Meaning |
|---|---|---|
dxf_gauge_required |
422 | A DXF carries no thickness: send sheet_gauge_mm (millimetres, above 0) |
dxf_material_required |
422 | A DXF names no material and has no title block to read one from: send material_grade_id or material_ref |
dxf_unreadable |
422 | The file does not start as a DXF, or ezdxf cannot read it (details.reason). Export the flat pattern as DXF (ASCII or binary) and re-upload |
dxf_units_unsupported |
422 | $INSUNITS (details.insunits) is neither 4 (millimetres) nor 1 (inches) — unitless files included. Export with the drawing unit set |
dxf_no_contour |
422 | The file draws no closed contour (details.entities geometric entities were read) |
dxf_open_loop |
422 | A contour of lines and arcs does not close: no entity continues from details.loose_end_mm (after details.chain_points points). Close the contour or join the gap |
dxf_multiple_outer_contours |
422 | The file draws more than one outer contour (details.contours closed contours; details.reason is second_outer, crossing, island — a contour inside an opening — or openings_cross). One part per file: a nest of several parts is a job nest's output, not its input |
Stock formats
Raised by /stock-formats, and — stock_format_unresolved — by every calculation entry point that accepts stock_format_id. Where a refusal is about one format, details.stock_format_id names it.
| Code | Status | Meaning |
|---|---|---|
stock_format_not_found |
404 | No format with this id is visible to your organization. Editing or retiring a standard format answers the same way: standard formats can be hidden, but not edited or retired |
stock_format_key_taken |
409 | One of your current formats already uses this key (details.key). Choose another key, or edit that format |
stock_format_superseded |
409 | The version you named is closed: it was retired, or replaced by a newer version when its size, gauge band or material changed. Past calculations were priced on it as it is, so it can't be changed, retired again or hidden — act on the current version, which GET /stock-formats lists. Its label can still be corrected |
stock_format_last_remaining |
409 | The change would leave your organization no format to price with. Add a format of your own, or show a hidden standard format again (DELETE /stock-formats/{id}/suppression), then retry |
stock_format_own_row_use_retire |
409 | Only standard formats can be hidden. To stop using one of your own formats, retire it with DELETE /stock-formats/{id} — it stays on record for the calculations priced on it |
stock_format_unresolved |
422 | The stock_format_id you sent names no format your organization prices with today: it does not exist, was retired, or is a standard format you have hidden. An id from an older version of a format you still buy is not refused — it prices on the current version. Pick a format from GET /stock-formats, or omit the field to price on the format that uses the least material. Raised identically by POST /calculations, /quote, /batch and /upload-and-quote(-json), before anything is stored. On a re-run (derived_from_calculation_id) that omits the field, the format kept from the calculation re-run is checked the same way, and details.derived_from_calculation_id names that calculation — send stock_format_id to choose another format, or null to price without one |
Scrap prices
Raised by PUT and DELETE /environments/{env_id}/rates/scrap/{material_category}/{scrap_class}, and — scrap_price_use_scrap_route — by the generic rate routes. Where a refusal is about one price, details names its material_category, scrap_class and grade_id.
| Code | Status | Meaning |
|---|---|---|
scrap_price_backdated |
422 | valid_from lies before today (details.valid_from, details.today). A scrap price starts today or later, so quotes already made keep the price they were costed at. Send today's date, a later one, or none |
scrap_price_grade_category_mismatch |
422 | The grade_id you sent is not of the material_category in the path (details.grade_category). State the price under the grade's own category |
scrap_price_not_found |
404 | The environment states no price for this material and kind of scrap from today on, so there is nothing to clear. The price in use comes from a parent environment or the platform — GET /environments/{env_id}/rates/scrap says which |
scrap_price_use_scrap_route |
422 | POST /environments/{env_id}/rates and DELETE /environments/{env_id}/rates/{kind}/{rate_id} do not take kind scrap: they would stack a second price, or erase one quotes were costed at. Use PUT or DELETE /environments/{env_id}/rates/scrap/{material_category}/{scrap_class} |
scrap_price_refresh_unconfigured |
409 | Platform operators only: no scrap price source is configured, so there is nothing to refresh |
scrap_price_refresh_unparsable |
422 | Platform operators only: the source document could not be read (details.reason). Nothing was written |
Job nesting and remnants
Raised by /nest-runs (nesting a backlog of parts together, sheet by sheet) and /remnants (the remnants a shop keeps in stock). POST /nest-runs/preview refuses exactly what starting the same job nest with POST /nest-runs would, by the same codes, and stores nothing.
| Code | Status | Meaning |
|---|---|---|
nest_run_not_found |
404 | No job nest with this id in your organization |
nest_run_selection_not_found |
404 | The environment, batch or folder you named is not your organization's (details names which) |
nest_run_items_unusable |
422 | Some items can't be nested. details.items names each by its index and calculation_id, with a reason: not_found, other_environment (priced in another environment than the one you named), not_succeeded (not priced yet), not_sheet (not cut from sheet), no_stored_layout (no layout was stored with its price), material_provided (the customer supplies the material) or missing_facts (its price does not state the thickness, the material price or the spacing a job nest reads); adding a part to a solved job nest's order also answers other_group (another material, thickness or margin than the material and thickness named, or a wider web than it was nested at). Nothing was queued; send the others |
nest_run_nothing_to_nest |
422 | Nothing in the batch or folder can be nested in this environment. details.left_out names each calculation with its reason, as for nest_run_items_unusable |
nest_run_selection_too_large |
422 | The batch or folder holds more calculations than one job nest takes (details.lines, details.max_lines). Nothing was queued; nest it in smaller selections |
nest_run_line_too_large |
422 | A line asks for more copies than one job nest line takes (details.max_copies_per_line). details.lines names each such line by its index and calculation_id, with the quantity it asked for — stated, or its calculation's lot size. Nothing was queued; nest it with a smaller quantity |
nest_run_too_many_copies |
422 | The lines together ask for more copies than one job nest takes (details.copies, details.max_copies_per_run). Nothing was queued; nest it in smaller selections |
nest_run_formats_unknown |
422 | A format the job nest was told to buy from is not a sheet format your organization buys today (details.formats names each). Nothing was queued; name formats from GET /stock-formats?kind=sheet |
nest_run_budget_too_long |
422 | group_budget_s asks for more computing time per material and thickness than your organization may (details.group_budget_s, details.max_group_budget_s). Nothing was queued; ask for the ceiling or less |
nest_run_in_flight |
409 | A job nest of your organization is still queued or running (details.run_id, details.status): one at a time. Nothing was queued; wait until it ends, or cancel it |
nest_run_too_many_nests |
422 | The selection would nest more materials and thicknesses than one job nest takes (details.nests, details.max_nests). Nothing was queued; split it |
nest_run_too_many_drafts |
409 | This month's runs are used, so the start would be kept as a draft — and your organization already keeps as many drafts as it may (details.drafts, details.max_drafts). Start one once runs are available, or cancel one, then start this one |
nest_run_not_draft |
409 | POST /nest-runs/{run_id}/start named a job nest that is not a draft (details.status): it was started already or has ended |
nest_run_not_cancellable |
409 | The job nest has already ended (details.status) |
nest_run_group_not_found |
404 | No material and thickness with this id in the job nest (details.group_id) |
nest_run_revision_not_found |
404 | The material and thickness has no revision with this number (details.revision); GET …/revisions lists them |
nest_run_not_editable |
409 | The job nest's sheets cannot be adjusted or released now: details.reason is not_ended (wait until it ends, or cancel it) or group_not_solved (this material and thickness is not nested) |
nest_run_released |
409 | The job nest was released (details.counted_at): its stock is booked, its sheets are frozen, and it cannot be a simulation again. Start a new job nest to lay them out differently |
nest_run_remnant_unavailable |
409 | A remnant the job nest planned to cut is no longer in stock as planned (details.remnant_id, details.pieces, details.available, details.status). Nothing was booked; adjust the sheets or nest again, then release |
nest_run_internal |
409 | The job nest is an assembly calculation's own (details.run_id): it prices the assembly's sheet components, so it is never released, kept a simulation, cancelled, started or given a new layout. Nothing was changed; start a job nest of the parts to cut them together |
nest_layout_invalid |
422 | The posted layout cannot be cut, or breaks your shop practice: details is the check POST …/layout-check answers — every sheet with its clashes (a, b, gap_mm), outside placements, unknown_lines, whether its container is unknown, and against_shop_practice (placement, reason — grain_direction with the allowed_rot_deg that keep it, or mirroring); missing and extra lines (stated, never a refusal: a layout short of a line's quantity or over it is stored and priced on the copies it cuts); containers_unknown; remnants_over. Nothing was stored |
nest_line_not_found |
404 | No line with this id in the job nest (details.line_id) |
nest_line_on_sheets |
409 | The job nest's active sheets still place the line (details.line_id, details.placed): take its copies off the sheets and save them as a revision, then take the line out |
nest_line_priced |
409 | A calculation was priced on the line (details.line_id, details.calculations), so it stays in the job nest |
nest_run_nothing_to_export |
409 | No material and thickness of the job nest is solved yet (details.status), so there is no sheet to export. Wait until one is, or until it ends |
nest_run_sheet_not_found |
404 | sheet names no sheet of the job nest (details.sheet). The nest plan (format=json) names every sheet as <group>:<index> |
sheet_layout_not_found |
404 | The calculation's price stored no sheet layout to export |
sheet_nest_preview_unavailable |
409 | POST /calculations/{id}/sheet-nest/preview: the calculation cannot be laid out on another sheet. details.reason is not_succeeded, not_sheet (not cut from sheet stock), no_stored_layout (its price stored no layout), material_provided (the customer supplies the material) or missing_facts |
stock_format_out_of_bounds |
422 | A sheet size of your own cannot be ordered: details.reason is below_minimum (details.min_mm), above_maximum (details.max_mm), machine_cannot_hold (details.machine_bed_mm, the selected machine's bed) or off_increment_grid (details.increment_mm, the environment's cut-to-size ordering step). details.length_mm and details.width_mm restate the size |
nest_export_timeout |
503 | The cutting file is still being built: the file route waited 60 seconds. Ask again after details.retry_after_s seconds (also the Retry-After header), or ask the …/export-url route, which answers at once (202 while the file is built) |
nest_export_failed |
500 | The cutting file could not be built (409: the layout changed while it was built). Ask again with fresh=true to build it again |
nest_export_unavailable |
503 | Cutting files cannot be queued for building right now. Ask again in a moment |
nest_item_unresolved |
422 | nest_item_id names no job-nest line that can price this calculation. details.reason says why: not_found (no line of your organization has this id), other_environment (it was nested in another costing environment), not_solved (its job nest has not solved its material and thickness), not_placed (no copy of it is on a sheet), other_part_revision (it was nested as another part revision — every upload creates a new one, so the upload forms refuse every line), other_currency (it was priced in another currency than the environment's), material_provided (the customer supplies the material) or stock_format_pinned (a stock format is pinned too; send one of the two). On a re-run that kept its parent's line, details.derived_from_calculation_id names the parent; send nest_item_id null to price the part on its own sheet. Nothing was stored |
remnant_not_found |
404 | No remnant with this id in your organization |
remnant_not_in_stock |
409 | The remnant can't be scrapped: a running job nest holds it, it was consumed or scrapped already, or fewer pieces are in stock than you named (details.status, details.quantity) |
remnant_unresolved |
422 | The environment or the material grade you named is not one of your organization's (details.env_id or details.material_grade_id) |
remnant_code_invalid |
422 | GET /remnants/by-code/{code}: the code is not a remnant code — hexadecimal digits of the id, at least 8 (details.min_digits), or the whole id |
remnant_code_ambiguous |
409 | More than one remnant of your organization begins with this code (details.matches). Scan or type more of the id; a label's 12-digit code is unambiguous in practice |
nest_run_no_rests |
404 | GET /nest-runs/{run_id}/remnant-labels: the job nest put no rest into stock — it is not released yet (details.released), or its sheets leave no reusable rest, or the environment does not credit remnants |
Rate limits & server
| Code | Status | Meaning |
|---|---|---|
rate_limited |
429 | Burst rate limit — the per-caller weighted request budget for the current window is spent (details.limit, details.window_seconds). A pacing problem, not a plan problem: honour Retry-After and slow down. Plan-quota exhaustion is quota_exceeded (402). A different 429 payload appears when a submit would exceed your plan's pending-calculation bound (details.reason: "tenant_pending_bound", with pending, bound, retry_after_s, queue_position, and — for a grid — cells_requested): wait for in-flight runs to drain or submit a smaller grid, and don't branch on details.limit alone |
too_many_requests |
429 | Auth-route limit exceeded |
system_busy |
503 | Transient overload (connection pool / queue saturation) — the request was not processed and cost nothing. Honour Retry-After, then retry with backoff |
service_unavailable |
503 | Deploy window or brownout — retry |
storage_unavailable |
503 | File storage is temporarily unavailable — the upload/download was not processed and cost nothing. Retry with backoff |
internal_error |
500 | Unexpected server error — quote the request_id in a ticket. Retry with backoff |
http_error |
4xx/5xx | An HTTP error with no more specific code. Branch on the status, and read message for the reason |
Cost sheet, what-if preview and scenarios
Every refusal on GET /calculations/{id}/cost-sheet, POST /calculations/{id}/preview and /calculations/{id}/scenarios carries its code in error.details.reason and the sentence below in error.message. A preview refusal caused by one of the adjustments comes back as invalid_adjustments with the per-adjustment codes nested in error.details.refused[] (unknown_factor, out_of_band with band, not_a_number, unstamped_key, not_an_adjustment, no_channel_for_factor).
| Reason | Status | Meaning |
|---|---|---|
calculation_not_priced |
409 | The calculation has no price yet (queued, running, failed or cancelled), so there is nothing to read or change at any lot size. Run it first |
priced_under_earlier_model |
409 | The calculation was priced by an earlier version of the costing model, so a new price worked out from it would move by everything that version changed, not only by your change; create a new calculation for the part revision and run it — that one can be adjusted |
quantity_not_on_curve |
404 | The calculation was not priced at that quantity and records no closed form to restate it from; read it at a quantity its lot-size curve lists |
kernel_corrupt |
404 | The calculation records a closed form that cannot be read; the curve's own quantities still answer — run it again to rebuild the rest |
cost_sheet_unavailable |
409 | The run recorded too little to itemise its price at that quantity. Run it again for a full cost sheet |
invalid_adjustments |
422 | Nothing was priced: at least one change cannot be — details.refused[] names each one and why |
too_many_adjustments |
422 | More changes than one what-if answers exactly; details.limit is the most at once |
lot_size_out_of_band |
422 | The quantity is outside what a what-if prices; details.limit is the ceiling (the same one every calculation accepts) |
nothing_to_preview |
422 | Nothing was changed; the calculation's own cost sheet already carries that answer |
machine_key_unscoped |
422 | The calculation states a machine rate per machine; name the machine (details.stamped_keys lists the keys its sheet carries) |
route_machines_unknown |
409 | A machine rate can only be moved for a machine the calculation was priced on; details.keys names the ones that could not be matched |
geometry_not_reusable |
409 | The part data the calculation was priced from is no longer on hand; run the calculation again to make it adjustable |
geometry_expired |
410 | The part data was kept for a limited time and has since been removed (details.expired_at); run the calculation again |
inputs_changed_since_run |
409 | The part's files or its applied corrections changed after the calculation ran (details.roles, details.corrections, details.latest_at), so a what-if would no longer match its figures; create a new calculation for the part revision and run it to include the change |
environment_changed_since_run |
409 | The costing environment changed after the calculation ran (details.latest_at, the same time the calculation's environment_changed_since_run states), so a what-if would mix that change into its answer; create a new calculation for the part revision and run it — that one can be adjusted. A saved scenario records the same reason |
geometry_store_disabled |
501 | What-if pricing is not switched on in this deployment yet; an administrator can turn it on |
sheet_unavailable |
409 | The change produced no priceable result; try a different value |
not_priceable |
409 | The calculation could not be priced under the environment's current settings (check its bought-in operation prices), then run it again |
compute_budget_exceeded |
503 | Working the change out took longer than one request may spend; ask for fewer changes at once or a smaller quantity |
too_many_scenarios |
409 | The calculation already holds the most scenarios one may keep (details.limit); delete one first |
nothing_to_change |
422 | A scenario update named neither a new name nor new adjustments |
Debugging a request
- Capture
X-Request-IDfrom the response — we log every request with this id. - Read
error.detailsfor the offending field / limit. - Check the status page for incidents.
- File a ticket at
[email protected]with the request id and timestamp (redact secrets).
Retry guidance
| Code | Safe to retry? |
|---|---|
429 rate_limited / too_many_requests |
Yes — honour Retry-After when present, else exponential backoff |
503 system_busy / service_unavailable / storage_unavailable |
Yes — wait Retry-After when present, then exponential backoff |
5xx (other) |
Yes — exponential backoff (start 1s, cap 30s, ~5 retries) |
402 insufficient_funds |
Only after topping up the wallet |
4xx (other) |
No — fix the request first |
Pair idempotency keys with retries so repeated attempts don't create duplicate billable calculations. For ready-to-paste backoff loops (Python / TypeScript / Bash), see Rate limits → Retry strategy.
See also
- Idempotency — safe-retry contract.
- Rate limits — the 429 envelope.
- Authentication —
invalid_api_key,insufficient_scope.