Errors

Every error MentionsAPI returns uses the same JSON shape and a stable error.code string. Match on the code — never on the message — to keep your integration resilient.

Error response shape

Every 4xx and 5xx response returns this shape. The request_id is the single best thing to include in a support ticket — we can look up every log line for that call.

bash
curl -i https://api.mentionsapi.com/v1/ask \
  -H "Authorization: Bearer lvk_live_deadbeef" \
  -H "Content-Type: application/json" \
  -d '{}'
http
HTTP/1.1 401 Unauthorized
Content-Type: application/json

{
  "error": {
    "code": "unauthorized",
    "message": "API key not found."
  },
  "request_id": "req_01H8X2Y3Z4PQR5STU6VW7XYZ8"
}

Validation errors (from malformed JSON bodies) additionally include an error.details object with the flattened Zod issue list — field paths and messages you can surface to your users.

HTTP status code reference

The high-level shape of every response. Status code tells you what happened; the error.code string in the body tells you why.

StatusMeaningTypical causeRetry?
400Invalid requestMalformed body, failed Zod validation, or missing required fields.No — fix the request
401UnauthorizedMissing, malformed, or revoked API key.No — rotate the key
402Payment requiredYour credit balance is insufficient for this call.After top-up
403ForbiddenAuthenticated but not permitted (e.g. cross-account resource access).No
404Not foundThe requested resource (ask id, monitor, webhook, key) does not exist for your account.No
409ConflictState conflict — e.g. trying to mutate a resource in an incompatible state.No
410GoneThe archived response is older than the 30-day retention window.No
422Unprocessable entityBody is syntactically valid but semantically wrong.No — fix the request
429Rate limitedPer-API-key token bucket exhausted.Yes — honor Retry-After
500Internal errorUnhandled exception or database failure on our side.Yes — with backoff
503Service unavailableTemporarily unavailable — usually a downstream provider outage.Yes — with backoff

Error code reference

Every error.code string the API emits, grouped by top-level code (returned in error.code) and provider-level code (returned inside response.providers[].error.code on partial-success /v1/ask responses).

Top-level codes

CodeHTTPWhenRecovery
invalid_request400Request failed Zod validation, JSON body is malformed, required fields are missing (e.g. prompt OR messages), or a referenced resource id is invalid. Also fires for: async: true without webhook_id, a disabled webhook endpoint, a webhook not subscribed to ask.completed, an empty PATCH body, and start >= end on /v1/usage.Inspect error.details (flattened Zod issues) and fix the request. Do not retry.
unauthorized401Missing or malformed Authorization header, token does not match lvk_(live|test)_… shape, the key is unknown, revoked, or has been detached from an account.Rotate or replace the key in the dashboard. Never retry — the token will not work on the next call either.
insufficient_credits402Legacy /v1/ask code — credit balance too low. Newer endpoints (/v1/check, /v1/discover, /v1/compare) use insufficient_balance with the same shape.Top up at /app/billing, then retry.
insufficient_balance402Wallet balance is below this call's price. /v1/check, /v1/discover, /v1/compare deduct upfront — the body includes balance_cents, required_cents, and topup_url (deep-link to the dashboard).Top up at topup_url, then retry.
mode_roadmap501You called mode:deep or mode:change_track. Both are on the Q3 2026 roadmap and return 501 today. Body includes mode, eta, and available_modes so your code can branch deterministically. No charge.Use mode:quick or mode:perplexity_live instead. Schedule via /v1/watch for the change-track use case.
mode_not_yet_available501Worker is misconfigured (missing MENTIONSAPI_WORKER_SECRET or SUPABASE_URL). Should never fire in production; if it does, file a support ticket with the request_id.Don't retry — wait for the misconfig to be resolved.
idempotency_body_mismatch422You reused an Idempotency-Key with a different request body. Stripe-style behavior — the original key locks the body shape until it expires (24h).Pick a fresh key, or wait 24h, or call without the header.
forbidden403Authenticated, but attempted an operation your account is not permitted to perform.Do not retry — contact support if the denial seems wrong.
not_found404The addressed resource does not exist within your account — GET /v1/ask/:id with an unknown id, a monitor id that belongs to another account, a webhook_id that has been deleted, a revoked key id, or an account record that cannot be found during credit check.Fix the id. Do not retry.
conflict409State conflict.Reconcile state on your side. Do not retry identically.
expired410Returned by GET /v1/ask/:id when the archived response is past the 30-day retention window. Response body includes retention_days: 30.Archive the payload on your side on receipt — the row is gone.
unprocessable_entity422Request was syntactically valid but semantically rejected.Do not retry. Inspect the message.
rate_limited429Your API key exceeded its per-key token-bucket limit. Response includes a Retry-After header (in seconds).Sleep for at least Retry-After seconds, then retry. Use exponential backoff on repeat 429s.
internal_error500Unhandled exception, database read failure on archive or queue paths, or a Supabase RPC error (debit, credit, subscription sync). The response body is sanitized — the real stack is in our logs, keyed by request_id.Retry with exponential backoff. If the error persists, include request_id in a support email.
service_unavailable503Downstream dependency outage (provider proxy, database) or maintenance.Retry with exponential backoff, capped at ~30s.
request_failed4xxGeneric client error for any 4xx status without a more specific code mapping.Inspect the message; do not retry.
upstream_unavailable503An upstream live-mode surface returned a non-success status. Surfaces per-platform inside errors[platform] on multi-platform modes (so the call still 200s and you get the platforms that worked).Retry with backoff. The upstream's failure may resolve within seconds.
upstream_rate_limited503An upstream live-mode surface rate-limited us. Per-platform; partial-success refund applied automatically.Retry with backoff. Don't retry the same platform faster than 30s.
upstream_timeout503An upstream live-mode surface didn't respond within our 90-second ceiling.Retry. Live UI capture has a heavy latency tail; intermittent timeouts are expected.

