Steerd API

Error catalog

The single error envelope and every stable error code.

Every error response uses a single envelope:

{ "error": { "code": "…", "message": "…" } }

code is a stable, machine-readable identifier; message is a human-readable description and may change. Branch on code (and the HTTP status), never on message.

Codes

codeHTTPWhen it happens
unauthorized401Missing, malformed, unknown, expired, or revoked API key, or a key whose creator is no longer an active member of its team.
forbidden403The key lacks the scope the endpoint requires (for example, a read-only key attempted a write).
bad_request400Request body or query failed validation; or a cursor that does not match the requested sort/order.
not_found404The resource does not exist, or is not in the key's team.
limit_reached402The team is at its plan's limit for the resource being created. Carries the billing fields.
feature_unavailable403The team's plan does not include a capability the request needs, api_access included. Carries the billing fields.
conflict409A request with the same Idempotency-Key is still in flight; retry shortly.
key_limit_reached409The team already holds the maximum number of active API keys.
idempotency_key_conflict422An Idempotency-Key was reused with a different request body.
idempotency_authorization_changed409Your authorization narrowed since the original request, so its stored response cannot be replayed. Retry without the key, or with a new one.
rate_limited429A per-key or per-IP rate limit was exceeded. The response includes Retry-After and RateLimit-* headers.

Billing refusals

limit_reached and feature_unavailable add machine-readable fields to the envelope so a client can say what would lift the refusal instead of only reporting it:

{
  "error": {
    "code": "limit_reached",
    "message": "You've reached your plan's limit for cvs.",
    "resource": "cvs",
    "limit": 10,
    "current": 10,
    "plan": "solo",
    "upgrade_to": "team"
  }
}
{
  "error": {
    "code": "feature_unavailable",
    "message": "Your plan doesn't include API access.",
    "feature": "api_access",
    "plan": "free",
    "upgrade_to": "solo"
  }
}
FieldOnMeaning
resource402The exhausted ceiling. storage_bytes reports bytes; invoices_per_month and trips_per_month are per-calendar-month windows; the rest are row counts.
limit402The plan's ceiling for resource.
current402The team's usage against it, in the same unit as limit.
feature403The capability the plan does not include.
planbothThe plan in force.
upgrade_tobothThe tier that lifts this refusal, or null when none does. On a 402 it is the lowest tier whose ceiling for resource accommodates the request, which is not always the adjacent tier: a team can sit above the next tier's ceiling too, and some ceilings are the same on neighboring tiers. Read it as the answer, not as "one step up".

The two are different situations for the customer: limit_reached means the plan includes the resource and the team has used it all, so deleting something also resolves it; feature_unavailable means the plan never included the capability, so only an upgrade does.

Notes

  • 404, not 403, for cross-team resources. An id that belongs to another team returns 404, so the API never reveals whether an id exists elsewhere.
  • Validation failures are 400 bad_request. The message summarizes the invalid field(s); do not parse it; treat it as human-facing.

For GraphQL, errors appear in the errors array with the same codes under extensions.code. See the GraphQL guide.

On this page