For developers

CreatorScore API

Submit any social handle. Get back the same 1–100 trust score that brands use to vet creators on creatorscore.io — content risk, brand safety, sentiment, authenticity, audience quality, community trust, ROI prediction. Every creator is a flat 1 credit — one platform or six, cached or scored fresh. Footprint discovery is priced separately — it runs a live profile fetch plus a web-grounded account search. Resolving a creator against our existing index stays free.

Quick start

  1. Create a developer account — takes 30 seconds, you'll get an API key immediately.
  2. Buy a credit pack from your dashboard. Starter is $399 for 50 creator scores.
  3. Hit GET /api/v1/creators/{platform}/{handle} with your key. Every creator is a flat 1 credit — cached or scored fresh, one platform or six.

Build with AI

The fastest way to integrate this API is to paste the system prompt below into Claude, ChatGPT, Cursor, or any AI assistant — it has the full API surface area, request/response shapes, and error codes baked in. You'll get working code in under a minute.

System prompt — copy this verbatim into your AI assistant

You are helping a developer integrate the CreatorScore API. The API enriches influencer rosters with a 1-100 trust score across 7 AI agents.

# Base URL
https://creatorscore.io

# Authentication
Every request requires an HTTP header: X-API-Key: <key>

# Pricing
1 credit = 1 creator. Scoring or vetting a creator costs a flat 1 credit, no matter how many platforms or accounts they have — one platform or a six-platform footprint, TikTok or YouTube, all the same 1 credit. This is true for both a fresh score (scraped + scored on demand) and a cache hit (a score ≤30 days old, returned immediately). Footprint discovery (POST /creators/discover — finding a creator's other accounts from one seed handle) costs 1 credit: it runs a live profile fetch plus a web-grounded account search. Resolving a creator against our existing index (POST /creators/resolve) is free. Every response includes credits_charged. If the balance can't cover a lookup, the API returns 402 insufficient_credits and does NOT return the score.
Customers buy credit packs: Starter $399 = 50 creators ($7.98 each), Pro $1,499 = 250 creators ($6.00 each), Scale $4,990 = 1,000 creators ($4.99 each).

# Endpoints

## GET /api/v1/account
Returns the authenticated tenant info + credit balance + pricing table.
Response 200: { tenant: {id, name}, api_key: {id, label, created_at, last_used_at}, credits: {balance: number}, pricing: {credits_per_creator: 1, discovery_credits: 1, cache_freshness_days: 30, note: "1 credit per creator, any platform; discovery priced separately"} }

## GET /api/v1/creators/{platform}/{handle}
Cache-or-queue lookup of one creator.
Path params: platform (tiktok | instagram | youtube | twitter | x | facebook | reddit | twitch | kick | linkedin | threads | snapchat), handle (URL-encoded social handle, no @).

Response 200 (cache hit):
{
  "status": "cached",
  "creator": {
    "platform": "tiktok", "handle": "alixearle",
    "display_name": "Alix Earle", "followers": 8400000,
    "score": 84, "tier": "Excellent",
    "agents": {
      "content_risk": 81.0, "brand_safety": 82.1, "sentiment": 85.0,
      "authenticity": 98.8, "audience_quality": 84.4,
      "community_trust": 83.9, "roi_prediction": 58.6
    },
    "knockouts": [],
    "last_scored_at": "2026-04-15T05:30:39Z",
    "public_profile_url": "https://creatorscore.io/profile/..."
  },
  "credits_charged": 1
}

Response 202 (queued, fresh score needed):
{ "status": "queued", "job_id": "uuid", "eta_seconds": 1800, "poll_url": "/api/v1/jobs/{job_id}", "deduplicated": false, "credits_charged": 1 }

## POST /api/v1/creators/bulk
Submit up to 100 (platform, handle) pairs at once.
Body: { "creators": [{ "platform": "...", "handle": "..." }, ...] }
Response 200: { "results": [...], "summary": { "total", "cached", "queued", "errored", "credits_spent", "credits_remaining" } }
Each result has the same shape as the single endpoint (cached/queued/error).

## GET /api/v1/jobs/{job_id}
Poll one job.
Response 200 (done): { "job_id", "status": "done", "progress": 100, "completed_at", "creator": {...same as cached payload...} }
Response 202 (in flight): { "job_id", "status": "queued"|"processing", "progress", "progress_message", "eta_seconds", "created_at" }
Response 200 (failed): { "job_id", "status": "failed", "progress", "error", "failed_at" }
Response 404: { "job_id", "status": "not_found", "message" }

## POST /api/v1/jobs/bulk-status
Poll up to 100 jobs at once.
Body: { "job_ids": ["uuid1", "uuid2", ...] }
Response 200: { "jobs": [...], "summary": { "total", "done", "in_flight", "failed", "not_found" } }

# Polling guidance
Fresh scores take 10–90 minutes (mostly Whisper transcription on YouTube). Poll every 60 seconds for the first 30 minutes, then every 5 minutes after. Use the eta_seconds field on every queued/processing response as the canonical guide.

# Errors
All errors return { "error": "code", "message": "..." }.
Codes: missing_api_key (401), invalid_api_key (401), insufficient_scope (403), insufficient_credits (402), unsupported_platform (400), invalid_handle (400), invalid_request (400), rate_limit_exceeded (429), daily_scrape_limit_exceeded (429), daily_discovery_limit_exceeded (429), creator_busy (409), queue_failed (500), auth_unavailable (503).
daily_scrape_limit_exceeded (429) is a per-key safety cap on FRESH scrapes per UTC day; cached lookups are unaffected. Treat like a soft 429 — slow down or ask support to raise the cap.
daily_discovery_limit_exceeded (429) is a per-key daily cap on discovery operations (/creators/discover and /creators/score-footprint calls, previews included) — each performs live open-web work. Resets 00:00 UTC.
creator_busy (409) means another job currently holds this creator (e.g. a scan in another workspace). Nothing was charged — retry in ~2 minutes.
insufficient_scope (403) means a read-only key tried to queue a fresh score (cache miss). Only full-scope keys can queue. Cached lookups (charged the flat cache fee) + job-status reads work on read-only keys.

# Constraints
- Bulk endpoints accept max 100 items.
- Duplicate handles in the same batch are charged once (deduped).
- Jobs are scoped to the calling tenant — cross-tenant polling returns not_found.
- Rate limit: 120 requests/minute per API key. Exceeding it returns 429 rate_limit_exceeded with a Retry-After header (seconds). Back off and retry.

When generating code: always send the API key from an environment variable, never hardcoded. Surface insufficient_credits errors clearly so customers know to top up.

Sample user prompts

  • "Write a Python script that enriches my CSV of TikTok handles with their CreatorScore. The CSV has one column called ‘handle’."
  • "Build a TypeScript function I can use in my Next.js app to display a creator's score and 7-agent breakdown."
  • "Show me how to bulk-score 50 creators, then poll their job IDs until they all complete, with backoff between polls."
  • "Generate a React component that takes an array of (platform, handle) pairs and renders each creator's tier with a colored badge."

Use it inside an AI assistant (MCP)

Prefer to vet creators without writing code? The CreatorScore MCP server wraps these same endpoints as tools for Claude, ChatGPT, Cursor, and any Model Context Protocol client. Add it with your API key and ask in plain language — get_creator_score, vet_roster (cache-only, so an agent can't fan out into surprise scrapes), score_creator_fresh, and a free calculate_fair_rate. See the MCP setup guide.

Authentication

Every request requires an X-API-Key header. Keys are tenant-scoped — keep them secret and rotate if compromised. Create or revoke keys at /account/api.

curl https://creatorscore.io/api/v1/account \
  -H "X-API-Key: sk_live_..."

Pricing

  • 1 credit = 1 creator. A creator's whole cross-platform footprint, scored or vetted, is a flat 1 credit — one platform or six, cached or scored fresh, any platform
  • Footprint discovery (find a creator's other accounts from one seed handle): 1 credit — a live profile fetch plus a web-grounded account search. Index resolution (POST /creators/resolve) is free
  • Platforms & resellers: a credit licenses one end-customer's evaluation of one creator. If you serve multiple brands, each brand's view of a creator is a separate read (and credit) — cache per brand for up to 30 days, never across brands. Send X-Customer-Ref: <your brand id> on each read — it's logged with the request so per-brand usage is reconcilable on both sides. Scores are point-in-time: always display last_scored_at alongside any score you surface
  • Pricing may adjust over time. Published pricing is authoritative and firm at the moment of purchase — credits you've bought are yours at the price you paid. Because every analysis runs on frontier AI models whose costs move, published pricing itself may adjust in either direction, with notice before it affects your next purchase or billing cycle
  • No monthly fees, no commitments — prepaid credits only
  • Refunds are automatic if the scoring pipeline fails

Credit packs

Starter
$399
50 creators
$7.98 / creator
Pro — 40% off
$1,499
250 creators
$6.00 / creator
Scale — 50% off
$4,990
1,000 creators
$4.99 / creator

List price is $9.99/creator (matching the dashboard one-off); packs apply a volume discount. The same flat credit covers a fresh scrape-and-score or a cache hit — you pay for the vetted creator, not for how we got there.

Endpoints

All endpoints are versioned under /api/v1.

GET/api/v1/account

Check your API key + credit balance

Use this to confirm a key works and view the current balance.

Request

curl https://creatorscore.io/api/v1/account \
  -H "X-API-Key: sk_live_..."

Response

{
  "tenant": { "id": "...", "name": "ACME Influence Co" },
  "api_key": {
    "id": "...",
    "label": "Score API key",
    "created_at": "2026-05-14T...",
    "last_used_at": "2026-05-15T..."
  },
  "credits": { "balance": 142 },
  "pricing": {
    "credits_per_creator": 1,
    "discovery_credits": 1,
    "cache_freshness_days": 30,
    "note": "1 credit per creator (whole cross-platform footprint), any platform. Footprint discovery costs 1 credit — it runs a live profile fetch plus a web-grounded account search. Resolving from our index is free."
  }
}
GET/api/v1/creators/{platform}/{handle}

Look up one creator (cache or queue)

Returns the cached score immediately if one exists and was calculated in the last 30 days — charged a flat 1 credit (credits_charged in the response). Otherwise queues a fresh score for the same flat 1 credit and returns a job_id you can poll. If your balance can't cover the cost, you get 402 insufficient_credits and no score.

Supported platforms: tiktok, instagram, youtube, twitter, x, facebook, reddit, twitch, kick, linkedin, threads, snapchat.

Request

curl https://creatorscore.io/api/v1/creators/tiktok/alixearle \
  -H "X-API-Key: sk_live_..."

Response — cached (200)

{
  "status": "cached",
  "creator": {
    "platform": "tiktok",
    "handle": "alixearle",
    "display_name": "Alix Earle",
    "followers": 8400000,
    "score": 84,
    "tier": "Excellent",
    "agents": {
      "content_risk": 81.0,
      "brand_safety": 82.1,
      "sentiment": 85.0,
      "authenticity": 98.8,
      "audience_quality": 84.4,
      "community_trust": 83.9,
      "roi_prediction": 58.6
    },
    "knockouts": [],
    "last_scored_at": "2026-04-15T05:30:39Z",
    "public_profile_url": "https://creatorscore.io/profile/..."
  },
  "credits_charged": 1
}

Response — queued (202)

{
  "status": "queued",
  "job_id": "fa9b2c1e-...-...",
  "eta_seconds": 1800,
  "poll_url": "/api/v1/jobs/fa9b2c1e-...",
  "deduplicated": false
}

Optional ?include= param

Append a comma-separated list of extra fields to add to the response. No effect on credits or pricing — just larger payloads.

  • narrative — natural-language summary of the score
  • velocity — 30/60/90-day score deltas (positive = score going up)
  • risk_flags — up to 10 unresolved critical/high/medium flags
  • detailed_analysis — raw per-agent dimension breakdown
  • engagement — this account's engagement metrics from our monitoring pipeline: engagement_rate, avg_likes / avg_comments / avg_views, the snapshot date (as_of), and follower_history (daily follower counts, last 90 days). Account-level, not footprint-level. Fields are null / empty for a creator we haven't been monitoring — we never fabricate a number we didn't measure
curl https://creatorscore.io/api/v1/creators/tiktok/alixearle?include=narrative,velocity \
  -H "X-API-Key: sk_live_..."
POST/api/v1/creators/bulk

Submit up to 100 creators at once

Main endpoint for roster enrichment. Cached results return inline (1 credit each); uncached creators get queued (same flat 1 credit each). Atomic credit accounting — if your balance runs out mid-batch, the remaining items (cached or uncached) return status: "error" with error: "insufficient_credits". Duplicate handles in the same batch are deduplicated (charged once).

Request

curl -X POST https://creatorscore.io/api/v1/creators/bulk \
  -H "X-API-Key: sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "creators": [
      { "platform": "tiktok", "handle": "alixearle" },
      { "platform": "instagram", "handle": "alixearle" },
      { "platform": "youtube", "handle": "MrBeast" }
    ]
  }'

Response (200)

{
  "results": [
    { "platform": "tiktok",    "handle": "alixearle", "status": "cached", "creator": { ... }, "credits_charged": 1 },
    { "platform": "instagram", "handle": "alixearle", "status": "cached", "creator": { ... }, "credits_charged": 1 },
    { "platform": "youtube",   "handle": "MrBeast",
      "status": "queued", "job_id": "...", "eta_seconds": 1800, "deduplicated": false, "credits_charged": 1 }
  ],
  "summary": {
    "total": 3,
    "cached": 2,
    "queued": 1,
    "errored": 0,
    "credits_spent": 3,
    "credits_remaining": 139
  }
}
GET/api/v1/jobs/{job_id}

Poll one queued job

Returns queued/processing while the job is in flight, done with the full creator payload when it completes, or failed if scoring couldn't complete. Failed jobs auto-refund the credit. HTTP codes: 200 for done/failed, 202 for in-flight, 404 for not-found.

Request

curl https://creatorscore.io/api/v1/jobs/fa9b2c1e-... \
  -H "X-API-Key: sk_live_..."

Response — done (200)

{
  "job_id": "fa9b2c1e-...",
  "status": "done",
  "progress": 100,
  "completed_at": "2026-05-15T03:42:11Z",
  "creator": { ...same shape as the cached response... }
}

Response — processing (202)

{
  "job_id": "fa9b2c1e-...",
  "status": "processing",
  "progress": 42,
  "progress_message": "Phase 2: enriching posts (TikTok)",
  "eta_seconds": 1200,
  "created_at": "2026-05-15T03:12:11Z"
}
POST/api/v1/jobs/bulk-status

Poll many jobs in one request

Same shape as the single-job endpoint, but accepts an array of up to 100 job_ids. Always returns 200 — per-job statuses are nested.

Request

curl -X POST https://creatorscore.io/api/v1/jobs/bulk-status \
  -H "X-API-Key: sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{ "job_ids": ["fa9b...", "ab12...", "..."] }'

Response (200)

{
  "jobs": [
    { "job_id": "fa9b...", "status": "done", "creator": { ... } },
    { "job_id": "ab12...", "status": "processing", "progress": 45, "eta_seconds": 1200 },
    { "job_id": "cd34...", "status": "not_found" }
  ],
  "summary": { "total": 3, "done": 1, "in_flight": 1, "failed": 0, "not_found": 1 }
}
POST/api/v1/creators/score-footprint

Score a creator's WHOLE footprint from one handle (1 credit)

The primary scoring call. From a single seed handle it discovers the creator's other accounts across platforms, links them as one identity, and scores the whole footprint as ONE creator for a flat 1 credit — the unified cross-platform CreatorScore is included, never charged again to "merge".

Two steps by design. Called without confirm it returns a free PREVIEW of the discovered footprint — every account with its profile URL, confidence, and how it was found — so you can verify it's the right person before a credit is spent. Call again with confirm: true to score. Full-scope key required.

Request

curl -X POST https://creatorscore.io/api/v1/creators/score-footprint \
  -H "X-API-Key: sk_live_..." -H "Content-Type: application/json" \
  -d '{ "platform": "tiktok", "handle": "alixearle", "confirm": true }'

Response — scoring queued (202)

{
  "status": "queued",
  "job_id": "ab12...",
  "identity_group_id": "f9e8...",
  "accounts": [
    { "platform": "tiktok", "handle": "alixearle", "url": "https://...", "confidence": 1, "source": "seed", "scoreable": true },
    { "platform": "instagram", "handle": "alixearle", "url": "https://...", "confidence": 0.95, "source": "link_in_bio", "scoreable": true }
  ],
  "scoreable_count": 2,
  "credits_charged": 1,
  "eta_seconds": 1800,
  "poll_url": "/api/v1/jobs/ab12..."
}

Errors: 404 seed_not_found (bad handle — nothing charged), 422 no_scoreable_accounts, 402 insufficient_credits.

POST/api/v1/creators/discover

Discover a creator's accounts across platforms (1 credit)

Identity discovery without scoring: given ONE account, find the creator's other accounts by reading the seed's bio and link-in-bio, then verifying candidates with a web-grounded search. Each discovered account carries a confidence (0–1) and a source. Costs 1 credit (a live profile fetch + a web-grounded lookup); a bad handle returns 404 seed_not_found and charges nothing. Pair with POST /creators/vet to read scores, or use score-footprint above, which discovers AND scores in one call.

Discovery is not a database query: every call performs live open-web searches, link-in-bio fetches, and profile verifications. It exists to resolve a specific creator you intend to evaluate — not to enumerate or browse creators — and it is bounded by a per-key daily discovery cap (429 daily_discovery_limit_exceeded, previews included) in addition to the rate limit.

Response (200)

{
  "seed": { "platform": "tiktok", "handle": "alixearle", "display_name": "Alix Earle" },
  "footprint": [
    { "platform": "instagram", "handle": "alixearle", "confidence": 0.95, "source": "link_in_bio", "scoreable": true }
  ],
  "discovery_method": "link_in_bio_plus_grounded_search",
  "grounded_search_skipped": false,
  "credits_charged": 1
}

If grounded_search_skipped is true the daily discovery budget was exhausted: you get link-in-bio results only, nothing is charged, and missing platforms mean UNKNOWN — not absent.

POST/api/v1/creators/resolve

Resolve a name or handle against our index (free)

Identity-first resolution: give a free-text name OR a known handle (optionally scoped by platform) and get ranked candidate creators from our index — each with its linked accounts, score recency, and a match_confidence. Nothing is auto-attributed: confirm the right person before scoring. The index lookup is FREE and never scrapes.

When the index has no strong match for a name, opt into the web fallback with allow_web_search: true — the same web-grounded lookup as discovery, billed at the same 1 credit, full scope required. Left off (the default), resolution is index-only and free, and the response says the web was NOT searched — so an empty result is never mistaken for "this creator doesn't exist".

Request

curl -X POST https://creatorscore.io/api/v1/creators/resolve \
  -H "X-API-Key: sk_live_..." -H "Content-Type: application/json" \
  -d '{ "name": "Mel Robbins", "allow_web_search": false }'
POST/api/v1/creators/vet

Vet one creator across all their accounts (cached, never scrapes)

Cross-platform vetting in one call: pass the SAME creator's handles across platforms (up to 12) and get the unified CreatorScore, the per-platform breakdown, and the worst risk flags found on any account. CACHED-ONLY — it never scrapes. Accounts with no recent cached score return not_scored (no charge); the vet is billed 1 credit per distinct creator delivered: one creator's accounts across any number of platforms cost 1 credit total, while unrelated creators in the same call are billed 1 credit each (the same rate as /creators/bulk). A creator already on your roster with a fresh score is free.

Request

curl -X POST https://creatorscore.io/api/v1/creators/vet \
  -H "X-API-Key: sk_live_..." -H "Content-Type: application/json" \
  -d '{ "creators": [
    { "platform": "tiktok", "handle": "alixearle" },
    { "platform": "instagram", "handle": "alixearle" }
  ] }'
