API Documentation
0. Which endpoint should I use?
CDMPOOL exposes two different flows for extracting DRM keys. Choosing the wrong one is the #1 source of failed extractions. Read this before doing anything else.
π POST /api/mpd-analyze β MPD sanity-check
Send us only a DASH manifest URL. We fetch it, parse it and return a suggestion bundle: detected DRM, Widevine + PlayReady PSSH, KID, resolutions, best-guess license URL and provider (EZDRM, Axinom, Pallycon, Kinescopeβ¦).
Free & rate-limited only β no credit consumed. Ideal to run
before /api/extract so you don't waste a call on a
typo. The Extract UI already wires this into a
non-intrusive "π Analyze" button.
π’ POST /api/extract β one-shot
CDMPOOL performs the HTTP call to the license server itself. You send us the PSSH + license URL + headers, we do everything, we return the keys.
Good for: everything β including Amazon Prime Video (with cookies), Bitmovin/Shaka test assets, Axinom DRMtoday, EZDRM, IPTV feeds, Brightcove-brokered channels, DStv-style Bearer-authed streams. CDMPOOL auto-handles TLS impersonation (chrome120) and JSON-envelope unwrapping for Amazon Prime.
Fails only when the session cookies you pass are expired or don't match the target region β never because of the caller IP. CDMPOOL proved end-to-end extraction from our Netherlands VPS on Amazon Prime FR/CA once the correct cookies are supplied.
π£ POST /api / POST /pr/api β advanced 3-step (legacy)
Same crypto, but you POST the challenge to the license server
yourself. Kept only for compatibility with existing browser
extensions and the cdrm-project.com API. For 99% of
services (including Amazon Prime, Netflix, Disney+, DStvβ¦) the
one-shot flow above is now the right answer.
POST /api/extract. Provide
cookies when the service requires auth (Amazon Prime,
Disney+, Netflix, DStvβ¦). CDMPOOL handles the TLS impersonation,
JSON-envelope unwrapping and key derivation for you.
0. Manifest analyzer β POST /api/mpd-analyze
/api/extract call.
Powers the "π Analyze" button on the Extract UI β but you can also hit it directly from your own tooling. The Extract UI displays each returned field as a chip that the user can click to fill the corresponding form input; existing values are never overwritten silently.
curl -X POST https://cdmpool.xyz/api/mpd-analyze \
-H "Content-Type: application/json" \
-d '{"mpd_url":"https://storage.googleapis.com/shaka-demo-assets/angel-one-widevine/dash.mpd"}'
Successful response (ok:true):
{
"ok": true,
"manifest_type": "dash", // "dash" | "hls" | "unknown"
"drm": "widevine", // best guess (widevine has priority)
"pssh": "AAAAPnBz...", // convenience default (widevine if any)
"pssh_widevine": "AAAAPnBz...",
"pssh_playready":null,
"kid": "800aacaa-5229-58ae-8880-62b5695db6bf",
"license_url": null, // filled when the PR PSSH carries a LA_URL
// OR when the provider is recognised
"drm_provider": null, // ezdrm|axinom|pallycon|kinescope|amazon|β¦
"resolutions": ["192x144","320x240","480x360","640x480","768x576"]
}
Failure response (ok:false) β never raises, always JSON:
{
"ok": false,
"fetch_status": 403,
"hint": "The CDN refused our request (HTTP 403). This usually
means the MPD is behind auth (signed cookies, session,
IP-lock). Try our platform-specific kits from /downloads."
}
What we can auto-detect
- β Public MPDs (Bitmovin, Shaka, Axinom test vectors, EZDRM, Kinescope, DRMToday public) β all fields filled
- β
PlayReady MPDs β we decode the WRMHEADER XML embedded in
the PSSH box and extract the exact
<LA_URL> - β οΈ Auth-protected MPDs (DAZN, Molotov, Axinom production) β
we can still fetch the PSSH if the URL is anonymously reachable,
but you'll need to fill
license_url+ auth headers yourself
mpd_url). Repeat calls within the window return
the cached result to save our bandwidth and resources.
1. One-shot API β POST /api/extract
CDMPOOL does GetChallenge, calls the license server,
unwraps JSON envelopes when needed, and returns the derived keys β all
in a single round-trip.
curl -X POST https://cdmpool.xyz/api/extract \
-H "Content-Type: application/json" \
-d '{
"token": "<your_api_token>",
"drm": "widevine",
"pssh": "AAAA... (base64 from <cenc:pssh>)",
"license_url": "https://example.com/license",
"headers": {"Authorization":"Bearer β¦", "X-Custom":"β¦"},
"cookies": {"session":"β¦"},
"channel_name":"Optional label",
"mpd_url": "https://.../manifest.mpd"
}'
Success (HTTP 200):
{
"ok": true,
"keys": [{"kid":"β¦","key":"β¦"}, ...],
"ms": 340,
"session_id": "β¦"
}
On success, CDMPOOL also silently stores the extraction under
/me/extractions together with an xaccel_url
(mpd_url + ?decryption_key=KID:KEY) that plays directly in
VLC / IPTV Smarters. That ready-to-play URL is not returned in
the API body β read it from the extractions library.
Failure (HTTP 4xx/5xx):
{
"ok": false,
"step": "license_request",
"status": 403,
"error_code": "E_LICENSE_FORBIDDEN",
"hint": "License server returned 403 Forbidden. Likely missing auth
header, missing Origin/Referer, or expired session."
}
Fields headers, cookies, channel_name,
mpd_url, source_url, country, note
are optional. Only token, drm, pssh and
license_url are required.
NO_PLAYBACK_STREAMS_AVAILABLE_OVER_SECURE_TRANSPORT,
Netflix device_not_supported), CDMPOOL automatically retries
the request in PlayReady using the PSSH from the same MPD (if
mpd_url was provided). On success the response includes:
"auto_retry": {
"attempted": true,
"from_drm": "widevine",
"to_drm": "playready",
"ok": true,
"pssh_source": "mpd"
}
Auth/session/geo/timeout failures do not trigger the retry β they
fail identically in PlayReady, so we don't add latency. For maximum
auto-recovery, always pass "mpd_url" in your requests.
cURL β parse keys with jq
curl -s -X POST https://cdmpool.xyz/api/extract \
-H 'Content-Type: application/json' \
-d '{
"token": "YOUR_TOKEN",
"drm": "widevine",
"pssh": "AAAAW3Bzc2g...",
"license_url": "https://cwip-shaka-proxy.appspot.com/no_auth"
}' | jq -r '.keys[] | "--key \(.kid):\(.key)"'
# β --key 62e6b3fd5b6c4c9c9f22a1e5f9a7d820:8f2b1e...
Python example
import requests
r = requests.post("https://cdmpool.xyz/api/extract", json={
"token": "YOUR_TOKEN",
"drm": "widevine",
"pssh": "AAAAW3Bzc2g...",
"license_url": "https://cwip-shaka-proxy.appspot.com/no_auth",
})
d = r.json()
if d["ok"]:
for k in d["keys"]:
print(f"--key {k['kid']}:{k['key']}")
else:
print(f"[{d['error_code']}] {d['hint']}")
Node.js example
const r = await fetch("https://cdmpool.xyz/api/extract", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({
token: "YOUR_TOKEN",
drm: "widevine",
pssh: "AAAAW3Bzc2g...",
license_url: "https://cwip-shaka-proxy.appspot.com/no_auth",
headers: {"X-AxDRM-Message": "eyJ..."} // optional per-service auth
})
}).then(r => r.json());
if (r.ok) {
for (const k of r.keys) console.log(`--key ${k.kid}:${k.key}`);
} else {
console.error(`[${r.error_code}] ${r.hint}`);
}
2. Quick start & token info
Every request needs your personal API token. Find it in My account after signing in. Each successful extraction consumes 1 unit from your daily quota (5/day on Free, unlimited on VIP).
Pass the token in the JSON body as "token" for
/api/extract, or as the api-key header for
/extension. Select the DRM system per call with
"drm": "widevine" or "drm": "playready".
3. Amazon Prime Video β full pipeline example
Good news! As of Feb 2026 CDMPOOL supports Amazon Prime end-to-end
via a single POST /api/extract call β we auto-detect
atv-ps.primevideo.com / atv-ps.amazon.com URLs
and:
- use
curl_cffiwith achrome120TLS fingerprint (Amazon rejects plainpython-requests); - form-encode the challenge as
playReadyChallenge(orlicenseChallengefor Widevine session-handoff); - unwrap the JSON envelope
(
playReadyLicense.encodedLicenseResponse) automatically.
Important: Amazon rotates the KID on every MPD fetch. You must
capture the PSSH from a fresh manifest and submit within the same session,
otherwise you will get PRS.InvalidRequest.
Recommended flow β PlayReady, one shot (works from any IP):
import requests
from curl_cffi import requests as cffi # pip install curl_cffi
ASIN = "amzn1.dv.gti.xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
MARKETPLACE = "A3K6Y4MI8GDYMT" # EU. NA=ATVPDKIKX0DER
DEVICE_ID = "fdd8c346-7883-430a-9fc3-dcdf4bf4df74"
COOKIES = { # export from your browser (Netscape or dict)
"at-main-av": "Atza|...", # auth token
"ubid-main-av":"131-...", # session id
"session-id": "143-...",
"session-token":"...",
}
# 1) Ask Amazon for the current playback URL & manifest
qs = {
"deviceID": DEVICE_ID, "deviceTypeID": "AOAGZA014O5RE",
"firmware": "1", "gascEnabled": "false", "marketplaceID": MARKETPLACE,
"playerType": "xp", "operatingSystemName": "Windows",
"operatingSystemVersion": "10.0", "deviceApplicationName": "EdgeNext",
"asin": ASIN, "consumptionType": "Streaming",
"desiredResources": "PlaybackUrls", "resourceUsage": "CacheResources",
"videoMaterialType": "Feature", "displayWidth": "2560", "displayHeight": "1440",
"deviceStreamingTechnologyOverride": "DASH", "deviceDrmOverride": "CENC",
}
base = "https://atv-ps.primevideo.com/cdp/catalog/GetPlaybackResources"
r = cffi.get(base, params=qs, cookies=COOKIES, impersonate="chrome120")
url_set = r.json()["playbackUrls"]["urlSets"]
mpd_url = url_set[next(iter(url_set))]["urls"]["manifest"]["url"]
# 2) Fetch MPD, extract the current PlayReady PSSH
import re
mpd = cffi.get(mpd_url, cookies=COOKIES, impersonate="chrome120").text
pssh = re.search(
r'<ContentProtection[^>]*schemeIdUri="urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95"[^>]*>'
r'.*?<cenc:pssh[^>]*>([^<]+)</cenc:pssh>',
mpd, re.DOTALL | re.IGNORECASE).group(1).strip()
# 3) Send everything to CDMPOOL β one call, one response with the keys
qs_lic = {**qs, "desiredResources": "PlayReadyLicense",
"resourceUsage": "ImmediateConsumption"}
license_url = base + "?" + "&".join(f"{k}={v}" for k, v in qs_lic.items())
result = requests.post("https://cdmpool.xyz/api/extract", json={
"token": YOUR_CDMPOOL_TOKEN,
"drm": "playready",
"pssh": pssh,
"license_url": license_url,
"headers": {}, # nothing extra required
"cookies": COOKIES, # <-- CDMPOOL passes them to Amazon for you
"channel_name": "Prime Video CA β my extraction",
}).json()
print(result["keys"]) # [{"kid": "...", "key": "..."}]
The full runnable version (with cookie loader, ASIN discovery and offline Widevine fallback) is in the Amazon Prime Video kit. Widevine still requires an L1 CDM which CDMPOOL does not currently host β prefer PlayReady for Amazon Prime.
4. Chrome Extension β POST /extension
POST /api/extract with cookies β same result, no browser
side glue.
Endpoint reference:
POST https://cdmpool.xyz/extension
Header: api-key: <YOUR_API_TOKEN>
Content-Type: application/json
Body:
{
"init_data": "<PSSH base64>",
"license_request": "<optional>",
"license_response": "<base64 license response>"
}
Response:
{ "message": "success", "keys": "--key KID:KEY\n--key KID:KEY" }
Extension setup β edit license.json inside the extension folder:
{
"api_url": "https://cdmpool.xyz/extension",
"api_key": "<YOUR_API_TOKEN>"
}
5. Recipe: Brightcove-brokered streams (TΓ©lΓ©-QuΓ©bec, TSN, MLBβ¦)
Brightcove exposes a public policy key that lets the license request
succeed without cookies. Perfect for a one-shot /api/extract
call:
import re, requests
CDMPOOL, TOKEN = "https://cdmpool.xyz", "<YOUR_API_TOKEN>"
PAGE = "https://telequebec.tv/regarder/en-direct/jeunesse"
# 1) Discover Brightcove account/player/media IDs from the public page
html = requests.get(PAGE).text
acc = re.search(r'brightcoveAccountId\\?"[,:\\"]*([0-9]+)', html).group(1)
pid = re.search(r'brightcovePlayerId\\?"[,:\\"]*([A-Za-z0-9]+)', html).group(1)
med = re.search(r'brightcoveMediaId\\?"[,:\\"]*([0-9]+)', html).group(1)
# 2) Grab the policy key baked in the Brightcove player JS
js = requests.get(f"https://players.brightcove.net/{acc}/{pid}_default/index.min.js").text
pk = re.search(r'BCpkAD[A-Za-z0-9._-]{40,}', js).group(0)
# 3) Ask Brightcove for the playback info (returns MPD + license URL)
pb = requests.get(
f"https://edge.api.brightcove.com/playback/v1/accounts/{acc}/videos/{med}",
headers={"Accept": f"application/json;pk={pk}"},
).json()
src = next(s for s in pb["sources"] if s["src"].endswith(".mpd")
and "com.widevine.alpha" in s.get("key_systems", {}))
mpd_url = src["src"]
lic_url = src["key_systems"]["com.widevine.alpha"]["license_url"]
# 4) Extract the Widevine PSSH from the MPD
mpd = requests.get(mpd_url).text
pssh = re.search(
r'urn:uuid:edef8ba9[^"]*"[^>]*>\s*<cenc:pssh[^>]*>([^<]+)',
mpd).group(1)
# 5) One-shot extraction via CDMPOOL β done
out = requests.post(f"{CDMPOOL}/api/extract", json={
"token": TOKEN, "drm": "widevine",
"pssh": pssh, "license_url": lic_url,
"mpd_url": mpd_url,
"channel_name": "TΓ©lΓ©-QuΓ©bec β Jeunesse",
}).json()
for k in out["keys"]:
print(f"--key {k['kid']}:{k['key']}")
# Ready-to-play xaccel URL is available in /me/extractions after each success.
Adapting to other services β replace the "discovery" step
(1-3) with the one specific to the service (e.g. Amazon Prime uses
atv-ps.primevideo.com/cdp/catalog/GetPlaybackResources).
The /api/extract call in step 5 is always the same
regardless of the target service β CDMPOOL detects the license server
format and handles the crypto for you.
6. Error responses & error codes
HTTP status returned by /api/extract, /api and /pr/api
| Status | Meaning |
|---|---|
| 200 | Success β response contains keys (or challenge+session_id on the low-level endpoints) |
| 400 | Bad PSSH / missing license_url β see error_code |
| 403 | Missing or invalid member API token |
| 429 | Daily quota reached (5/day on Free β upgrade to VIP for unlimited) |
| 495 | SSL/TLS handshake with the license server failed |
| 502 | License server unreachable (DNS / connection refused) |
| 504 | License request timed out (30 s limit) |
Every failing response includes a stable error_code + a human hint
Sample failing response:
{
"ok": false,
"step": "challenge",
"status": 400,
"error_code": "E_PSSH_NOT_BASE64",
"hint": "PSSH is not valid base64. Copy the exact base64 string inside
<cenc:pssh>β¦</cenc:pssh> in the MPD (Widevine block,
schemeIdUri urn:uuid:edef8ba9-β¦-21ed). No line breaks, no XML tags."
}
All error codes (branch on error_code, never on the hint text)
| Code | Step | What went wrong & how to fix |
|---|---|---|
E_INVALID_TOKEN | auth | Missing or wrong token field. |
E_QUOTA_REACHED | auth | Daily quota hit (Free = 5/day). Upgrade to VIP. |
E_BAD_DRM | input | drm must be widevine or playready. |
E_LICENSE_URL_MISSING | input | You forgot the license_url field. |
E_BAD_HEADERS | input | headers must be a JSON object (dict), not a string. |
E_BAD_COOKIES | input | cookies must be a JSON object {name: value}. |
E_PSSH_MISSING | challenge | Field pssh is empty β pass the base64 PSSH from the MPD. |
E_PSSH_NOT_BASE64 | challenge | PSSH is not valid base64 β no line breaks, no XML tags, no URL. |
E_PLAYREADY_BAD_INIT | challenge | PlayReady init is empty or not a base64-encoded WRMHEADER XML. |
E_CHALLENGE_BAD_REQUEST | challenge | CDM rejected the request β check PSSH matches the DRM you picked. |
E_APIMAIN_AUTH | challenge | Internal APIMAIN token issue β contact support. |
E_CHALLENGE_UNKNOWN | challenge | Unclassified CDM error β inspect the body/description. |
E_LICENSE_UNAUTHORIZED | license_request | License 401 β add Authorization / Cookie in headers JSON. |
E_LICENSE_FORBIDDEN | license_request | Generic 403 β likely missing auth (Authorization / Cookie / Origin) or expired session headers. |
E_LICENSE_TOKEN_EXPIRED | license_request | Session token expired (5-15 min TTL) β capture fresh headers. |
E_LICENSE_NOT_ENTITLED | license_request | Not a paying subscriber for this content. |
E_LICENSE_MISSING_ORIGIN | license_request | Missing Origin / Referer / User-Agent β add them in headers JSON. |
E_LICENSE_NOT_FOUND | license_request | License URL 404 β the endpoint path is wrong. |
E_LICENSE_BAD_CHALLENGE | license_request | License 400 saying the challenge is malformed β mismatched PSSH or missing service-specific header. |
E_LICENSE_BAD_REQUEST | license_request | Other 400 β see body. |
E_LICENSE_RATE_LIMITED | license_request | License 429 β wait 1-5 minutes before retrying. |
E_LICENSE_REDIRECT | license_request | 301/302/307/308 β use the FINAL URL after redirect. |
E_LICENSE_UPSTREAM_DOWN | license_request | License 5xx β retry later. |
E_LICENSE_UNKNOWN | license_request | Other license failures. |
E_LICENSE_UNREACHABLE | connection_error | DNS failure / connection refused / RemoteDisconnected. |
E_LICENSE_SSL | ssl_error | Cert verification failed β bad cert on the license server. |
E_LICENSE_TIMEOUT | timeout | > 30 s β license server too slow or unreachable. |
E_KEYS_DERIVATION | get_keys | CDM couldn't derive keys β retry with a fresh session. |
E_INTERNAL | exception | Unexpected server error β details in the body. |
Programmatic branching (Python)
r = requests.post("https://cdmpool.xyz/api/extract", json=payload).json()
code = r.get("error_code", "")
if code == "E_LICENSE_UNAUTHORIZED":
payload["headers"]["Authorization"] = "Bearer " + fresh_token()
elif code in ("E_PSSH_MISSING", "E_PSSH_NOT_BASE64"):
print("Fix your PSSH:", r["hint"])
elif code == "E_LICENSE_RATE_LIMITED":
time.sleep(60)