Partner API base path is /api/v1 on https://api.dcast.pro. This site documents the live surface; use the Quickstart QA curls to validate keys and routes.

Authentication

Send your API key as a Bearer token in the Authorization header. The X-API-Key header is also accepted; a key in the query string is not. A key is pk_ followed by 40 hex characters.

Header

curl -s -H "Authorization: Bearer pk_YOUR_KEY_HERE" https://api.dcast.pro/api/v1/videos

Why not a query parameter

A key in the URL is written down by every proxy, CDN and access log it passes through. We measured our own logs on 2026-08-09 and found three different live secrets recorded that way, so the query form was removed rather than documented with a warning. A key in the query string is not read: a request that carries only ?api_key= gets 401 UNAUTHORIZED.

Invalid or expired key

{
  "success": false,
  "error": {
    "code": "INVALID_KEY",
    "message": "Invalid or unknown API key"
  }
}

Other 401 codes: UNAUTHORIZED (no key sent), INVALID_KEY_FORMAT (not a pk_ key), KEY_DISABLED (revoked), KEY_EXPIRED. Full list: Errors & limits.

Create and manage API keys in the dashboard: Settings → API Keys. Use different keys per environment (e.g. test vs production) and revoke compromised keys promptly.

Key scope policy

Every pk_* key is creator-scoped. A key acts on resources owned by the account that minted it — never on resources owned by other creators, and never on system / infrastructure / payout state.

Capability matrix

SurfaceAllowed with pk_*?
Read & mutate caller's own videos / streams / restreams / roomsYes
Read & mutate caller's own profile (/me, /me/avatar, /me/embed-allowlist)Yes
Sell access — create subscription checkouts, list tiers, list own purchasesYes
Read financial data — revenue, payments, payouts, ledger summary, purchases, own subscriptionsOnly with an Owner-class key (FULL_ACCESS) or finance:read permission — see Financial scope. READ_ONLY / STANDARD → 403 FINANCIAL_SCOPE_REQUIRED
Mutate tier prices — POST / PATCH / DELETE /tiersOnly with FULL_ACCESS or finance:write — see Financial scope. READ_ONLY / STANDARD → 403 FINANCIAL_SCOPE_REQUIRED
Public reads — /channel/:username, /videos/:id/related (anchor must be PUBLIC)Yes
List, create, revoke API keys — GET / POST / DELETE /me/api-keysListing: yes (key material is redacted). Minting and revoking on the account: only with an Owner-class credential — a FULL_ACCESS key, or an X-User-Token from POST /v1/auth/login. A READ_ONLY / STANDARD key → 403 OWNER_SCOPE_REQUIRED, and no key can mint above its own grant (403 MINT_CEILING_EXCEEDED). Any key may revoke itself, so a leaked key is killable by whoever noticed the leak.
Read or mutate other creators' private resourcesNo — returns 404 NOT_FOUND
Initiate payout / withdrawal — POST /monetization/connect-onboard, GET /monetization/connect-dashboard (the read-only GET /monetization/connect-status is open to any key)ADMIN-ONLY — returns 403 ADMIN_ONLY for pk_* keys
Worker / fleet / server / capability managementNo — surface is not mounted under /api/v1
Mutate User.role, User.balance, or any system tableNo — not reachable from this surface. The caller's own partnerApiKeys rows are the one exception, through the Owner-class door in the row above.

Error envelope

# No key sent
{ "success": false, "error": { "code": "UNAUTHORIZED", "message": "API key is required. Send it as "Authorization: Bearer pk_..." or the X-API-Key header." } }

# Unknown key
{ "success": false, "error": { "code": "INVALID_KEY", "message": "Invalid or unknown API key" } }

# READ_ONLY key attempting a write
{ "success": false, "error": { "code": "INSUFFICIENT_SCOPE", "message": "Scope 'READ_ONLY' does not allow POST operations" } }

# Admin-gated endpoint called with creator key
{ "success": false, "error": { "code": "ADMIN_ONLY", "message": "Payout account management is admin-controlled. Contact DCAST support to onboard or update your Stripe Connect account." } }

# Foreign resource (we never reveal existence)
{ "success": false, "error": { "code": "NOT_FOUND", "message": "Resource not found" } }

Financial scope

Money-bearing surfaces are gated on the key's scope, not the HTTP method. A key reaches financial data only when it is Owner-class (FULL_ACCESS scope) or carries an explicit finance:read / finance:write permission. READ_ONLY and STANDARD keys are denied with 403 FINANCIAL_SCOPE_REQUIRED even on a GET.