GET/api/v1/creators/roster

List your saved creators (free)

Your tenant's roster — the same list as the dashboard, grouped into footprints (one creator = one entry across platforms), each with its unified score and the TITLES of any active risk flags, never just a count. Free, read-only, cached; never scrapes or charges. Paginate with limit (max 200) and offset.

POST/api/v1/background-checks

Start a Social Background Check ($19.99, paid via Stripe)

Starts a QUICK Social Background Check — a one-shot, full-history brand-safety scan that produces a categorized report of findings (hate, NSFW, violence, profanity, political, legal, and more), each with evidence. No score is produced — a Background Check is a distinct product from the CreatorScore, and it is billed by a one-time Stripe payment, NOT API credits: the response returns a payment_url the brand completes in the browser, and the scan starts automatically when payment clears.

The VERIFIED tier ($29.99) adds creator-authorized OAuth data and is deliberately not available via API — it requires the creator's own consent flow, which runs in the dashboard.

Request

curl -X POST https://creatorscore.io/api/v1/background-checks \
  -H "X-API-Key: sk_live_..." -H "Content-Type: application/json" \
  -d '{ "handle": "alixearle", "platforms": ["tiktok", "instagram"] }'

Response (200)

{
  "check_id": "bc12...",
  "status": "awaiting_payment",
  "tier": "quick",
  "amount": 1999,
  "currency": "usd",
  "payment_url": "https://checkout.stripe.com/...",
  "platforms": ["tiktok", "instagram"]
}

