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/creators/{platform}/{handle}/report?sections=...&format=json|pdf
The FULL report for a scored creator (whole footprint from any one handle): risk_flags WITH EVIDENCE (post_url, caption/transcript excerpt, on_screen_text, why_flagged, signed thumbnail + frame image links), brand_mentions with per-brand disclosure verdict (undisclosed = FTC exposure), controversy (summary + filtered web findings), drivers (SHAP per agent), audience, following, score_history, engagement, narrative, detailed_analysis, and pdf_url (browser-openable, no key, expires in 24h). format=pdf streams the PDF.
Billing: 0 credits for a creator this account already paid to score; 1 credit as a cached read otherwise; 404 not_scored/score_stale with no charge. Never scrapes. Evidence image links expire — do not cache them.
Response 200: { "status": "ok", "access": "included"|"paid_cache_read", "sections": [...], "report": {...}, "credits_charged" }

## GET /api/v1/reports/{token}
The pdf_url target. No API key. HMAC-signed, expires (410 when expired), ownership re-checked at download.

## GET /api/v1/background-checks  and  GET /api/v1/background-checks/{id}?format=json|pdf
List this account's Social Background Checks / read one. Completed checks return the full categorized report (report.categories[] with per-flag description, content_preview, post_url, risk_weight, wayback marker, evidence.thumbnail/frame signed links; summary with categories_clean, categories_unassessed + reason, coverage gaps, archive_timeline), a flat flags[] for compatibility, and pdf_url. A Background Check is a separate product: NO score, paid via Stripe not credits.

## GET /api/v1/jobs/{job_id}?include=...
Poll one job. include= takes the same options as the creator lookup (narrative, risk_flags, detailed_analysis, engagement, velocity, report, all) and is free.
Response 200 (done): { "job_id", "status": "done", "progress": 100, "completed_at", "creator": {...same as cached payload...}, "report_url": "/api/v1/creators/{platform}/{handle}/report" }
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", ...], "include": "narrative,risk_flags" }
Response 200: { "jobs": [...], "summary": { "total", "done", "in_flight", "failed", "not_found" } }

# include= options (creator lookup, jobs, bulk-status)
narrative, velocity, risk_flags (footprint-wide, WITH evidence + risk_flags_summary), detailed_analysis, engagement, report (embeds the full report), all. Free on every endpoint that accepts it.

# 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 — score_creator_footprint, vet_creator (cache-only, so an agent can't fan out into surprise scrapes), get_creator_report (every flag with its evidence, brand mentions, drivers, and a PDF link), check_background_check, 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 — footprint-wide active flags with evidence (up to 25): category, severity, confidence, description, signal_source, score_impact (scored vs visibility-only), why_flagged (the reviewer's reasoning), post_url, and an evidence object with signed thumbnail and frame image links, caption_excerpt / transcript_excerpt / on_screen_text, and web_findings. Comes with risk_flags_summary (count, titles, by_severity, cleared_count, media_expires_at, truncated). Image links expire — re-request rather than caching them
  • detailed_analysis — the analyst brief: methodology, risk assessment, brand fit, campaign recommendations with fit scores, ROI projections, brand-category fit, peer comparison
  • report — embeds the full report (brand mentions, controversy, drivers, audience, following risk, score history, PDF link) on the same call, same credit
  • all — everything above
  • 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_..."
GET/api/v1/creators/{platform}/{handle}/report

The full report — every finding with its evidence (JSON or PDF)

Everything the brand dashboard shows for a creator you've scored, as structured JSON your own tooling can render, or as the PDF vetting dossier. Pass ANY one of the creator's handles — the report covers their whole footprint.

  • risk_flags — every active flag with post_url, caption_excerpt / transcript_excerpt / on_screen_text, why_flagged, score_impact, and signed evidence.thumbnail + evidence.frame image links (the exact frame we captured before the platform CDN link expired)
  • brand_mentions — each brand with a disclosure verdict: disclosed, undisclosed (the FTC exposure), unverified, or organic mention, plus the posts behind it
  • controversy — the summary and web findings, after we filter out the creator's own profiles, Wikipedia, and bio pieces (findings_filtered_out tells you how many we dropped)
  • drivers — SHAP drivers per agent (why each of the seven numbers is what it is) and per dimension
  • audience, following, score_history, engagement — demographics per platform, flagged accounts the creator follows, the full score timeline, engagement metrics
  • creator — the headline payload with narrative and detailed_analysis, plus is_stale / score_age_days
  • pdf_url — a browser-openable PDF link (no API key needed) valid for 24 hours. Paste it into a deck or a chat.
  • Every image object carries a source: archive means it is our own durable copy, served through a signed link that expires at expires_at; cdn means we only hold the platform's original URL, which the platform may have already expired. Render images when you read the report — never store the links.

