API reference

REST over HTTPS, JSON in and out, workspace-scoped bearer keys. Create keys under Settings → API keys; they look like cc_live_… and are shown once.

Authentication

Authorization: Bearer cc_live_xxxxxxxxxxxxxxxx

# missing/invalid/revoked key → 401
{ "error": { "code": "unauthorized", "message": "Missing or invalid API key." } }

Revoke keys any time under Settings — revocation is immediate.

Rate limits

Per key: 120 requests/min for reads, 60/min for POST /posts, 30/min for media staging. Per IP: 240/min overall. Exceeding a budget returns 429 with Retry-After.

{ "error": { "code": "rate_limited", "message": "Too many requests. Slow down and retry." } }

POST/media

Stage a video. Returns a presigned upload URL — PUT the file bytes there, then the asset is ready to attach to posts.

curl -X POST https://crosscast.io/api/v1/media \
  -H "Authorization: Bearer cc_live_..." \
  -H "Content-Type: application/json" \
  -d '{ "filename": "final.mp4", "bytes": 18422011, "content_type": "video/mp4" }'

# content_type must be video/* (default video/mp4); files cap at 1GB (413).
# The PUT's Content-Type header must match content_type or S3 rejects the
# signature.

# → 201
{ "id": "med_01hxkq...", "upload_url": "https://...", "expires_in": 3600 }

curl -X PUT "<upload_url>" --data-binary @final.mp4 -H "Content-Type: video/mp4"

POST/media/:id/cover

Stage a custom cover image for a video. PUT the image bytes to the returned URL, then pass cover_key when creating the post. Applies on Instagram and YouTube; TikTok only accepts a frame time (cover_offset_ms).

curl -X POST https://crosscast.io/api/v1/media/med_01hxkq.../cover \
  -H "Authorization: Bearer cc_live_..." \
  -H "Content-Type: application/json" \
  -d '{ "content_type": "image/jpeg" }'

# → 201
{ "cover_key": "ws_.../med_.../cover.jpeg", "upload_url": "https://...", "expires_in": 3600 }

GET /channels

List your workspace's channels, sorted by slug. Uses the same bearer authentication and read rate limits as GET /posts.

GET https://crosscast.io/api/v1/channels

# → 200
{ "channels": [{ "id": "ch_01...", "slug": "mapsofwhy", "name": "Maps of Why" }] }

POST/posts

Create a post. Targets default to the channel's accounts; captions default to the channel's templates rendered with your title. Both are overridable per call. Send an Idempotency-Key header and retries return the original post instead of double-scheduling — always do this from pipelines. (Idempotency applies to post creation only, not media staging.)

Use schedule: "draft" to hold a post for human approval in Dashboard → Queue → Drafts. Drafts have status draft, scheduled_at: null, and no targets. Captions and cover are saved at creation. Explicit accounts are ignored for drafts: approval resolves the channel's current accounts. Choose a channel when creating a draft; a draft without one cannot be scheduled. Draft creation supports the same Idempotency-Key retries. All post responses include channel_slug (or null).

curl -X POST https://crosscast.io/api/v1/posts \
  -H "Authorization: Bearer cc_live_..." \
  -H "Idempotency-Key: render-7f3a2c" \
  -H "Content-Type: application/json" \
  -d '{
    "channel": "mapsofwhy",
    "media_id": "med_01hxkq...",
    "title": "why the mississippi runs backwards",
    "schedule": "next_slot",          // or "draft", "now", or "2026-09-01T17:00:00Z"
    "captions": {                      // optional per-platform overrides
      "tiktok": "why the mississippi runs backwards #maps #geography"
    },
    "cover_offset_ms": 1200,           // optional frame-time cover
    "cover_key": "ws_.../cover.jpeg",  // optional uploaded cover (see /media/:id/cover)
    "accounts": ["acc_01..."]          // optional subset of the channel
  }'

# → 201
{ "id": "post_01hxkr...", "status": "scheduled", "channel_slug": "mapsofwhy",
  "targets": [
    { "id": "tgt_01...", "platform": "tiktok",    "account": "@mapsofwhy", "at": "2026-09-01T17:00:00Z" },
    { "id": "tgt_02...", "platform": "instagram", "account": "@mapsofwhy", "at": "2026-09-01T17:00:00Z" },
    { "id": "tgt_03...", "platform": "youtube",   "account": "mapsofwhy",  "at": "2026-09-01T17:00:00Z" }
  ] }

GET/posts

List posts, newest first. Filter with ?status=draft|scheduled|publishing|published|partial|failed|canceled, cap with ?limit= (max 100), and page by passing the response's next_before back as ?before=.

curl "https://crosscast.io/api/v1/posts?status=failed&limit=10" \
  -H "Authorization: Bearer cc_live_..."

# → 200
{ "posts": [ { "id": "post_01...", "title": "...", "status": "failed",
    "targets": [ { "platform": "tiktok", "status": "failed", "at": "..." } ] } ] }

GET/posts/:id

Status, permalinks, and the latest metrics snapshot per target.

curl https://crosscast.io/api/v1/posts/post_01hxkr... \
  -H "Authorization: Bearer cc_live_..."

# → 200
{ "id": "post_01hxkr...", "status": "published",
  "targets": [
    { "platform": "instagram", "status": "published",
      "permalink": "https://www.instagram.com/reel/...",
      "metrics": { "views": 8211, "likes": 402, "avg_watch_time_ms": 11400 } },
    { "platform": "tiktok", "status": "on_phone",
      "note": "delivered to TikTok inbox; finish in-app" }
  ] }

PATCH/posts/:id DELETE/posts/:id

PATCH edits title, captions, moves still-scheduled targets with a new schedule, or aborts individual platforms via cancel_targets. DELETE removes a post that hasn't reached any platform yet (409 otherwise — published records are permanent).

For drafts, PATCH with schedule creates targets from the channel's current accounts and changes status to scheduled. Schedule accepts now, next_slot, or a future ISO 8601 timestamp. Next slot requires channel slots. Missing accounts, unavailable media, and YouTube duration limits are checked on approval. Scheduling a post outside draft/scheduled status returns 409; PATCH cannot turn a scheduled post back into a draft.

curl -X PATCH https://crosscast.io/api/v1/posts/post_01hxkr... \
  -H "Authorization: Bearer cc_live_..." \
  -H "Content-Type: application/json" \
  -d '{ "schedule": "2026-09-02T17:00:00Z" }'

curl -X DELETE https://crosscast.io/api/v1/posts/post_01hxkr... \
  -H "Authorization: Bearer cc_live_..."   # → 204

Errors

Standard HTTP status codes with a stable machine-readable shape. Platform-side failures include the platform's own message verbatim — debugging distribution should never require guessing.

{ "error": { "code": "quota_exceeded",
  "message": "YouTube upload quota reached for today (6/6). Next reset 08:00 UTC." } }