Then poll GET /api/v1/background-checks/{id} (read scope suffices) every ~30s until status is completed. The completed report returns every flag with its type, severity, description, the offending post's caption snippet, and its URL — never a bare count.

POST/api/v1/rate-card

Calculate a fair sponsorship rate (free)

Computes a fair sponsorship rate with a confidence band from inputs you supply (platform, median views, niche, plus optional deliverable details). Pure sourced math — no scrape, no scoring, no charge. GET /api/v1/rate-card returns the valid option catalogs (niches, geos, rights tiers).

Code samples

Reference implementations for the typical workflow: bulk-submit a roster, poll for completion, collect scores. Both samples handle insufficient_credits and failed jobs cleanly. Paste either into your AI assistant with the system prompt above to extend.

Python

import os
import time
import requests

API_KEY = os.environ["CREATORSCORE_API_KEY"]
BASE = "https://creatorscore.io/api/v1"
HEADERS = {"X-API-Key": API_KEY, "Content-Type": "application/json"}

def enrich_roster(creators: list[dict]) -> list[dict]:
    """
    creators: [{"platform": "tiktok", "handle": "alixearle"}, ...]
    Returns a list of score dicts (or {"error": "..."} for failures).
    """
    # 1. Bulk-submit
    r = requests.post(f"{BASE}/creators/bulk", json={"creators": creators}, headers=HEADERS)
    r.raise_for_status()
    data = r.json()

    results = list(data["results"])
    pending_jobs = {row["job_id"]: idx for idx, row in enumerate(results) if row["status"] == "queued"}

    if not pending_jobs:
        return [row.get("creator") or {"error": row.get("error")} for row in results]

    print(f"Cached: {data['summary']['cached']}  Queued: {data['summary']['queued']}  Spent: {data['summary']['credits_spent']} credits")

    # 2. Poll until all queued jobs finish (or fail). Backoff: 30s -> 60s -> 5m.
    delay = 30
    while pending_jobs:
        time.sleep(delay)
        r = requests.post(
            f"{BASE}/jobs/bulk-status",
            json={"job_ids": list(pending_jobs.keys())},
            headers=HEADERS,
        )
        r.raise_for_status()
        for job in r.json()["jobs"]:
            if job["status"] == "done":
                idx = pending_jobs.pop(job["job_id"])
                results[idx] = {"status": "done", "creator": job["creator"]}
            elif job["status"] in ("failed", "not_found"):
                idx = pending_jobs.pop(job["job_id"])
                results[idx] = {"status": job["status"], "error": job.get("error", "unknown")}
        delay = min(delay * 2, 300)  # 30 -> 60 -> 120 -> 240 -> 300 cap

    return [row.get("creator") or {"error": row.get("error", "unknown")} for row in results]


