Skip to main content
Every non-2xx response shares the same typed envelope. Match on error in code, show message to humans, rely on extras only when present.

The envelope

{
  "error": "upgrade_required",
  "message": "This endpoint requires the 'see' tier",
  "feature": "clusters",
  "current_tier": "watch",
  "required_tier": "see",
  "upgrade": true
}
FieldAlways presentNotes
errorYesMachine-readable code. Never localized. Safe to branch on.
messageYesHuman-readable English. Do not parse.
ExtrasNoContext-specific: required_tier, feature, retry_after, limit, window_seconds, etc. Stable per code.
Every endpoint returns this envelope on every failure. One handler, every case.

401 — Authentication

You did not provide a credential the server can verify.
CodeGuidance
missing_api_keyProvide your API key in the X-API-Key header.
invalid_api_keyThe key is unknown, revoked, or rotated. Mint a new one or use your active key.
authentication_requiredThis endpoint requires authentication even when the underlying tier is public.
Example:
{
  "error": "invalid_api_key",
  "message": "API key is invalid, revoked, or rotated"
}

403 — Authorization

Your credential is valid, but you cannot access this resource at this tier or scope.
CodeGuidance
upgrade_requiredYour tier does not grant this feature. See required_tier and upgrade.
topic_deniedYour key is not scoped to this topic. Request topic access or upgrade to Know.
topic_not_subscribedYour account is valid but not subscribed to this topic.
key_expiredYour key is past expires_at. Renew or mint a new trial.
Example of the upgrade wall — this is the shape to drive upsell UI:
{
  "error": "upgrade_required",
  "message": "This endpoint requires the 'see' tier",
  "feature": "clusters",
  "current_tier": "watch",
  "required_tier": "see",
  "upgrade": true
}

404 — Not found

The requested resource does not exist.
CodeGuidance
unknown_topicThe topic slug is not in the curated catalog. See /v1/topics.
api_key_not_foundThe key has no record in the backing store (revoked externally).
watchlist_not_foundWatchlist id does not exist for your client_id.
cluster_not_foundCluster id does not exist within the current window.
entity_not_foundEntity has no recent activity within the requested window.
narrative_snapshot_not_foundNarrative snapshot id is unknown or outside retention.
report_not_foundNo report exists for the requested topic and period.
subscription_not_foundSubscription id does not exist for your client_id.

409 — Conflict

Your request conflicts with current state. The resource already exists, or the operation is not valid in the current state.
CodeGuidance
watchlist_name_conflictA watchlist with this name already exists for your client_id.
key_not_rotatableInactive or expired keys cannot be rotated. Mint a new key instead.
subscription_name_conflictA subscription with this target and webhook already exists.

422 — Validation

Your request body or query parameters failed schema validation. The response includes a precise reason.
CodeGuidance
invalid_request_bodyRequest body failed schema validation. Inspect the response for the failing field.
invalid_query_parameterA query parameter failed validation. Inspect the response body.
watchlist_invalid_term_typeTerm type must be one of keyword, phrase, entity, domain.
watchlist_invalid_term_valueTerm value is empty, too long, or malformed for its type.
watchlist_invalid_base_topicbase_topic must be one of the curated topics in /v1/topics.
watchlist_term_limit_reachedToo many terms on this watchlist. See per-tier term limits in Tiers and Quotas.
invalid_emailEmail address is malformed.
invalid_webhook_urlWebhook URL must be HTTPS and reachable.
invalid_sensitivity_levelMust be one of low, medium, high.
invalid_subscription_typeMust be one of topic, watchlist.
Typical payload:
{
  "error": "watchlist_invalid_term_type",
  "message": "Term type must be one of: keyword, phrase, entity, domain",
  "field": "terms.type",
  "got": "url"
}

429 — Rate limiting and quotas

You are over a limit or quota. Quota codes are distinct from rate-limit codes — they carry different remediation.

Rate limits

CodeGuidance
rate_limitedYou exceeded the per-minute rate limit. Back off using Retry-After.
trial_issuance_rate_limitedToo many trial keys requested from this IP. Wait and retry.
Rate-limited responses also carry:
HeaderMeaning
Retry-AfterSeconds to wait before retrying.
X-RateLimit-LimitYour tier’s per-minute ceiling.
X-RateLimit-RemainingRequests left in the current 60-second window.
X-RateLimit-ResetUNIX seconds until the window resets.
See Rate limits for the full backoff pattern.

