View as markdown
Getting started

Errors & rate limits

One error envelope across every endpoint. Per-minute rate limits scaled by plan, surfaced via standard headers on every response.

Error envelope

The same shape on every endpoint, on every status code from 400 upward.

{
  "error": {
    "code": "invalid_query",
    "message": "Unknown field 'percent_change'. Did you mean 'day_change_pct'?",
    "request_id": "req_01J9PZA1F2C9V3XQ"
  }
}

Always include the request_id when you contact support. It points us at the exact log entry for your request.

Error codes

Stable, machine-readable codes. The HTTP status is set accordingly.

CodeStatusWhen you see it
bad_request400
The request was malformed: missing required parameter, invalid format, value out of range, or unsupported combination.
invalid_query400
The `q` clause failed to parse, references an unknown column, or uses a disallowed SQL feature (semicolons, comments, INSERT/UPDATE keywords).
unknown_signal404
The signal name in the path is not a column on `ticker`. Check /api/schema and /api/flags for valid names.
signal_not_available_at_interval400
You requested a fast resolution (1h or 1m) for a signal that is only computed daily. Slow-moving signals like SMAs, RSI, MACD, and fundamentals are daily-only.
history_window_exceeded400
You asked for data older than your plan's historical depth. The response includes `max_history_days` and the earliest allowed cutoff so your code can clamp the request. Hobby has no history (live snapshot only). Pro: 5 years. Scale and Enterprise: all-time. Five demo tickers (AAPL, TSLA, NVDA, SPY, BTC-USD) get full history on every plan.
unknown_tickers400
A universe POST/PATCH referenced symbols that are not on the active scanner. Check spelling and that the symbol is currently tracked.
Emitted by: Create a universe
resource_limit_reached403
You've hit the plan cap on a resource (universes, rules, or webhooks). Delete an existing one or upgrade.
slug_taken409
A POST tried to create a universe or rule with a slug that already exists in your account.
universe_not_found404
A `?universe=` parameter referenced a slug that is neither a system universe (`top_10`, `top_100`) nor one your account owns.
rule_not_found404
A `?rule=` parameter (or a webhook `rule_id`) references a rule that doesn't exist on your account.
premium_signal_required402
Your `q`, `order`, `fields`, or signal path references a column gated to plans with premium signals (Scale or above).
Emitted by: Create a rule
cadence_above_plan_max403
A webhook POST/PATCH asked for a faster cadence than the plan allows. The response includes the plan's maximum cadence so you can fall back.
index_building503
A Firestore composite index needed for a list query is still building (typically <1 minute on initial deploy). Retry shortly.
not_found404
The requested resource (ticker, webhook, delivery) does not exist or is not accessible to your API key.
unauthenticated401
The Authorization header is missing, malformed, or carries a key that has been revoked or never existed. API keys are passed as `Authorization: Bearer tb_(test|live)_<key>`.
subscription_required402
The key is valid but the owning account has no active subscription. Pick a plan in the dashboard or contact sales for Enterprise.
webhook_tier_required403
You tried to create a webhook on an account whose current plan tier does not include webhooks. Surfaces after a subscription is canceled and the account falls back to the no-webhook fallback tier.
universes_tier_required403
You tried to create a universe on an account whose current plan tier does not include user-defined universes. Surfaces after a subscription is canceled and the account falls back to the no-universes fallback tier.
rules_tier_required403
You tried to create a saved rule on an account whose current plan tier does not include saved rules. Surfaces after a subscription is canceled and the account falls back to the no-rules fallback tier.
scan_asof_tier_required403
You passed `?asof=` to `/v2/scan` on an account whose current plan tier does not include historical scan. Historical scan is included on Pro (5 years) and Scale (all-time). Hobby is live-only, but the five demo tickers (AAPL, TSLA, NVDA, SPY, BTC-USD) get full asof history on every plan.
webhook_limit_reached403
You've hit the maximum number of webhook subscriptions your plan allows. Delete an inactive one or upgrade to add another.
rate_limited429
You exceeded your plan's per-minute request limit. The `Retry-After` header indicates seconds until the next minute boundary.
internal500
Something went wrong on our side. Safe to retry with exponential backoff. Include the `request_id` if you contact support.

Rate limit

Per-key, per-minute window. The cap depends on your plan.

PlanPer-minute limitSustained equivalent
Hobby60 / min~1 req/sec
Pro2,000 / min~33 req/sec
Scale10,000 / min~166 req/sec
Enterprisecustom

The window is a rolling-minute bucket: brief bursts above the sustained rate are absorbed as long as you stay under your per-minute cap. When you cross the limit you get 429 rate_limited with a Retry-After header pointing at the next minute boundary (always under 60s).

Every successful response (and every 429) includes rate-limit headers. Use them to back off proactively rather than waiting to be limited.

HeaderDescription
X-RateLimit-LimitPer-minute request limit for the API key (60/2,000/10,000 for Hobby/Pro/Scale, custom for Enterprise).
X-RateLimit-RemainingRequests remaining in the current minute window.
X-RateLimit-ResetSeconds remaining until the per-minute window resets.
Retry-AfterSeconds to wait before retrying. Present only on 429 responses.

No daily cap. The per-minute window is what catches runaway clients; daily quotas mostly punish honest traffic without solving abuse. See authentication for the full plan comparison.

Backoff guidance

What to do when you get a 429.

  • Read the Retry-After header on the 429 response. Wait at least that many seconds before retrying.
  • Use exponential backoff for transient 5xx errors, starting at 1 second and doubling. Cap at 60 seconds.
  • Use the X-RateLimit-Remaining header to throttle yourself proactively rather than racing into the cap.
  • Treat internal_error (500) as retryable. Treatinvalid_request (400), invalid_query(400), and unauthorized (401) as fatal; retrying won't help.