if __name__ == "__main__":
    roster = [
        {"platform": "tiktok",    "handle": "alixearle"},
        {"platform": "instagram", "handle": "MrBeast"},
        {"platform": "youtube",   "handle": "MrBeast"},
    ]
    for creator in enrich_roster(roster):
        print(creator)

TypeScript / Node.js

const API_KEY = process.env.CREATORSCORE_API_KEY!
const BASE = "https://creatorscore.io/api/v1"
const HEADERS = { "X-API-Key": API_KEY, "Content-Type": "application/json" }

type Creator = { platform: string; handle: string }

export async function enrichRoster(creators: Creator[]) {
  // 1. Bulk-submit
  const submitRes = await fetch(`${BASE}/creators/bulk`, {
    method: "POST",
    headers: HEADERS,
    body: JSON.stringify({ creators }),
  })
  if (!submitRes.ok) throw new Error(`Bulk submit failed: ${submitRes.status}`)
  const submit = await submitRes.json()

  const results = [...submit.results]
  const pending = new Map<string, number>()
  results.forEach((r: any, i: number) => {
    if (r.status === "queued") pending.set(r.job_id, i)
  })

  if (pending.size === 0) return results

  console.log(`Cached: ${submit.summary.cached}, Queued: ${submit.summary.queued}, Spent: ${submit.summary.credits_spent}cr`)

  // 2. Poll with exponential backoff (30s -> 60 -> 120 -> 240 -> 300 cap)
  let delay = 30_000
  while (pending.size > 0) {
    await new Promise((r) => setTimeout(r, delay))
    const pollRes = await fetch(`${BASE}/jobs/bulk-status`, {
      method: "POST",
      headers: HEADERS,
      body: JSON.stringify({ job_ids: Array.from(pending.keys()) }),
    })
    const poll = await pollRes.json()
    for (const job of poll.jobs as any[]) {
      if (job.status === "done") {
        const idx = pending.get(job.job_id)!
        results[idx] = { status: "done", creator: job.creator }
        pending.delete(job.job_id)
      } else if (job.status === "failed" || job.status === "not_found") {
        const idx = pending.get(job.job_id)!
        results[idx] = { status: job.status, error: job.error ?? "unknown" }
        pending.delete(job.job_id)
      }
    }
    delay = Math.min(delay * 2, 300_000)
  }

  return results
}

