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, 499, 500, 502 and 503.
| Code | HTTP | What it means | What to do |
|---|---|---|---|
VALIDATION_ERROR | 400 | A parameter is missing, malformed, the wrong type, or a value the data source does not accept. We check before calling the source, and a source that refuses your input answers the same way. | 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. Builder plans and the trial get DATA_CREDITS_EXHAUSTED instead. | Top up the wallet, then retry. Never retry on a timer: the balance does not change on its own. |
DATA_CREDITS_EXHAUSTED | 402 | On a builder plan or the trial, the data credits it includes cannot cover the call. Nothing was returned and nothing was charged. | Send a person error.upgrade.confirm_url. If the offer shows credits left, a cheaper call can still go through. Never retry on a timer. |
PLAN_LIMIT_REACHED | 402 | Creating this resource, or this period's usage, would cross a plan cap. On builder plans every published limit counts; on managed plans it is workspaces, agents, databases and webhook events. | On a builder plan, error.upgrade names the resource, the used and cap figures, and the next plan. Upgrade, or remove something. |
PLAN_REQUIRED | 402 | No running plan covers this call: the trial ended or was never available, the plan lapsed, or the organization has none. Everything is paused and nothing is deleted. | Send a person error.upgrade.confirm_url. Calls work again the moment a plan is active. /v1/credits and /v1/usage keep answering meanwhile. |
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 plan, a trial or a credit package. | Choose a plan (or start the trial), then deploy again. |
SUBSCRIPTION_REQUIRED | 402 | A paid surface was reached by an organization with no plan. | Choose a plan, then retry. |
CAPABILITY_UNAVAILABLE | 409 | The workspace's database backend does not offer this feature (file storage buckets or advisors on the shared database, for example). No plan changes it. | Do it another way, for example store the data in a table. Retrying will not help. |
VENDOR_CREDENTIAL_REJECTED | 400 | A connected vendor refused, or could not confirm, the connection's credential when it was checked live (starting or previewing a sync, for example). The body carries error.vendorCredential with provider (the connection id) and reason: rejected, no_answer, wrong_key_type or plan_required. Nothing was created. | On rejected or wrong_key_type, replace the key or reconnect the account on the connection page. On no_answer, retry in a minute. On plan_required, the vendor's own plan needs API access. |
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. |
CONFLICT | 409 | The request contradicts current state (a duplicate, a race). | Re-read the resource and decide; retrying as-is will not help. |
INTEGRATION_NOT_CONNECTED | 409 | The call needs a vendor this workspace has not connected, or whose connection no longer works (turning on a sync, or a connection tool). Over MCP it comes back as a tool error with a connect_url. | Connect the vendor (or reconnect it) on its connection page, then retry. |
WEBHOOK_ENDPOINT_MANAGED | 409 | The webhook endpoint belongs to a vendor sync, which creates and retires it. Deleting, deactivating, reactivating, rotating or revealing its secret, and linking it to a data job are refused. Linking it to an agent still works. | Manage it from the Sync panel on the vendor's connection page (the message names it). Turning the sync off removes the endpoint. |
TRIAL_ALREADY_USED | 409 | This organization has already used its one managed-plan trial. | Subscribe instead. The managed trial is once per organization, ever. |
SUBSCRIPTION_PAST_DUE | 409 | A plan checkout was started (in the dashboard or from a confirm_url) while the organization's builder plan has an unpaid invoice. The body carries error.billingPortalUrl. | Update the payment method in the billing portal. That pays the open invoice and the plan comes back on its own. |
RATE_LIMITED | 429 | The organization (builder plans and the trial) or the workspace (managed plans) exceeded its requests-per-minute ceiling, the data source is throttling requests right now, or the balance covers a data call but the organization's other running calls still hold its credits. | Wait retryAfter seconds (also sent as a Retry-After header), then retry. |
PROVIDER_ERROR | 503 · connection 502 or 4xx | A data source failed the call: an outage, a server error, a timeout, or a failure it reported inside a successful answer. We have already retried the failures a retry usually fixes. On a connection (your own vendor credential) a vendor 5xx or no answer is a 502, and the vendor's 4xx passes through with its status. | On a 503 or a 502 retry with backoff. On a connection 4xx read the vendor's message and fix the call or the credential. Nothing was billed. |
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. |
CLIENT_CLOSED_REQUEST | 499 | Your connection closed before the data source answered, so the call was stopped. Your client never reads this response; your usage shows the call as failed. | Nothing to do: it was not charged. If your client gives up too early, raise its timeout. |
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 and we correlate it to our own logs. |
Out of money, at a limit, or out of plan
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.
On a builder plan or the trial the plan itself is the budget, so a refusal comes with a way forward. DATA_CREDITS_EXHAUSTED, PLAN_LIMIT_REACHED and PLAN_REQUIRED carry an upgrade offer under error.upgrade: why the call stopped, the next plan and what it costs, and a confirm URL for a person. Over MCP the same offer is the tool result itself, not an error, so the agent relays it instead of burying it.
HTTP/1.1 402 Payment Required
{
"success": false,
"error": {
"code": "DATA_CREDITS_EXHAUSTED",
"message": "The data credits included in the Starter plan are used up for this billing period; Growth ($149/mo) includes $90 of data credits every month.",
"upgrade": {
"status": "requires_upgrade",
"reason": "included_credits_exhausted",
"message": "The data credits included in the Starter plan are used up for this billing period; Growth ($149/mo) includes $90 of data credits every month.",
"current_tier": "builder_starter",
"upgrade": {
"tier": "builder_growth",
"name": "Growth",
"price_usd_month": 149,
"included_data_credits_usd": 90,
"unlocks": { /* every limit Growth raises */ }
},
"confirm_url": "https://app.superagnt.com/confirm/<token>",
"human_step": "The data call did not run and returned no data: this billing period's included data credits are used up. Data calls resume when the plan renews or right after an upgrade. Nothing was charged. Share the confirm_url with a person on this account: they review Growth there and confirm it. You never pay or change the plan yourself. Retry once they have confirmed."
}
}
}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
Every reason an offer can carry is listed on Plans and limits.
What an upstream failure looks like, and what it costs
Curated data sources sit behind upstream providers. Their answers are classified, never passed through raw:
- A valid query that matched nothing is a success: 200 with the source's empty answer, meta.empty set and meta.notice saying so. It is not an error, so do not retry it.
- A source that refuses your input (a value it does not accept, an identifier in the wrong form) becomes VALIDATION_ERROR (400) naming the field.
- A source throttling requests becomes RATE_LIMITED (429) with retryAfter, after we have waited and retried ourselves.
- Upstream server errors, network errors, timeouts and failures reported inside a 200 become PROVIDER_ERROR (503). We retry the ones a retry usually fixes first.
- Our own quota exhaustion becomes UPSTREAM_CREDITS_EXHAUSTED (503), never a 402.
- If your connection closes before the source answers, the call is stopped and not charged.
billing
One ceiling per minute, sized by the plan
The limiter counts requests in a 60-second window against the plan's requests-per-minute figure from the billing catalog. On a builder plan or the trial there is one counter per organization, shared by every API key and every MCP connection in every workspace. On a managed plan the counter is per workspace and covers REST. A Toolkit subscription is counted per organization like a builder plan, and an organization with no plan per workspace like a managed plan. Either way, minting more keys buys no extra throughput.
| Plan | Requests / minute | Counted per |
|---|---|---|
| Starter | 60 | organization · REST + MCP |
| Growth | 300 | organization · REST + MCP |
| Scale | 600 | organization · REST + MCP |
| No-card trial (Growth limits) | 300 | organization · REST + MCP |
| Solo | 120 | workspace · REST |
| Team | 300 | workspace · REST |
| Business | 600 | workspace · REST |
Over REST, going past the ceiling returns the 429 below. Over MCP on a builder plan it returns an upgrade offer with reason rate_limited and retry_after_seconds, so the agent can wait or ask for the next plan.
limited
- /v1/data/*
- /v1/data/agnt/*
- /v1/connections/*
- /v1/db/*
- MCP tool calls (builder plans)
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
}
}The window is fixed: it opens with the first request and resets 60 seconds later. Past the ceiling, every request gets this 429 until the reset, and retryAfter is the seconds left until then. A burst that spends the budget early waits out the rest of the minute, so pace a long loop instead.
A repeated call counts again: responses are not served from a cache (meta.cached is always false), so keep results you will reuse. A bulk call counts once for all its records: see bulk calls for people and companies.
not ours
/v1/connections/* call is the vendor throttling your own credentials, not us. Backing off still helps; raising your superagnt plan does not.What an agent should do when it is rate limited, over REST or MCP, is on Rate limits in troubleshooting.
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, an empty wallet and a plan limit 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 or your plan: 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.