GateEndpointsRequired
finance:readGET /monetization/revenue, GET /monetization/payments, GET /me/payouts, GET /analytics/summary, GET /analytics/stats, GET /purchases, GET /videos/:id/purchases, GET /me/subscriptionsFULL_ACCESS, or finance:read / finance:write / finance:* / *
finance:writeTier price mutations: POST /tiers, PATCH /tiers/:id, DELETE /tiers/:idFULL_ACCESS, or finance:write / finance:* / *
OPENContent reads stay open to any key: GET /analytics/videos, GET /analytics/streams, GET /analytics/geography, and the tier catalogue GET /tiers / GET /tiers/:id.Any pk_* key (owner-scoped)
# READ_ONLY or STANDARD key hitting a financial endpoint
{ "success": false, "error": { "code": "FINANCIAL_SCOPE_REQUIRED",
  "message": "This endpoint requires an Owner-class key (FULL_ACCESS) or a finance scope." } }

Note: reading revenue/payouts with a READ_ONLY key, and editing tier prices with a STANDARD key, used to be (incorrectly) permitted. Both are now denied — provision an Owner-class key for any storefront feature that reads money or changes prices. Tier-catalogue reads and content analytics are unaffected. See also the Monetization API.

Optional second factor: X-User-Token

Desktop and native-app clients can attach an additional X-User-Token header carrying the JWT returned by POST /api/v1/auth/login. The token represents the end-user who signed in inside the app instance, independent of which account owns the pk_* key.

Enforcement rules — when X-User-Token is present, it is always validated:

  • If the header is missing or empty, the request is scoped to the pk_* key owner (default behaviour).
  • If the header is present but the JWT fails to verify (garbage value, wrong signature, expired) — 401 INVALID_USER_TOKEN. The request is rejected; the pk_* key alone does not rescue it.
  • If the JWT verifies but its sub does not equal the pk_* key owner's user id — 401 INVALID_USER_TOKEN. Multi-user flow over a shared pk_* key is currently disabled; provision one key per end-user, or omit the header entirely.
  • If the JWT verifies and matches the key owner, the request proceeds normally.
{
  "success": false,
  "error": {
    "code": "INVALID_USER_TOKEN",
    "message": "X-User-Token is invalid or expired. Omit the header to use pk_ key owner scope, or obtain a fresh token via POST /api/v1/auth/login."
  }
}

Request correlation: X-DCAST-Request-Id

Every response carries an X-DCAST-Request-Id header containing a UUIDv4 that identifies the request in dcast backend logs. Quote it in any support ticket so the team can grep the exact lifecycle in seconds.

HTTP/2 200
x-dcast-request-id: 26658639-a503-42d6-b718-15c59c9c0455
...

You may also supply the header on the way in — if the value is a strict UUIDv4 it will be echoed back. Anything else (non-UUID, UUIDv1, oversized strings) is silently replaced by a server-minted one to keep the format predictable.

curl -sS -H "X-DCAST-Request-Id: $(uuidgen)" \
  -H "Authorization: Bearer pk_..." \
  https://api.dcast.pro/api/v1/me

Idempotency: Idempotency-Key

Monetary and resource-creating POSTs accept a Stripe-style Idempotency-Key header. The first request runs the handler and caches the response for 24 hours; any retry with the same key and the same body replays the cached response unchanged — no duplicate Stripe checkout sessions, no duplicate streams or rooms.

Supported endpoints

  • POST /v1/checkout/subscription
  • POST /v1/streams
  • POST /v1/restreams
  • POST /v1/rooms

Key format

Opaque string, 8–255 characters from [A-Za-z0-9_-]. A UUID, a ULID, or any hash works.

curl -sS -X POST https://api.dcast.pro/api/v1/streams \
  -H "Authorization: Bearer pk_..." \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"title":"My Stream","visibility":"PUBLIC"}'

Behavior

ScenarioResponse
Header absentNormal behavior. No dedup.
First request with keyHandler runs. Response cached 24h.
Retry with same key + same bodyCached response replayed. X-DCAST-Idempotent-Replay: true header set.
Retry with same key + different body422 IDEMPOTENCY_KEY_REUSE. Use a fresh key.
Two concurrent requests, same keyOne runs the handler; the other waits ≤3s and replays its result. If it can't wait long enough → 409 IDEMPOTENCY_IN_FLIGHT.
Key format invalid400 INVALID_IDEMPOTENCY_KEY.

Cache scope is per pk_* key per (method, path, idempotency-key). Two different keys, two different paths, or two different methods do not collide. Cache entries expire automatically after 24 hours.

Security & scope

Treat your pk_* key as a long-lived secret. Never commit it to a repo, never embed it in browser-side JavaScript, never send it from an unauthenticated form. Rotate immediately on suspected leak.

For public read endpoints (/channel/:username, /videos/:id/related, /videos/:id/views) you may omit the Authorization header entirely — sending a key only attaches it for attribution / personalization.

Authentication — dcast.pro API docs