// Example usage
enrichRoster([
  { platform: "tiktok",    handle: "alixearle" },
  { platform: "instagram", handle: "MrBeast" },
  { platform: "youtube",   handle: "MrBeast" },
]).then((rows) => rows.forEach((r) => console.log(r)))

Polling cadence

Most fresh scores complete in 10–90 minutes (depends on platform count + video-heavy posts that need transcription). We recommend polling every 60 seconds for the first 30 minutes, then every 5 minutes after that. Don't hammer — the eta_seconds field on every queued/processing response is your guide.

Errors

All errors return a consistent JSON shape:

{ "error": "error_code", "message": "Human-readable description." }
CodeHTTPMeaning
missing_api_key401No X-API-Key header sent.
invalid_api_key401Key not found or revoked.
insufficient_scope403Read-only key tried to queue a fresh score (cache miss). Use a full-scope key. Cached lookups and job-status reads still work on read-only keys.
insufficient_credits402Balance is 0. Top up to continue.
unsupported_platform400Platform name not in the supported list.
invalid_handle400Handle is empty or too long.
invalid_request400Body didn't match the expected schema.
queue_failed500Couldn't enqueue the scoring job. Retry; if it persists, contact us.
rate_limit_exceeded429120 req/min per key. Retry-After header tells you when to retry.
daily_scrape_limit_exceeded429Per-key daily cap on fresh scrapes hit (a safety backstop). Cached lookups still work; resets at 00:00 UTC. Email us to raise your cap.
daily_discovery_limit_exceeded429Per-key daily cap on discovery operations (/creators/discover + /creators/score-footprint, previews included) — every discovery call performs live open-web searches and profile fetches. Resets at 00:00 UTC.
creator_busy409Another job currently holds this creator (e.g. a scan running in a different workspace). Nothing was charged — retry in ~2 minutes.
not_found404The resource doesn't exist or belongs to another tenant (we don't reveal which).
seed_not_found404The seed profile couldn't be fetched — check the handle and platform. Nothing charged.
no_scoreable_accounts422Discovery found no account on a scoreable platform. Nothing charged.
charge_failed500The credit ledger write failed. Nothing was delivered; retry safely.
internal_error500Unexpected server error. Retry; if it persists, contact us.
discovery_failed502The discovery pipeline errored mid-run. Retry.
payment_setup_failed502Stripe checkout for a background check couldn't be created. Retry.
discovery_unavailable503Account discovery isn't configured on this deployment.
resolve_unavailable503The creator index lookup failed. Retry.
auth_unavailable503Auth backend is temporarily unavailable. Retry.

