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/videosWhy 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
| Surface | Allowed with pk_*? |
|---|---|
| Read & mutate caller's own videos / streams / restreams / rooms | Yes |
Read & mutate caller's own profile (/me, /me/avatar, /me/embed-allowlist) | Yes |
| Sell access — create subscription checkouts, list tiers, list own purchases | Yes |
| Read financial data — revenue, payments, payouts, ledger summary, purchases, own subscriptions | Only 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 /tiers | Only 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-keys | Listing: 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 resources | No — 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 management | No — surface is not mounted under /api/v1 |
Mutate User.role, User.balance, or any system table | No — 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.
| Gate | Endpoints | Required |
|---|---|---|
finance:read | GET /monetization/revenue, GET /monetization/payments, GET /me/payouts, GET /analytics/summary, GET /analytics/stats, GET /purchases, GET /videos/:id/purchases, GET /me/subscriptions | FULL_ACCESS, or finance:read / finance:write / finance:* / * |
finance:write | Tier price mutations: POST /tiers, PATCH /tiers/:id, DELETE /tiers/:id | FULL_ACCESS, or finance:write / finance:* / * |
| OPEN | Content 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; thepk_*key alone does not rescue it. - If the JWT verifies but its
subdoes not equal thepk_*key owner's user id —401 INVALID_USER_TOKEN. Multi-user flow over a sharedpk_*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/meIdempotency: 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/subscriptionPOST /v1/streamsPOST /v1/restreamsPOST /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
| Scenario | Response |
|---|---|
| Header absent | Normal behavior. No dedup. |
| First request with key | Handler runs. Response cached 24h. |
| Retry with same key + same body | Cached response replayed. X-DCAST-Idempotent-Replay: true header set. |
| Retry with same key + different body | 422 IDEMPOTENCY_KEY_REUSE. Use a fresh key. |
| Two concurrent requests, same key | One runs the handler; the other waits ≤3s and replays its result. If it can't wait long enough → 409 IDEMPOTENCY_IN_FLIGHT. |
| Key format invalid | 400 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.