Provider-level codes (inside /v1/ask responses)

POST /v1/ask fans out to multiple LLM providers. If one provider fails, the overall HTTP status is still 200 — the failure is reported under providers[] with one of these codes. You only pay for successful provider calls.

CodeWhenRecovery
provider_errorCatch-all for an upstream provider failure we could not classify more specifically.Retry idempotently; the rest of the fan-out still succeeded.
provider_timeoutThe provider request was aborted for exceeding the deadline.Retry; consider reducing max_tokens or dropping the slow provider from providers.
provider_rate_limitedThe upstream provider returned 429 to our proxy.Retry with backoff; transient on a per-provider basis.
provider_auth_failedProvider-side auth failure at the proxy (model blocked by the allowlist, or provider credentials rotated).Verify the model override is in the provider's allowlist; email support if the issue is persistent.
provider_unavailableProvider returned 500/502/503 to our proxy.Retry with exponential backoff.
json_schema_not_supportedThe call included json_schema but the targeted provider does not support structured output natively. Other providers in the same fan-out still ran.Drop json_schema or remove the unsupported provider from providers.

Rate-limit headers

The rate-limit headers are emitted on every authenticated response so you can track headroom without waiting for a 429. Retry-After only appears on 429 responses. The unique call identifier is returned in the JSON body as request_id, not as an HTTP header.

http
X-RateLimit-Limit: 30
X-RateLimit-Remaining: 27
Retry-After: 2
HeaderMeaning
X-RateLimit-LimitToken-bucket capacity for your API key (requests the bucket can hold at rest).
X-RateLimit-RemainingTokens remaining after this call. When this hits 0 the next request will 429 unless the bucket has refilled.
Retry-AfterPresent on 429 responses. Seconds to wait before the next attempt. Always honor it — aggressive retries extend your throttle window.

Handling errors in code

Match on error.code, honor Retry-After on 429, and back off exponentially on 500/503. Capture request_id for any error you surface in logs — it is what support will ask for.

mentionsapi.ts
type ApiError = {
  error: { code: string; message: string; details?: unknown };
  request_id?: string;
};

async function callMentionsApi<T>(
  path: string,
  body: unknown,
  attempt = 0,
): Promise<T> {
  const res = await fetch(`https://api.mentionsapi.com${path}`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.MENTIONSAPI_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify(body),
  });

  if (res.ok) return (await res.json()) as T;

  const payload = (await res.json().catch(() => null)) as ApiError | null;
  const code = payload?.error?.code ?? "unknown";
  const requestId = payload?.request_id;

  // 429 — always wait the header value before retrying.
  if (res.status === 429 && attempt < 4) {
    const retryAfter = Number(res.headers.get("Retry-After") ?? 1);
    await sleep(retryAfter * 1000);
    return callMentionsApi<T>(path, body, attempt + 1);
  }

  // 500 / 503 — exponential backoff capped at 30s.
  if ((res.status === 500 || res.status === 503) && attempt < 4) {
    const delay = Math.min(2 ** attempt * 500, 30_000);
    await sleep(delay);
    return callMentionsApi<T>(path, body, attempt + 1);
  }

  // 402 — balance is low. Surface to the user, don't retry.
  if (code === "insufficient_credits") {
    throw new Error("Top up at mentionsapi.com/app/billing");
  }

  // 401 / 400 / 404 etc — fix the request, don't retry.
  throw new Error(
    `MentionsAPI ${res.status} ${code}: ${payload?.error?.message ?? "unknown"} (request_id=${requestId})`,
  );
}

const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
mentionsapi.py
import os
import time
import requests

BASE = "https://api.mentionsapi.com"
KEY = os.environ["MENTIONSAPI_KEY"]

def call_mentions_api(path: str, body: dict, attempt: int = 0):
    res = requests.post(
        f"{BASE}{path}",
        headers={
            "Authorization": f"Bearer {KEY}",
            "Content-Type": "application/json",
        },
        json=body,
        timeout=60,
    )

    if res.ok:
        return res.json()

    try:
        payload = res.json()
    except ValueError:
        payload = {}

    code = payload.get("error", {}).get("code", "unknown")
    request_id = payload.get("request_id")

    # 429 — honor Retry-After
    if res.status_code == 429 and attempt < 4:
        retry_after = int(res.headers.get("Retry-After", "1"))
        time.sleep(retry_after)
        return call_mentions_api(path, body, attempt + 1)

    # 500 / 503 — exponential backoff capped at 30s
    if res.status_code in (500, 503) and attempt < 4:
        time.sleep(min(0.5 * (2 ** attempt), 30))
        return call_mentions_api(path, body, attempt + 1)

    if code == "insufficient_credits":
        raise RuntimeError("Top up at mentionsapi.com/app/billing")

    raise RuntimeError(
        f"MentionsAPI {res.status_code} {code}: "
        f"{payload.get('error', {}).get('message', 'unknown')} "
        f"(request_id={request_id})"
    )

When to contact support

If a 500 persists across retries, or a 4xx response does not match what the docs describe, email [email protected] with the request_id from the response body — we can trace every log line for that call.