Webhooks (avoid polling)

For job completion you have two options: poll /api/v1/jobs/{id} or configure a webhook on your API key. With a webhook configured, we POST a score.completed event the moment your job finishes, including the full creator payload — same shape as the GET response.

Set the URL at /account/api. The URL must be HTTPS. When you set or update it we generate a fresh signing secret and show it to you exactly once.

Delivery

We POST a JSON body with these headers:

X-CreatorScore-Event: score.completed       # or score.failed
X-CreatorScore-Signature: <hex HMAC-SHA256>
X-CreatorScore-Delivery: <delivery uuid>
X-CreatorScore-Attempt: 1                   # 1..5
Content-Type: application/json

Body

{
  "event": "score.completed",
  "job_id": "fa9b2c1e-...",
  "platform": "tiktok",
  "handle": "alixearle",
  "creator": { ...same shape as the GET /api/v1/jobs/{id} 'done' creator field... },
  "delivered_at": "2026-05-25T03:42:11Z"
}

Verifying the signature

Compute HMAC-SHA256 over the raw request body using your signing secret. Compare with the X-CreatorScore-Signature header. Reject anything that doesn't match.

// Node.js (Next.js route example)
import { createHmac, timingSafeEqual } from "crypto"

export async function POST(req: Request) {
  const raw = await req.text()
  const sig = req.headers.get("x-creatorscore-signature") || ""
  const expected = createHmac("sha256", process.env.CREATORSCORE_WEBHOOK_SECRET!)
    .update(raw)
    .digest("hex")

  // Constant-time comparison; both must be the same length first.
  if (
    sig.length !== expected.length ||
    !timingSafeEqual(Buffer.from(sig), Buffer.from(expected))
  ) {
    return new Response("bad signature", { status: 401 })
  }

  const event = JSON.parse(raw)
  // ... store event.creator, update your roster, etc.
  return new Response("ok")
}

