Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.exorde.io/llms.txt

Use this file to discover all available pages before exploring further.

The envelope

Every non-2xx response has the same shape:
{
  "error": "<stable_code>",
  "message": "<human readable, English, may change>",
  "trace_id": "<16-hex>",
  "...": "code-specific extras"
}
Three rules:
  1. error is a stable enum. Match on it in code. We never change a code without a major version bump.
  2. message is for humans. Show it in UIs. Wording may evolve; do not parse.
  3. trace_id is the support handshake. Quote it in any ticket — we resolve in one round trip.
Every response (success and error) also returns the trace id in the X-Exorde-Trace-Id header.

Authentication errors

StatuserrorWhenWhat to do
401missing_api_keyX-API-Key header absentAdd the header
401invalid_api_keyUnknown / rotated / revoked keyMint via /v1/keys/trial or rotate
401malformed_api_keyHeader present but doesn’t match exd_<tier>_<token>Check for stray whitespace, quotes
403key_expiredPast expires_atUpgrade or mint a new trial with a different email
403key_revokedKey was explicitly deleted via DELETE /v1/keys/currentMint a new key
Sample:
{
  "error": "invalid_api_key",
  "message": "API key not recognised",
  "trace_id": "8b3a47ce91d04f17"
}

Authorisation errors

StatuserrorWhenWhat to do
403topic_deniedKey valid but not scoped to the requested topicUpgrade to add the topic
403upgrade_requiredEndpoint requires a higher tierUpgrade
403feature_disabledFeature gated off for your account (rare; bespoke contracts)Contact support
upgrade_required carries upgrade context so a UI can render a CTA without a second roundtrip:
{
  "error": "upgrade_required",
  "message": "This endpoint requires the 'see' tier",
  "feature": "clusters",
  "current_tier": "watch",
  "required_tier": "see",
  "upgrade": true,
  "trace_id": "8b3a47ce91d04f17"
}
topic_denied echoes which topics you do have:
{
  "error": "topic_denied",
  "message": "Key not authorised for topic 'cyber'",
  "requested_topic": "cyber",
  "allowed_topics": ["global"],
  "trace_id": "8b3a47ce91d04f17"
}

Resource errors

StatuserrorWhenWhat to do
404unknown_topicTopic slug not in curated setUse /v1/topics for the list
404unknown_watchlistWatchlist id not found or not owned by this clientUse GET /v1/watchlists
404unknown_clustercluster_id not in the requested topic snapshotRe-query /clusters
404unknown_entityEntity not in the requested topic snapshotRe-query /entities
404unknown_narrativeNarrative id not in topic snapshotRe-query /narratives
404report_not_foundNo report yet for this topicTry /reports/archive for older reports
410snapshot_expiredRequested snapshot rolled out of retentionUse latest or a recent snapshot_id

Validation errors

StatuserrorWhen
422validation_errorPydantic validation failed (missing field, wrong type, etc.)
422invalid_emailTrial mint with a non-RFC-compliant email
422invalid_term_typeWatchlist term.type not in keyword/phrase/entity/domain
422empty_termsWatchlist payload with terms: []
422watchlist_term_limit_reachedMore terms than tier allows
422invalid_base_topicWatchlist base_topic not curated
422invalid_signal_typeSubscription with unknown signal_type
422invalid_webhook_urlSubscription delivery.url malformed or non-HTTPS
409duplicate_watchlist_nameWatchlist name already used by this client
Pydantic-style detail when validation_error:
{
  "error": "validation_error",
  "message": "Request body failed validation",
  "fields": [
    { "loc": ["body", "terms", 0, "type"], "msg": "value is not a valid enum member" },
    { "loc": ["body", "name"],              "msg": "field required" }
  ],
  "trace_id": "8b3a47ce91d04f17"
}

Quota and rate-limit errors