Quota caps

CodeGuidance
subscription_limit_reachedYou hit your tier’s subscription cap. Delete one or upgrade.
watchlist_limit_reachedYou hit your tier’s watchlist cap. Delete one or upgrade.
Example:
{
  "error": "rate_limited",
  "message": "Rate limit exceeded. Retry after 1 second.",
  "retry_after": 1,
  "limit": 30,
  "window_seconds": 60
}

503 — Backend unavailable

A dependency is temporarily down. Retry with exponential backoff and jitter; never tight-loop.
CodeGuidance
auth_backend_unavailableKey storage backend is temporarily unavailable. Retry with backoff.
subscriptions_backend_unavailableSubscription backend is temporarily unavailable. Retry with backoff.
watchlists_backend_unavailableWatchlist backend is temporarily unavailable. Retry with backoff.
data_backend_unavailableAnalytics backend is temporarily unavailable. Retry with backoff.
If you see any 503 persist beyond 30 seconds, check status.exorde.io for the affected stream.

Error handling, end to end

A single handler covers every case. Match on error first, then status. Examples in three languages.
Python
import httpx, time, random

class ExordeError(Exception):
    def __init__(self, status, code, message, payload):
        super().__init__(f"{status} {code}: {message}")
        self.status = status
        self.code = code
        self.payload = payload

def call(method, url, *, headers, **kw):
    for attempt in range(5):
        r = httpx.request(method, url, headers=headers, timeout=10, **kw)
        if r.status_code == 429 or r.status_code == 503:
            retry = int(r.headers.get("Retry-After", "1"))
            time.sleep(retry + random.uniform(0, 0.5 * (2 ** attempt)))
            continue
        if r.status_code >= 400:
            p = r.json()
            raise ExordeError(r.status_code, p.get("error"), p.get("message"), p)
        return r.json()
    raise RuntimeError("exhausted retries")
Node
class ExordeError extends Error {
  constructor(status, code, message, payload) {
    super(`${status} ${code}: ${message}`);
    this.status = status;
    this.code = code;
    this.payload = payload;
  }
}

async function call(method, url, { headers, body } = {}) {
  for (let attempt = 0; attempt < 5; attempt++) {
    const r = await fetch(url, { method, headers, body });
    if (r.status === 429 || r.status === 503) {
      const retry = Number(r.headers.get("Retry-After") ?? 1);
      const jitter = Math.random() * 0.5 * Math.pow(2, attempt);
      await new Promise(res => setTimeout(res, (retry + jitter) * 1000));
      continue;
    }
    if (!r.ok) {
      const p = await r.json();
      throw new ExordeError(r.status, p.error, p.message, p);
    }
    return r.json();
  }
  throw new Error("exhausted retries");
}
PowerShell
function Invoke-Exorde($Method, $Uri, $Body = $null) {
  for ($i = 0; $i -lt 5; $i++) {
    try {
      return Invoke-RestMethod `
        -Method $Method `
        -Uri $Uri `
        -Headers @{ "X-API-Key" = $env:EXORDE_API_KEY } `
        -ContentType "application/json" `
        -Body $Body
    } catch [System.Net.WebException] {
      $resp = $_.Exception.Response
      $status = [int]$resp.StatusCode
      if ($status -eq 429 -or $status -eq 503) {
        $retry = [int]$resp.Headers["Retry-After"]
        Start-Sleep -Seconds ($retry + (Get-Random -Maximum 1))
        continue
      }
      throw
    }
  }
  throw "exhausted retries"
}

Branching rules — what to do per code class

  • 401 → mint/rotate a new key and retry once. Never loop on 401.
  • 403 upgrade_required → don’t retry. Surface the upsell using required_tier + feature.
  • 403 topic_denied → don’t retry. Change topic or request access.
  • 403 key_expired → don’t retry. Mint a new key.
  • 404 → don’t retry. The resource does not exist in the current window.
  • 409 → don’t retry. Read the state you conflicted with, then decide.
  • 422 → don’t retry. Fix the request body or parameters.
  • 429 rate_limited → retry with Retry-After + jitter.
  • 429 *_limit_reached → don’t retry. Delete one or upgrade.
  • 503 → retry with exponential backoff + jitter, capped at 30 s.
This page is maintained by hand and validated against the live API’s error surface and QA suite. Found a code in the wild that isn’t listed? Email [email protected] — it is a bug.