Retries

We treat any non-2xx response (or connection failure / timeout >10s) as a retryable failure. Backoff is 30s → 1m → 5m → 30m → 2h, with up to 5 attempts. After that the delivery is marked failed and surfaced in your dashboard. Make your handler idempotent — duplicate deliveries are rare but possible.

Idempotency (safe retries)

Send an Idempotency-Key header (any string, 8–128 chars — use a UUID) on a charged POST (/creators/score-footprint, /creators/discover, /creators/vet, /creators/bulk) and retries become safe: the first request runs normally, and a retry with the same key + same body replays the stored response with Idempotency-Replay: true — nothing re-runs, nothing is charged twice.

  • Same key + different body422 idempotency_key_reused (a client bug, never silently replayed)
  • Retry while the original is still running → 409 idempotency_in_flight; retry in a few seconds
  • Non-2xx outcomes are not stored — after a 402, top up and retry the same key for a genuine re-attempt
  • Keys expire after 24 hours
curl -X POST https://creatorscore.io/api/v1/creators/score-footprint \
  -H "X-API-Key: sk_live_..." \
  -H "Idempotency-Key: 9b2f7c3e-8d41-4c5a-b1aa-2f5c9e7d1a03" \
  -H "Content-Type: application/json" \
  -d '{ "platform": "tiktok", "handle": "alixearle", "confirm": true }'

