02 · CONCEPTS

Errors

Every non-2xx response has exactly one shape. Codes tell your agent what to do next — most of them are not “try again.”

Error envelope
{
  "error": {
    "code": "insufficient_credits",
    "message": "available credits below the maximum cost of this call",
    "request_id": "…",      // when a request was created
    "details": { }              // code-specific context
  }
}
1

Codes & the retry matrix

CodeHTTPRetry?What to do
unauthorized401noMissing or revoked key. Create one in the console.
forbidden403noKey lacks the required scope, or the workspace needs review for external actions.
confirmation_required403after approvalA human must approve this action in the console. Get a fresh preview, have them approve, retry with the token.
insufficient_credits402after top-upTop up in the console, then retry. A data call (search, enrichment, verification, research, pictures, LLM) keeps its Idempotency-Key: once the balance covers the call, the same key runs it as a new attempt, with no cooldown. For setup_sending use a new key; quote tokens survive this — no need to re-quote.
rate_limited429yes, backoff300 requests/min per key, plus daily caps. Back off (details.retry_after_seconds when set) and retry: a data call with the same Idempotency-Key runs again once that time has passed, a send-class call needs a new key.
idempotency_conflict409noSame Idempotency-Key, different parameters. Use a new key for a new request.
invalid_request400noSchema/validation failure — the message says which field.
suppressed_recipient422noRecipient is suppressed. This is permanent and by design.
not_found404noWrong id, or it belongs to another workspace.
operation_unknown_state409poll, don't retryExternal outcome is ambiguous and being reconciled. Poll the request/operation; never blind-retry a purchase, launch or reply.
reply_already_sent409noThis inbound reply already has an answer in flight or sent. One reply per inbound message; wait for their next message.
reply_content_unavailable409noThe provider message this reply would answer is not held, so it cannot be threaded. Read it with get_reply first.
reply_send_disabled403noIn-thread reply sending is switched off for this deployment or workspace.
mailbox_unavailable409noThe mailbox that received this reply is no longer an active sending asset in your workspace. Layrcake never substitutes another sender.
agent_suspended403noThis agent's paid plan is suspended for non-payment (seven days after its paid-through date). Every REST/MCP verb refuses; the console still works. Update the card on Billing to restore it.
card_required402after saving a cardA paid item — a third agent, mailboxes, domains — needs a saved card, or the saved card was declined. An owner adds or updates it on the console's Billing page; a pending order is placed automatically once the payment succeeds.
payment_required402after paymentServices are stopped for unpaid billing. The message names what is owed and links to Billing. Campaigns, launches, replies and paid lookups stop; reads, exports, waiting approvals, mailboxes and domains are kept. Paying lifts the billing stop but does not resume campaigns. This behavior ships off (NON_PAYMENT_STOP).
payment_action_required402after confirmingThe bank requires the customer to confirm the payment on-session. Layrcake never retries it unattended; the console's Billing page completes it.
billing_unknown409poll, don't retryA card payment's result cannot be proven yet and is being reconciled with Stripe. Do not resubmit: the order or key is created automatically once the outcome is known.
stream_consumed409yes, new keyWhere hosted agents are enabled: the streamed /v1/llm/chat call that used this Idempotency-Key already finished, and a stream is never replayed. details.credits_charged says what it cost. Retry with a new Idempotency-Key; that is a new, separately charged call. A key whose call is still running answers 202 in_progress instead. A stream the gateway had to cut short ends with a chunk whose finish_reason is error and whose error.metadata.retryable is true.
agent_paused403after resumeWhere agents are on: this key belongs to an agent a person paused. Nothing it has queued sends until it is resumed on the Agents page. See Pause and resume.
run_budget_exceeded403noThis call would take the agent run named in X-Layrcake-Run over its budget, model spend included. The run is stopped. See The per-run budget.
run_unavailable403noThe agent run has ended, or it belongs to another agent. Start a new run for the next piece of work.
run_required403after starting a runThis key spends only inside a run. Start one with POST /v1/agent/runs and send its id in X-Layrcake-Run. See Runs outside Layrcake.
approval_declined409ask againWhere Layrcake runs approved launches and replies itself: a person said no, so nothing was sent. details.approval.note may say why. Change the request if needed, then call again without confirmation_token to ask again (details.can_ask_again).
approval_expired409ask againNobody decided within the approval's lifetime, so nothing was sent. Call again without confirmation_token to ask again.
approval_superseded409noA newer version of the same request replaced this approval. Use details.superseded_by, the newer approval's token.
approval_cancelled409ask againThe approval was stopped before anything was sent: sending was paused, the key was revoked, or it couldn't start (details.reason). Call again without confirmation_token to ask again once the cause is fixed.
approval_changed409noThis request would replace an approval a person already approved. To replace it, call again with supersedes set to its token (details.approval.id).
provider_unavailable503yes, laterEvery route exhausted. Nothing was charged. A data call retried with the same Idempotency-Key runs again as a new attempt with its own request_id once its cooldown has passed: 30 s, or its own longer details.retry_after_seconds. Before that the key returns the failure again with details.replayed: true and the seconds left. If a provider may have done the work (it timed out), the same key never runs it again: the failure comes back with no retry hint; use a new key to try once more. The failed attempt stays readable at GET /v1/requests/{id}. A send-class call (setup_sending, create_campaign, add_leads, launch_campaign, reply_to_thread, set_lead_status) keeps its failure under that key: retry with a new key. details.reason, when present, says why (vendor_account · invalid_request · rate_limited · temporarily_paused · upstream_error) and details.retry_after_seconds is set when known. Vendor names are never exposed.
internal500yes, laterOur fault. The request_id helps support trace it.

Workspace monthly spend controls are implemented but off (SPEND_CAP_MODE=off). When enforcement is enabled, workspace_spend_cap refuses new paid lookups, models, mailbox/domain purchases and paid agents, with durable refusal/reset details. Free work, running campaigns, replies and approved launches continue; renewals and OAuth connections are never refused. Credit purchases do not count. Refunds reduce their original charge month in UTC. Near the cap, calls settling between the read and admission lock can exceed it by at most those calls’ quotes; renewals and OAuth are explicit exceptions. get_me exposes mode, settled spend, in-flight commitments and remaining room only with usage:read. A key cap is separate and may refuse free calls when already over. Spend emails remain DRY_RUN.

Reserved for the limits rollout: details.refusal distinguishes workspace_spend_cap, key_spend_cap and daily_limit_full from provider failures. When supplied, it and the ISO timestamp details.resets_at are preserved on replay and in request status under error_details. A monthly reset may be more than a day away. These optional fields do not enable new limits.

🔁 Retrying safely: reuse the same Idempotency-Key and you are never charged twice. A completed request replays its original result ("replayed": true); an in-flight one returns 202 in_progress. A data call that failed with provider_unavailable, rate_limited or insufficient_credits (once the balance covers it) runs once more as a new attempt with a new request_id after its cooldown: 30 s, or its own longer retry_after_seconds (insufficient_credits needs no cooldown, only the top-up). Concurrent retries share that one attempt. A retry during the cooldown gets the failure back with details.replayed: true and details.retry_after_seconds (the time left). A call whose provider may already have done the work (a timeout) is never run again under the same key. Any other replayed failure without a retry hint is final for that key — except insufficient_credits, which runs once a top-up covers it: fix the cause and use a new key.