API reference

Base URL https://api.superdrm.com (hosted) or https://api.<your domain> (on-premises). JSON in and out unless stated. Every error is {"error": "<message>", "code": "<code>"} with a 4xx/5xx status.

#Authentication

CredentialHeader / queryWho holds itUsed on
Tenant API key sdrm_live_… / sdrm_test_…Authorization: Bearer <key>The tenant's servers/v1/keys*, /v1/cpix, /v1/tokens, /v1/usage*, /v1/me, /v1/attest/*, and the server-relay form of the licence routes
Licence token (HS256 JWT signed with the tenant's token_secret)?lt=<token>The player, minted by the tenant's backend/v1/<tenant>/license/*
Operator tokenAuthorization: Bearer <SUPERDRM_ADMIN_TOKEN>Operators of an install/v1/admin/*

API key scopes: keys:write, keys:read, licence:serve, usage:read, admin (grants all). Only a SHA-256 of a key is stored; the first 15 characters are shown as its prefix.

#Service

#GET /

Product banner. No auth.

#GET /health

No auth. 200 when the vault and database answer, 503 otherwise.

JSON
{ "ok": true,
  "vault": { "provider": "pkcs11", "id": "pkcs11:superdrm-kek-1", "ok": true, "detail": "YubiHSM 2" },
  "db": { "ok": true, "detail": "postgres" },
  "fairplay_ksm": { "ok": true, "detail": "production credentials · Apple FPS SDK 26.0.4" },
  "widevine": { "env": "prod", "partners": ["widevine:prod"], "builtin_uat": false },
  "install": { "mode": "licensed", "org": "Acme", "tenant_cap": 10, "tenants": 3 },
  "time": "2026-09-16T12:00:00.000Z" }

#Key Protect

#POST /v1/keys — scope keys:write

Issue (or fetch, if content_id exists) one content key.

Body fieldTypeNotes
content_iduuidOptional; generated when absent. Idempotent: the same id returns the same key with created:false
labelstringYour reference, e.g. superfile-<file id>
schemecbcs | cencDefault cbcs
systemsstring[]Subset of the tenant's enabled systems; default all

Response: content_id, kid (hex), key (hex, plaintext over TLS), iv (hex), scheme, systems, wv_pssh (base64 pssh box or null), pr_pssh, fp_hls (the URI=… attribute set for EXT-X-KEY), wv_provider, key_source (widevine | local | import), licence_urls (EME key system → URL), fairplay_cert_url, created.

#POST /v1/keys/import — scope keys:write

Bring your own key (a previous vendor's CPIX, your KMS). SuperDRM seals it and licences Widevine for it via provider-supplied keys.

Body fieldTypeNotes
content_iduuidRequired
kid, key32 hexRequired, 16 bytes each
iv32 hexOptional; random when absent
scheme, label, systemsAs above
wv_pssh, pr_psshbase64Optional; SuperDRM builds a Widevine PSSH when none is given

200 with the same shape as /v1/keys. 409 when the id already holds a different kid. 400 on bad hex.

#GET /v1/cpix?k=<content_id>&c=<label>&EncryptionScheme=cbcs|cenc — scope keys:write

Same as POST /v1/keys, returned as CPIX 2.3 XML (application/xml). Elements: ContentKey kid explicitIVPlainValue; one DRMSystem per system with PSSH (Widevine, PlayReady), ContentProtectionData, and HLSSignalingData playlist="media|master" (FairPlay). Header X-SuperDRM-Created: 1|0. A drop-in for the EZDRM key generator.

#GET /v1/keys?limit=100 — scope keys:read

Newest first: content_id, kid, scheme, label, systems, wv_provider, key_source, created_at, revoked_at.

#GET /v1/keys/:content_id[?reveal=1] — scope keys:read (reveal needs keys:write)

Metadata plus PSSH; reveal=1 adds the plaintext key.

#POST /v1/keys/:content_id/revoke — scope keys:write

{"revoked": true|false}. Every later licence for the id is refused with revoked.

#Licence tokens

#POST /v1/tokens — scope licence:serve

Mint a player token server-side instead of signing HS256 yourself.

Body fieldNotes
content_idRequired
subOpaque user id, recorded on meter rows
refYour reference (file id, share id), echoed to the webhook and meter
ttl_s30 … 2 592 000 (30 days); default 3600
policyPolicy overrides (below)
systemsRestrict to a subset

Response: token, exp, servers (EME key system → licence URL with ?lt= appended), fairplay_cert_url.

#Token claims (if you sign them yourself)

HS256 JWT over the tenant's token_secret (hex → bytes):

ClaimMeaning
cidContent id, or "*" (only with an auth webhook and ?cid= on the request)
expUnix seconds, required
sub, refAs above
sysAllowed systems, e.g. ["widevine","fairplay"]
polPolicy overrides
iss, iatInformational

#Policy vocabulary

Merged in order: built-in defaults → tenant settings.policy → token pol (or X-SuperDRM-Policy on relays) → auth-webhook policy.

FieldDefaultWidevineFairPlayClear Key
hwtrueRefuse L3 clients; with key_source local/import also asks Google for security_level: 1HDCP requiredRefused entirely when true
hdcptype0none | type0 | type1 to the KSM
persistfalsecan_persistpersistence
rental_srental_duration_secondsrental
playback_splayback_duration_seconds
licence_s3600license_duration_secondslease when renew
renewfalsecan_renewlease
tracksSD_HDallowed_track_types (SD_ONLY | SD_HD | SD_UHD1 | SD_UHD2)
attestedfalseRequires a valid TPM attestation token (X-SuperDRM-Attestation or ?at=) on every licence request — any system, Clear Key included. See Attested devices.samesame

#Attestation

Management (API key): POST/GET /v1/attest/devices, POST /v1/attest/devices/:id/revoke, POST/GET /v1/attest/images. Device-facing (open; the quote is the credential): POST /v1/<tenant>/attest/challenge → nonce, POST /v1/<tenant>/attest/verify → 15-minute attestation token. Full contract, trust model and client script: Attested devices.

#License Service

CORS is open on these routes (Access-Control-Expose-Headers: X-SuperDRM-Security). <tenant> is the tenant slug.

#POST /v1/<tenant>/license/widevine|fairplay|clearkey|playready

Body: the raw CDM challenge (Widevine bytes; FairPlay SPC raw, or spc=<base64> form, or {"spc":…} JSON; Clear Key {"kids":[…]}). Auth, one of:

  • ?lt=<token> — player form.
  • Authorization: Bearer sdrm_… + ?cid=<content_id> + optional X-SuperDRM-Policy: {json} — server relay form.

Response: raw licence bytes (application/octet-stream; Clear Key application/json JWK set), header X-SuperDRM-Security: L1|L3|hw|sw, Cache-Control: no-store. Every attempt writes a meter row.

#GET /v1/<tenant>/fairplay/cert

The tenant's FairPlay application certificate (application/pkix-cert). 404 until installed.

#Licence error codes

HTTPcodeMeaning
400bad_requestMissing cid on a relay, non-JSON policy header, malformed Clear Key request
401unauthorizedNo token and no valid API key
403forbiddenAPI key lacks licence:serve
403token_invalidBad signature, expired, no cid, or wildcard without webhook
403system_not_allowedToken sys excludes this system
403webhook_deniedTenant webhook said no (its reason is the message)
403system_disabledSystem switched off for the tenant
404no_keyUnknown content id (relays fall back to their previous vendor on this)
403revokedKey revoked
409system_not_on_keyKey was issued without this system
403hw_requiredPolicy requires hardware; client is L3, or Clear Key was asked
403kid_mismatchClear Key asked for a kid that is not this content id's
429quota_exceededThe plan's included licences are used up and it has no overage
501system_disabledPlayReady, until licensed
502upstreamGoogle or the KSM errored (message carries their status)
503no_providerNo Widevine credentials match the key's provider
503no_ksmFairPlay KSM not configured

#Authorization webhook

Set auth_webhook on a tenant and SuperDRM POSTs before every token-authenticated licence:

JSON
{ "content_id": "…", "system": "widevine", "sub": "user-42", "ref": "share-9", "ip_hash": "…" }

Reply {"allow": true, "policy": {…}} or {"allow": false, "reason": "share revoked"} within 2.5 s. Allows are cached 20 s per (content, user, system). Header X-SuperDRM-Tenant carries the slug.

#Usage and self-service

#GET /v1/usage?from=YYYY-MM-DD&to=YYYY-MM-DD — scope usage:read

{ from, to, totals: { widevine: { ok, failed }, … }, keys_issued, daily: [{ day, system, ok, n }] }. Default: last 30 days.

#GET /v1/usage/licences?limit=100 — scope usage:read

Recent meter rows: id, content_id, system, kind (license|renewal|service_cert), security, device_hash, user_ref, ref, bytes_out, ms, ok, error, issued_at.

#GET /v1/me — scope usage:read

Tenant profile, enabled systems, DRM switches (public halves), API keys (prefixes), your scopes.

#POST /v1/me/rotate-token-secret — scope admin

New token_secret (hex), shown once. Old tokens stop verifying immediately.

#Portal

Everything the self-service portal does is plain JSON under /v1/portal/* with the session cookie sdsess; every write needs the X-SuperDRM-Portal: 1 header (that plus SameSite=Lax is the CSRF story). Public: signup, verify-email, login, logout, reset/request, reset/perform, invite/accept. Session: me, keys, usage, events, domains (+/verify), team (+invite/remove), plan, rotate-token-secret, sessions, accounts. See Portal & console.

#Admin (operator token)

MethodPathBody / queryReturns
GET/v1/admin/tenantsTenants with keys and licences_30d counts
POST/v1/admin/tenantsslug, name, plan?, auth_webhook?, settings?201 tenant + one-time token_secret and api_key
PATCH/v1/admin/tenants/:slugname?, plan?, status?, auth_webhook? ("" clears), settings?{ok}
POST/v1/admin/tenants/:slug/keysname?, scopes?, env? (live|test)201 new API key (shown once)
POST/v1/admin/tenants/:slug/keys/:id/revoke{revoked}
POST/v1/admin/tenants/:slug/rotate-token-secret{token_secret}
POST/v1/admin/tenants/:slug/drm/:systemenabled?, mode? (shared|own), public?, config? (sealed){ok}
POST/v1/admin/tenants/:slug/membersemail, role (owner|admin|member){ok} — the user must already exist
GET/v1/admin/partnersPartner credential rows (public halves)
POST/v1/admin/partners/:idpublic?, config (widevine:prod config: provider, signing_key, signing_iv, host){ok}
GET/v1/admin/plansPlan catalogue
GET/v1/admin/usage?tenant=&from=&to=As /v1/usage, all tenants when tenant is omitted
GET/v1/admin/licences?limit=100Meter rows across tenants
GET/v1/admin/audit?limit=200Audit log
GET/v1/admin/eventsServer-sent events: every licence, key and admin action, live
GET/v1/admin/statsCounters for dashboards
POST/v1/admin/users/:email/verifyManually verify a portal user (support)
POST/v1/admin/users/:email/master{"on": true|false}Grant or revoke the master-admin flag
POST/v1/admin/importbundleImport an export bundle (see Operations & migration)

#Example: relay from an edge function

relay.ts
const r = await fetch(`${SUPERDRM_URL}/v1/${SUPERDRM_TENANT}/license/${system}?cid=${contentId}`, {
  method: "POST",
  headers: { Authorization: `Bearer ${SUPERDRM_KEY}`, "Content-Type": "application/octet-stream",
             "X-SuperDRM-Policy": JSON.stringify({ hw: strict }) },
  body: challenge,
});
if (r.status === 404) return relayToPreviousVendor(system, challenge);   // no_key: packaged before the switch
if (!r.ok) throw new Error(`superdrm ${r.status}: ${await r.text()}`);
return new Uint8Array(await r.arrayBuffer());
Shell
curl -s -X POST "https://api.superdrm.com/v1/<tenant>/license/widevine?cid=<content_id>" \
  -H "Authorization: Bearer sdrm_live_…" -H 'X-SuperDRM-Policy: {"hw":true}' \
  --data-binary @challenge.bin -o licence.bin -w '%{http_code}\n'

Updated September 2026