Skip to content
Back to home

Developer Docs

Creator Offload REST API · v1

Full API reference

Quickstart

Get from zero to your first API response in under five minutes.

Step 1 — Create an API key

step-1.tsts
// 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

step-2.tsts
// 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

step-3.tsts
// 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.

Authentication

All API requests require a Bearer token in the Authorization header.

co_live_…Every key uses the single co_live_ prefix. A key operates against whichever workspace it is scoped to — including a sandbox for safe testing.
api-client.tsts
// 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();}

Scopes

Each API key is granted a subset of scopes. Requests that require a scope the key lacks return 403 forbidden.

campaigns:readList and retrieve campaigns
campaigns:writeCreate and update campaigns
creators:readList and retrieve creators
creators:writeCreate and update creators
submissions:readList and retrieve submissions
submissions:writeCreate submissions and replace media
reviews:writePost QA checklist updates and reviews
rights:readRead rights grants
rights:writeUpsert rights grants
assets:readRead asset readiness and metadata
webhooks:readList webhook endpoints and deliveries
webhooks:writeCreate and manage webhook endpoints

Errors

All error responses use a consistent JSON envelope regardless of HTTP status.

error-shape.tsts
// 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 requests

Rate Limits

API requests are rate-limited per API key. The current limits are:

Read endpoints120 req / min per key
Write endpoints60 req / min per key
Webhook deliveriesNo limit on inbound payloads

When a limit is exceeded the API returns 429 rate_limited. Implement exponential back-off with jitter in your client.

Idempotency

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.

idempotency.tsts
// 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.

Webhooks

Subscribe to real-time events by registering a webhook endpoint in Settings → Webhooks.

Available event types

submission.createdsubmission.qa_checklist_updatedsubmission.approvedsubmission.revision_requestedsubmission.rejectedrights_grant.createdrights_grant.expiringrights_grant.expiredasset.createdasset.ready

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)

verify-webhook.tsts
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.

Versioning & Deprecation

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.

Additive — no version changeNew endpoint · new optional response field · new webhook event type · new enum value. Ships in v1.
Breaking — new versionRemoving or renaming a field · changing a type · removing an endpoint · changing auth or error semantics. Requires a new major version.

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.

webhook-version.tsts
{  "id": "01jx...",  "type": "submission.approved",  "apiVersion": "v1",            // ← pin/branch on this  "createdAt": "2026-06-15T...",  "workspaceId": "01jw...",  "data": { /* resource snapshot */ }}
Advance notice before deprecationAt least 6 months
Where deprecations are announcedChangelog + Deprecation / Sunset response headers
Old version availabilityMaintained through the full notice window
Per-endpoint signalDeprecation: true + Sunset: <date> headers (RFC 8594)

See the Changelog for everything we've shipped and any active deprecations.

Sandbox & Testing

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.

Caps (hard, per account)1 campaign · 5 creators · 10 submissions
At the cap403 { code: "sandbox_quota_exceeded" } from the create endpoint
EmailsSuppressed — no real creators are contacted
WebhooksStill dispatched; every payload carries sandbox: true
Rights PDFsWatermarked "SANDBOX — not binding"
sandbox.tsts
// 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.ts

Webhook payloads from a sandbox

sandbox-webhook.jsonts
{  "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.

We collect a bit of analytics

To understand how visitors use our platform, we record usage events along with some of your non-sensitive device information and approximate location. You may opt out anytime. Privacy Policy