Errors and rate limits
Every response from the API carries the same two-shape envelope, and every failure names itself with a stable code. Read the code, not the message: messages are written for humans and change, codes are a contract. Failed calls are never billed.
One shape for success, one for failure
A success is success: true with the payload under data, and, on the metered data surfaces, a meta block carrying what the call cost and what is left. A failure is success: false with a single error object. There is no third shape, and no endpoint returns a bare payload.
{
"success": true,
"data": { },
"meta": {
"costCents": 1.5,
"purchasedBalanceCents": 4821.5,
"subscriptionRemainingCents": 1000,
"cached": false,
"latencyMs": 412
}
}error.details appears on validation failures: one entry per offending field, each with field, location (query, path or body) and message. error.retryAfter appears whenever waiting is the right move, and the same number of seconds is sent as a real Retry-After header so an HTTP client can honour it without parsing the body.
The complete code table
These are all of them. A code you do not see here is not something the API emits. Statuses in play across the surface are 400, 401, 402, 403, 404, 409, 410, 422, 429, 500, 502 and 503.
| Code | HTTP | What it means | What to do |
|---|---|---|---|
VALIDATION_ERROR | 400 | A parameter is missing, malformed, or the wrong type. | Read error.details — one entry per bad field, with its location and what was expected. Do not retry unchanged. |
UNAUTHORIZED | 401 · 403 | 401 when the Authorization header is missing, malformed, or names a key that does not resolve. 403 when the key resolves but is not allowed to do this. | Send Authorization: Bearer <key>. On /v1/* a 401 also carries how_to_get_access with the MCP endpoint, signup, docs and OpenAPI URLs. |
INSUFFICIENT_CREDITS | 402 | Your organization's balance cannot cover the call. The message carries the required and available amounts. | Top up the wallet, then retry. Never retry on a timer — the balance does not change on its own. |
PLAN_LIMIT_REACHED | 402 | Creating this resource would exceed a plan cap (workspaces, agents, databases, webhook events, runtime hours). | The message names the resource and the used/limit pair. Upgrade the plan or delete something. |
SEAT_LIMIT_REACHED | 402 | An invite would push occupied seats plus pending invites past the billed seat capacity. | Add a seat, or upgrade to a plan with a seat add-on. |
DEPLOY_GATE_FAILED | 402 | Deploying an agent needs a trial, AI credits, or a plan. | Start the trial or add the module the message names, then deploy again. |
SUBSCRIPTION_REQUIRED | 402 | A paid surface was reached by an organization with no plan. | Subscribe, or start the trial, then retry. |
INVITATION_EMAIL_MISMATCH | 403 | The signed-in account is not the address the invitation was sent to. | Sign in as the invited address and open the link again. |
NOT_FOUND | 404 | No such route, or no such record in this workspace. | Check the path and the id. A record from another workspace reads as missing, not forbidden. |
PROVIDER_NOT_FOUND | 404 · 503 | 404 when the source slug does not exist (the message lists the ones that do). 503 when the slug exists but is not currently serving. | Use a slug from GET /v1/platforms. On a 503 the source is temporarily out; retry later. |
ENDPOINT_NOT_FOUND | 404 | The source exists, but not that method and path on it. | Check the endpoint against the API reference for that source. |
ENDPOINT_MIGRATED | 410 | A retired path with a direct successor. The message and a Link header name the new one. | Move to the successor path. Parameters and key are unchanged. |
CONFLICT | 409 | The request contradicts current state (a duplicate, a race). | Re-read the resource and decide; retrying as-is will not help. |
TRIAL_ALREADY_USED | 409 | This organization has already used its one free trial. | Subscribe instead. Trials are once per organization, ever. |
RATE_LIMITED | 429 | The workspace exceeded its requests-per-minute ceiling. | Wait retryAfter seconds (also sent as a Retry-After header), then retry. |
PROVIDER_ERROR | 422 · upstream 4xx · 502 | The upstream source rejected or failed the call. 422 when it answered 200 with a failure body; its own 4xx passes through; 5xx becomes 502. | Fix the inputs on a 4xx. On a 502 retry with backoff. Nothing was billed either way. |
PROVIDER_UNAVAILABLE | 503 | A dependency we call is unreachable or not configured. | Retry with backoff. |
UPSTREAM_CREDITS_EXHAUSTED | 503 | Our own account with the upstream provider is out of quota. Deliberately not a 402, so it is never read as your balance. | Retry shortly. The outage is ours and pages our team; the call cost you nothing. |
INTERNAL_ERROR | 500 | An unhandled failure on our side. | Retry once with backoff. If it persists, send us the path, the time and the parameters — we correlate it to our own logs. |
Two different ways to be out of money
INSUFFICIENT_CREDITS (402) is your balance. The message states exactly what the call required and what was available, so an agent can relay a real number to its human. Nothing happens until someone tops up.
UPSTREAM_CREDITS_EXHAUSTED (503) is our account with the upstream provider running dry. It is deliberately not a 402, because a passed-through 402 would read as your problem and send you to a billing page that cannot fix it. The customer-facing message says only that the source is temporarily unavailable; on our side it pages the team.
rule of thumb
What an upstream failure looks like, and what it costs
Curated data sources sit behind upstream providers, and their failures are normalized into one code rather than leaked raw:
- A 200 carrying a failure body becomes PROVIDER_ERROR with status 422 — the provider said yes and meant no.
- An upstream 4xx passes its own status through as PROVIDER_ERROR, so a 404 from the source stays a 404.
- Upstream 5xx responses, network errors and timeouts all become 502.
- Our own quota exhaustion becomes UPSTREAM_CREDITS_EXHAUSTED (503), never a 402.
billing
One ceiling per workspace, per minute
The limiter counts requests in a fixed 60-second window, keyed by workspace. Every API key in a workspace shares that one counter, so minting more keys buys no extra throughput. The ceiling is the plan's requests-per-minute figure from the billing catalog.
| Plan | Requests / minute |
|---|---|
| Pay as you go | 60 |
| Toolkit | 300 |
| Toolkit All-Access | 600 |
| Solo | 120 |
| Team | 300 |
| Business | 600 |
limited
- /v1/data/*
- /v1/data/agnt/*
- /v1/connections/*
- /v1/db/*
not limited
- /v1/credits
- /v1/usage
- /v1/platforms
- /v1/webhook-endpoints/*
HTTP/1.1 429 Too Many Requests
Retry-After: 37
{
"success": false,
"error": {
"code": "RATE_LIMITED",
"message": "Rate limit exceeded",
"retryAfter": 37
}
}not ours
/v1/connections/* call is the vendor throttling your own credentials, not us. Backing off still helps; raising your superagnt_ plan does not.A retry helper that knows when to stop
Honour Retry-After when it is there, back off on 429, 502 and 503, and never retry a 400 or a 402 — a malformed request and an empty wallet do not heal by waiting.
const RETRYABLE = new Set(["RATE_LIMITED", "PROVIDER_UNAVAILABLE"]);
export async function callAgnt(path: string, init: RequestInit = {}) {
for (let attempt = 0; ; attempt++) {
const res = await fetch("https://api.superagnt.com" + path, {
...init,
headers: {
...init.headers,
Authorization: `Bearer ${process.env.AGNTDATA_API_KEY}`,
},
});
const body = await res.json();
if (body.success) return body.data;
const code = body.error.code;
const retryable =
RETRYABLE.has(code) || res.status === 502 || res.status === 503;
// A 400 is your request and a 402 is your balance: neither heals by
// waiting, so stop immediately.
if (!retryable || attempt >= 4) throw new Error(`${code}: ${body.error.message}`);
const headerWait = Number(res.headers.get("Retry-After"));
const waitSeconds = Number.isFinite(headerWait) && headerWait > 0
? headerWait
: body.error.retryAfter ?? 2 ** attempt;
await new Promise((r) => setTimeout(r, waitSeconds * 1000));
}
}Related: workspaces and organizations for where the plan and the wallets live, and the API reference for per-endpoint parameters and pricing.