Sandbox / test keys

Create a key with scope: "test" (it's issued with an sk_test_ prefix) and build against the real endpoints without spending a cent: every response is a deterministic fixture — nothing is scraped, scored, queued, charged, or written. Request validation still runs for real, so your error handling is exercised too.

  • Every sandbox payload carries "sandbox": true — assert on it so production code can never be pointed at fixtures unnoticed
  • Fixtures mirror the real response shapes field-for-field; IDs and timestamps are stable, so integration tests can snapshot them
  • Any job_id polled on a test key returns done immediately with the fixture creator; a sandbox background check returns a completed report with sample flags

Rate limits

Each API key is rate-limited to 120 requests per minute (rolling 60-second window). Exceeding the limit returns 429 rate_limit_exceeded with a Retry-After header in seconds. The credit balance remains your real ceiling for fresh-score work — the rate limit is just there to prevent runaway loops.

OpenAPI spec + SDKs

The full machine-readable spec lives at /api/v1/openapi.json (OpenAPI 3.1). Use it to:

  • Import the API into Postman / Insomnia in one click
  • Generate a typed SDK in any language with openapi-generator or openapi-typescript
  • Render interactive docs in Swagger UI / Redocly
# Generate a TypeScript client from the spec
npx openapi-typescript https://creatorscore.io/api/v1/openapi.json -o creatorscore-api.d.ts

Versioning + changes

This is v1. We'll add fields and endpoints without bumping the version; we won't remove or rename anything without bumping to v2 and maintaining v1 for at least 6 months after. Build clients that tolerate unknown fields.

One API for everyone. We don't fork behavior per customer: every integrator gets the same endpoints, the same response shapes, and the same semantics. Per-key configuration is limited to numeric limits (rate, daily caps, discovery pricing) — never different behavior.

Deprecations are announced by email to every active key at least 90 days ahead, and deprecated fields are marked in the OpenAPI spec before removal.

Questions

Email [email protected]. We respond same-day.