Skip to main content
Every error response has the same shape:
{
  "error":   "stable_string_code",
  "message": "Human-readable hint",
  "reason":  "Optional extra context (rate-limit endpoints)"
}
Branch on error, not on message. The error string is stable across versions; message may be tightened or expanded.

4xx — caller errors

StatuserrorCauseRecover
400body.domain requiredPOST body had no domain field.Add it.
400sandbox: only [...] supportedLive domain with a test key.Use a ki_test_ key for sandbox, ki_live_ key for real.
400test domains require a ki_test_ keyTest domain with a live key.Same — match domain to key prefix.
400invalid_cursorHistory pagination cursor malformed.Drop the cursor (start over) or use the next_cursor we returned.
400window must be one of ['7d','30d','90d']Bad ?window= value on /v1/movers.Use a supported value.
401unauthorizedMissing or invalid X-API-Key.Check the header is set and the key is active. Allow up to 5 min for revocations to propagate.
403free_tier_sandbox_onlyLive key on Free tier attempted a real-domain score. The Free trial is sandbox-only.Use a ki_test_ key against the 4 canned test domains, or upgrade to Starter.
403forbiddenJob belongs to a different account.Job IDs are bound to the user that started them — any of that user’s keys can poll, but keys from another account cannot.
404no score available for this domainGET /v1/score/{domain} against a never-scored domain.Trigger one with POST /v1/score.
404no_historySame shape, history endpoint.Trigger an initial score.
404job not foundUnknown or expired (>30d) job ID.Re-run POST /v1/score to start a fresh cold pipeline.
429cold_budget_exhausted (reason: monthly_cap)Account exhausted its tier’s monthly cold-call budget. v1.0 is flat-only — no overage.Upgrade to the next tier, or wait for the next monthly reset (Retry-After header gives seconds remaining). v1.5 will add opt-in metered overage.
429rate limited (reason: per_key_cold_1h)This key hit 200 cold calls in 1 hour.Wait — Retry-After header gives seconds until reset.
429rate limited (reason: account_cold_24h)Account hit 2× tier monthly cold cap inside 24h.Wait, or email support if legitimate spike.

5xx — server / pipeline errors

StatuserrorCauseRecover
500internal errorUnhandled exception.Retry once. If persistent, email support with requestId from headers.
500sandbox_invariant_violatedA sandbox key reached a live-path codepath. Should never fire — alerts us when it does.Retry; email support so we can investigate the regression.
500scoring engine unavailableThe scoring pipeline failed to start.Retry once; email support if persistent.

How cold scoring fails after kickoff

A successful POST /v1/score cold path returns 202 with a job_id immediately, then queues the run. If the engine later fails to produce a score (insufficient data, fetcher credit exhaustion, etc.), the corresponding GET /v1/jobs/{job_id} poll returns:
{ "status": "failed", "failure_reason": "scoring_completed_no_record" }
Possible failure_reason values: sfn_failed, sfn_timed_out, sfn_aborted, scoring_completed_no_record. Retry with a different domain, or wait a few hours and re-run — fetcher credit exhaustion clears on the daily rollover. See GET /v1/jobs/{job_id} for the full response shape.

What we never do

  • Surface fetcher errors directly. A data fetcher timeout becomes a data_warnings entry in GET /v1/company/{domain}/confidence, not a 5xx. The score is still produced.
  • Return partial data on 200. A 200 response always has the full schema. Missing data appears as neutral defaults (signal score ≈ 50) flagged in confidence.
  • Auto-retry your call on a 4xx. If you sent bad input, retrying doesn’t help.