Get from zero to your first API response in under five minutes.
Step 1 — Create an API key
// 1. Create an API key in Settings → API Keys// Choose scopes matching what you need, e.g. campaigns:readconst API_KEY = 'co_live_...';Navigate to Settings → API Keys in the dashboard to create a key.
Step 2 — Make your first request
// 2. Make your first requestconst res = await fetch('https://app.creatoroffload.com/api/v1/campaigns', { headers: { Authorization: `Bearer ${API_KEY}` },});const { data } = await res.json();console.log(data); // CampaignResponse[]Step 3 — Full campaign-to-asset walk-through
// 3. Full walk-through: campaign → submission → rights → asset// See #sandbox for a safe sandbox workspace to test against.// a) List campaignsconst campaigns = await apiFetch('/api/v1/campaigns');// b) List submissions for a campaignconst submissions = await apiFetch( '/api/v1/submissions?campaignId=' + campaigns.data[0].id);// c) Check asset readiness for a submissionconst readiness = await apiFetch( '/api/v1/submissions/' + submissions.data[0].id + '/asset-readiness');See Sandbox & Testing for a safe, isolated sandbox workspace to test against.
All API requests require a Bearer token in the Authorization header.
co_live_ prefix. A key operates against whichever workspace it is scoped to — including a sandbox for safe testing.// Inject the Bearer token on every requestasync function apiFetch(path: string, init: RequestInit = {}) { const res = await fetch(`https://app.creatoroffload.com${path}`, { ...init, headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${API_KEY}`, ...(init.headers as Record<string, string> | undefined), }, }); if (!res.ok) { const body = await res.json(); throw new Error(body.error?.message ?? `HTTP ${res.status}`); } return res.json();}Each API key is granted a subset of scopes. Requests that require a scope the key lacks return 403 forbidden.
campaigns:readList and retrieve campaignscampaigns:writeCreate and update campaignscreators:readList and retrieve creatorscreators:writeCreate and update creatorssubmissions:readList and retrieve submissionssubmissions:writeCreate submissions and replace mediareviews:writePost QA checklist updates and reviewsrights:readRead rights grantsrights:writeUpsert rights grantsassets:readRead asset readiness and metadatawebhooks:readList webhook endpoints and deliverieswebhooks:writeCreate and manage webhook endpointsAll error responses use a consistent JSON envelope regardless of HTTP status.
// All API errors use this envelope:{ "error": { "code": "not_found", // machine-readable code "message": "...", // human-readable description "requestId": "01jx..." // UUIDv7 for support traces }}// Common codes:// unauthorized — missing or invalid API key// forbidden — key lacks the required scope// not_found — resource does not exist in this workspace// validation_error — request body failed schema validation// conflict — idempotency key reused with different body// rate_limited — too many requestsAPI requests are rate-limited per API key. The current limits are:
When a limit is exceeded the API returns 429 rate_limited. Implement exponential back-off with jitter in your client.
All mutating endpoints (POST, PUT, PATCH, DELETE) accept an optional Idempotency-Key header. Replaying the same key within 24 hours returns the cached response without re-executing the request.
// Add the header to any mutating request:await fetch('/api/v1/submissions', { method: 'POST', headers: { Authorization: `Bearer ${API_KEY}`, 'Idempotency-Key': crypto.randomUUID(), // UUIDv4 or v7 'Content-Type': 'application/json', }, body: JSON.stringify({ ... }),});// Replaying the same key within 24 h returns the cached 201 response.Subscribe to real-time events by registering a webhook endpoint in Settings → Webhooks.
Available event types
Payload envelope
Every webhook POST includes id, type, apiVersion, createdAt, workspaceId, and data (the resource snapshot). The signature is delivered in the X-CO-Signature header as t=timestamp,v1=signature.
Signature verification (Node.js)
import { createHmac, timingSafeEqual } from 'node:crypto';function verifySignature( rawBody: string, secret: string, header: string,): boolean { const [tPart, v1Part] = header.split(','); const timestamp = tPart.split('=')[1]; const expected = v1Part.split('=')[1]; const sig = createHmac('sha256', secret) .update(`${timestamp}.${rawBody}`) .digest('hex'); return timingSafeEqual(Buffer.from(sig), Buffer.from(expected));}// Usage in Express:app.post('/webhooks', express.raw({ type: 'application/json' }), (req, res) => { const ok = verifySignature( req.body.toString(), process.env.WEBHOOK_SECRET!, req.headers['x-co-signature'] as string, ); if (!ok) return res.sendStatus(401); const event = JSON.parse(req.body.toString()); console.log(event.type, event.data); res.sendStatus(200);});Retries
Failed deliveries (non-2xx or timeout) are retried up to 5 times with exponential back-off (5 s → 30 s → 5 min → 30 min → 2 h). You can inspect delivery history in Settings → Webhooks.
The API is versioned in the URL path /api/v1. The major version only changes for breaking changes; additive changes ship within v1 and are announced on the Changelog.
The apiVersion field
Every webhook envelope already carries apiVersion (see Webhooks). Branch on apiVersion, not on the presence of individual fields — new fields can be added within a version; apiVersion only changes for a new major version.
{ "id": "01jx...", "type": "submission.approved", "apiVersion": "v1", // ← pin/branch on this "createdAt": "2026-06-15T...", "workspaceId": "01jw...", "data": { /* resource snapshot */ }}See the Changelog for everything we've shipped and any active deprecations.
A sandbox is a real, fully-isolated workspace flagged kind='sandbox'. You get one sandbox per account. Create it from the workspace switcher (“Create as sandbox”), switch into it, then mint an API key — every key uses the single co_live_ prefix.
// A sandbox is a real, isolated workspace flagged kind='sandbox'.// Create one in the app (Workspace switcher → "Create as sandbox"),// then mint a key while the sandbox is your active workspace.// All keys use the single co_live_ prefix — there is no co_test_.const API_KEY = 'co_live_...'; // scoped to your sandbox workspace// Caps (per account, hard limits): 1 campaign, 5 creators, 10 submissions.// Exceeding a cap returns:// 403 { error: { code: 'sandbox_quota_exceeded', ... } }// In a sandbox:// • Outbound emails are SUPPRESSED (no real creators are contacted).// • Webhooks STILL fire, and every payload carries `sandbox: true`.// • Rights PDFs are watermarked "SANDBOX — not binding".// Seed a demo sandbox from the API (mints a co_live_ key):// pnpm --filter api ts-node scripts/seed-sandbox.tsWebhook payloads from a sandbox
{ "id": "01jx...", "type": "submission.created", "apiVersion": "v1", "sandbox": true, // ← present & true only for sandbox events "createdAt": "2026-07-07T...", "workspaceId": "01jw...", "data": { /* resource snapshot */ }}// On standard (non-sandbox) events the `sandbox` field is ABSENT// (never false) — treat "missing" as live.