Errors
Stable error codes, HTTP statuses, the error envelope and retry guidance.
Every error, from any endpoint, uses one envelope with a stable, enumerable code:
{
"error": {
"code": "insufficient_balance",
"message": "Top up your wallet to run this call.",
"request_id": "req_1f2e...",
"details": {"optional": "structured context"}
}
}
Branch on code, show message to humans. An error the message does not explain is worth a feedback report with its request_id (free); every error response carries Link: </feedback>; rel="agent-feedback" to point agents there. People can write to hello@routergrowth.com.
Codes
| Code | HTTP | Meaning | Retry? |
|---|---|---|---|
invalid_request |
400 | malformed body, unknown field or bad value | fix the request |
invalid_api_key |
401 | missing, malformed or revoked key | no |
invalid_credentials |
401 | bad dashboard sign-in | no |
forbidden |
403 | the key cannot do this (for example: sandbox-only routes on production) | no |
capability_not_found |
404 | unknown capability slug | no; discover first |
endpoint_not_found |
404 | unknown raw endpoint for that provider | no; list /v1/catalog/providers/{provider}/endpoints |
no_match |
404 | providers answered authoritatively: no result | not with the same input |
insufficient_balance |
402 | reservation exceeds available balance | after a top-up |
cost_limit_exceeded |
402 | the run's hold exceeds the workspace's per-run ceiling; the run is blocked |
lower the call or raise the ceiling at /v1/spend-guard |
budget_exceeded |
402 | the daily spend guard would be breached; the run is blocked |
after the UTC reset, or raise the cap |
suppressed |
403 | a recipient of the send is on the workspace's suppression list; the run is blocked, nothing charged |
do not send; remove the suppression only if the person asked to hear from you again |
opted_out |
403 | the recipient of the send opted out with RouterGrowth itself (privacy policy); binds every workspace; the run is blocked, nothing charged |
no |
repeat_touch |
409 | a new conversation with someone contacted on the same channel inside the repeat window; the run is blocked, nothing charged |
reply in the existing thread, or pass routing.allow_repeat with the user's say-so |
paced |
429 | a send from a connected account inside its minimum gap or over its day's ceiling (send pacing); the run is blocked, nothing charged |
after details.retry_after seconds |
duplicate_request |
409 | conflicting concurrent request | no |
not_found |
404 | no such route | no |
payload_too_large |
413 | request body above 1 MB | with a smaller body |
provider_not_available |
409 | no live provider for this capability yet, or a provider not exposed raw | later |
rate_limited |
429 | too many requests from your client (counted per key, with a wider cap shared by every key on one IP; per IP for calls without a key, and tighter on sign-in, code requests and OAuth registration); Retry-After says when |
after Retry-After |
provider_rate_limited |
429 | the upstream provider throttled us | with backoff |
provider_error |
502 | the upstream provider failed; nothing charged | yes, with an Idempotency-Key |
provider_timeout |
504 | the upstream provider timed out; nothing charged | yes, with an Idempotency-Key |
internal_error |
500 | our fault; nothing charged without settlement | yes, with an Idempotency-Key |
interrupted |
none (on the run) | the router restarted while the run was executing; the run is failed and its hold released |
yes, with a new Idempotency-Key |
Retrying safely
Retry provider_error, provider_timeout, internal_error and both rate limits with exponential backoff. When the answer was lost (a timeout, a dropped connection), resend the same Idempotency-Key: a retry that races a completed run gets the original result back (Idempotent-Replay: true) instead of a second charge. When you did get a failed run back, retry with a new key: the old one returns that same failed run.
On auto routing, most provider failures never reach you: a rate-limited or 5xx provider is retried once after a short wait, and the waterfall tried the next-best provider before returning an error. When max_cost kept a fallback from running, the error message names it and the cap that admits it: raise the cap once rather than resubmitting the same call.
Send pacing
Sends from a connected account (linkedin.invite, linkedin.message, instagram.message) keep a minimum gap and a daily ceiling on the platform side, whatever the agent asks for. A day here is the UTC calendar day: the count starts again at 00:00 UTC, and the whole allowance comes back at once. A send inside the gap, or past the ceiling, ends as a blocked run with error paced: nothing is reserved, nothing reaches the network. details.retry_after says how many seconds to wait (with jitter, so the next send does not land on a fixed interval either), and details.sent_today / daily_cap (with resets_at, the next 00:00 UTC) or sent_this_week / weekly_cap say which ceiling held. The week is the last 7 days, rolling. Runs still in flight count as sent, so a parallel burst is refused from its second call.
| Capability | Gap | Per day, reset at 00:00 UTC (account connected under 7 days / under 30 / older) | Per 7 days |
|---|---|---|---|
linkedin.invite |
60 s | 20 / 30 / 30 | 180 |
linkedin.message |
15 s | 40 / 80 / 150 | none |
instagram.message |
30 s | 20 / 50 / 100 | none |
These sit under LinkedIn's and Instagram's own limits, which are unpublished and tighten on a freshly connected or bursting account. Sandbox keys are not paced. The ceilings count sends made through RouterGrowth; what the account sends elsewhere still counts for the network.
Reading this as an agent? This page as markdown: /docs/api/errors.md · every page: /docs/llms.txt