StatuserrorWhenWhat to do
429rate_limitedRPM exceededWait Retry-After, retry
429monthly_quota_exceededCalls-per-month capUpgrade or wait for rollover
429subscription_limit_reachedWebhook count capDelete an existing subscription or upgrade
429watchlist_limit_reachedWatchlist count capDelete an existing watchlist or upgrade
429trial_mint_throttledToo many trial mints from this IPWait, then retry
rate_limited:
{
  "error": "rate_limited",
  "message": "Rate limit exceeded — retry after 7s",
  "retry_after_seconds": 7,
  "limit_rpm": 30,
  "trace_id": "8b3a47ce91d04f17"
}
watchlist_limit_reached:
{
  "error": "watchlist_limit_reached",
  "message": "Your tier allows up to 4 watchlists",
  "current_tier": "see",
  "limit": 4,
  "current": 4,
  "trace_id": "8b3a47ce91d04f17"
}
Full handling pattern: Rate limits.

Server-side errors

StatuserrorWhenWhat to do
500internal_errorUncaught exceptionRetry once; if it persists, email support with trace_id
502upstream_errorClickHouse / model service errorRetry with backoff
503service_unavailableStatus endpoint reports degraded; or service draining for deployRetry; check /v1/status
504upstream_timeoutBackend query exceeded the per-request budgetRetry; if persistent on a specific endpoint, email support
service_unavailable:
{
  "error": "service_unavailable",
  "message": "Backing store is degraded — try again shortly",
  "retry_after_seconds": 5,
  "trace_id": "8b3a47ce91d04f17"
}

Subscription / webhook errors

StatuserrorWhen
422invalid_subscription_scopescope mismatched (e.g. kind: watchlist without an id)
422unknown_subscription_typetype not in alert/digest/report
409duplicate_subscriptionSame scope+type+url already exists
410webhook_deadEndpoint returned 4xx/5xx N times in a row, auto-paused
Webhook delivery (server-to-you, not request errors):
HeaderMeaning
X-Exorde-Signaturesha256=<hex> HMAC of the body, signed with your subscription’s secret
X-Exorde-Delivery-IdUnique id for this delivery attempt; use to dedup
X-Exorde-Subscription-IdThe subscription that produced this event
X-Exorde-Event-Typealert, digest, report
Verify the signature server-side before trusting the payload.

Common patterns

Match on error, not status

A 403 can be one of topic_denied, key_expired, key_revoked, upgrade_required, feature_disabled — all need different UX. Don’t hardcode “403 = upgrade prompt”; read error.
def render_error(envelope: dict) -> str:
    code = envelope["error"]
    if code == "upgrade_required":  return f"Upgrade to {envelope['required_tier']} for {envelope['feature']}"
    if code == "topic_denied":      return f"This topic isn't in your plan. You have: {envelope['allowed_topics']}"
    if code == "key_expired":       return "Your trial expired. Renew at [email protected]"
    if code == "rate_limited":      return f"Slow down — try again in {envelope['retry_after_seconds']}s"
    return envelope["message"]  # fallback

Always log trace_id

try:
    r = httpx.get(url, headers=h)
    r.raise_for_status()
except httpx.HTTPStatusError as e:
    body = e.response.json()
    log.error("intel api error",
              extra={"status": e.response.status_code,
                     "code":   body.get("error"),
                     "trace":  body.get("trace_id")})
    raise

Distinguish “retry” from “act”

Retryable with no actionNeed to act
rate_limited, service_unavailable, upstream_error, upstream_timeout, internal_error (once)everything else
Encode this as a function:
RETRYABLE = {"rate_limited", "service_unavailable", "upstream_error",
             "upstream_timeout", "internal_error"}

def is_retryable(envelope: dict) -> bool:
    return envelope.get("error") in RETRYABLE

Support handshake

When you email [email protected], include:
  1. The full error envelope (especially trace_id)
  2. The exact request URL and method
  3. Your client_id (from /v1/keys/current, not the api_key itself)
  4. Approximate UTC timestamp
We resolve from trace_id alone in most cases. The other fields are belt-and-braces.
Last reviewed: 2026-05-19. API version 1.2.8.