Billing

Included (0 credits) for any creator this account has already paid to score — the evidence behind the number is part of the number. For a creator scored by someone else, a fresh (≤30-day) report is a flat cached-score credit. Nothing is ever charged when there is nothing to return (404 not_scored / score_stale). It never scrapes.

Request

# JSON, all sections
curl https://creatorscore.io/api/v1/creators/tiktok/alixearle/report \
  -H "X-API-Key: sk_live_..."

# Only the risk + brand sections
curl "https://creatorscore.io/api/v1/creators/tiktok/alixearle/report?sections=risk_flags,brand_mentions" \
  -H "X-API-Key: sk_live_..."

# The PDF dossier
curl "https://creatorscore.io/api/v1/creators/tiktok/alixearle/report?format=pdf" \
  -H "X-API-Key: sk_live_..." -o alixearle.pdf

Response (200, abridged)

{
  "status": "ok",
  "access": "included",
  "credits_charged": 0,
  "report": {
    "creator": { "score": 72, "tier": "Good", "score_scope": "footprint", "agents": { ... }, "narrative": "...", "is_stale": false },
    "accounts": [{ "platform": "tiktok", "handle": "alixearle", "followers": 7100000 }, { "platform": "instagram", ... }],
    "risk_flags": {
      "count": 3, "titles": ["profanity", "alcohol_presence", "web_controversy"], "by_severity": { "medium": 2, "low": 1 },
      "cleared_count": 4, "media_expires_at": "2026-09-04T18:02:11Z",
      "items": [{
        "category": "profanity", "category_label": "Profanity", "severity": "medium", "confidence": 0.91,
        "description": "Repeated strong language in captions", "signal_source": "caption", "score_impact": "visibility_only",
        "why_flagged": "Casual swearing in a comedy context; surfaced for visibility, not penalized.",
        "post_url": "https://www.tiktok.com/@alixearle/video/73...",
        "evidence": {
          "thumbnail": { "url": "https://...signed...", "expires_at": "2026-09-04T18:02:11Z", "source": "archive" },
          "frame": null, "caption_excerpt": "...", "transcript_excerpt": null, "on_screen_text": null, "web_findings": []
        }
      }]
    },
    "brand_mentions": { "total_brands": 14, "undisclosed": 1, "disclosed": 5, "unverified": 2, "organic": 6,
      "brands": [{ "brand": "Tarte", "verdict": "undisclosed", "verdict_label": "undisclosed partnership", "posts": [{ "post_url": "..." }] }] },
    "controversy": { "summary": null, "web_findings": [], "findings_filtered_out": 3 },
    "drivers": { "agents": [{ "agent": "Content Risk", "score": 74, "negative": [{ "text": "Casual profanity in 6% of captions", "shap_contribution": -1.8 }] }] },
    "audience": { "tiktok": { "locations": [{ "country": "United States", "country_code": "US", "percentage": 62.5 }] } },
    "following": { "total_followed_accounts": 240, "flagged": [], "clean_count": 240 },
    "score_history": [{ "date": "2026-06-01", "score": 70 }, { "date": "2026-08-01", "score": 72 }],
    "pdf_url": "https://creatorscore.io/api/v1/reports/eyJ...",
    "pdf_url_expires_at": "2026-09-04T18:02:11Z"
  }
}

Transparency contract: every count comes with its titles, every flag with its proof. If a section is empty we say so (summary: null,flagged: []) rather than omit it.

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 response is the full categorized report the dashboard renders:

  • report.categories[] — Hate Speech, NSFW, Violence, Profanity, Political, Legal, Public Feuds, Brand Criticism, Substance, Deceptive, … each with its flags: description, content_preview, post_url, detected_at, risk_weight (risk counted vs context shown-only), source: "wayback" for deleted content we recovered, and evidence.thumbnail / evidence.frame signed image links
  • summarycategories_clean, categories_unassessed (with the reason), coverage.platforms_not_read, content window, archive_timeline
  • flags[] — the original flat list, kept for compatibility, now with evidence
  • pdf_url — a browser-openable PDF (no key) valid 24h; or pass ?format=pdf to stream it

Reviewer-dismissed findings are excluded. GET /api/v1/background-checks lists all of this account's checks (newest first, with PDF links on completed ones). A Background Check produces no score.

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": { ...GET /api/v1/jobs/{id} 'done' creator field, WITH narrative + risk_flags (evidence) inline... },
  "report_url": "/api/v1/creators/tiktok/alixearle/report",
  "delivered_at": "2026-05-25T03:42:11Z"
}

The webhook carries the narrative and the evidence-bearing risk flags so a reviewer can act on it directly; report_url is the full report (brand mentions, drivers, PDF link) and costs nothing extra for the creator you just scored.

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.