API Documentation
1. Quick start
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).
Two DRM systems are supported:
- Widevine — endpoint
POST /api - PlayReady — endpoint
POST /pr/api
Both follow the same 3-step flow: GetChallenge → send to license server → GetKeys.
2. Ready-to-run Python client
Save this file as cdmpool.py, replace YOUR_API_TOKEN, then run:
python cdmpool.py wv --pssh <PSSH> --license <LICENSE_URL>
python cdmpool.py pr --pssh <PSSH> --license <LICENSE_URL>
# with license headers (JSON string):
python cdmpool.py wv --pssh <PSSH> --license <URL> \
--headers '{"X-AxDRM-Message":"eyJ…"}'
#!/usr/bin/env python3
"""cdmpool.py — minimal client for the CDMPOOL API."""
import argparse, base64, json, sys, requests
CDMPOOL_URL = "https://cdmpool.xyz"
API_TOKEN = "YOUR_API_TOKEN" # <-- put your token here
def extract_widevine(pssh, license_url, headers=None):
"""Widevine 3-step flow — returns list of {kid,key} dicts."""
headers = headers or {}
# 1) get challenge
r = requests.post(f"{CDMPOOL_URL}/api", timeout=15, json={
"method": "GetChallenge",
"params": {"init": pssh, "cert": "", "raw": False,
"licensetype": "STREAMING", "device": "chromecdm"},
"token": API_TOKEN,
})
r.raise_for_status()
ch = r.json()
challenge_b64, sid = ch["challenge"], ch["session_id"]
# 2) forward the (binary) challenge to the upstream license server
lic = requests.post(license_url,
data=base64.b64decode(challenge_b64),
headers=headers, timeout=20)
lic.raise_for_status()
# 3) decrypt license -> keys
r = requests.post(f"{CDMPOOL_URL}/api", timeout=15, json={
"method": "GetKeys",
"params": {"cdmkeyresponse": base64.b64encode(lic.content).decode(),
"session_id": sid},
"token": API_TOKEN,
})
r.raise_for_status()
return r.json()["keys"]
def extract_playready(pssh, license_url, headers=None):
"""PlayReady 3-step flow (XML challenge / XML response)."""
headers = {"Content-Type": "text/xml; charset=utf-8", **(headers or {})}
r = requests.post(f"{CDMPOOL_URL}/pr/api", timeout=15, json={
"method": "GetChallenge",
"params": {"init": pssh},
"token": API_TOKEN,
})
r.raise_for_status()
ch = r.json()
challenge_xml, sid = ch["challenge"], ch["session_id"]
lic = requests.post(license_url,
data=challenge_xml.encode("utf-8"),
headers=headers, timeout=20)
lic.raise_for_status()
r = requests.post(f"{CDMPOOL_URL}/pr/api", timeout=15, json={
"method": "GetKeys",
"params": {"session_id": sid, "license_response": lic.text},
"token": API_TOKEN,
})
r.raise_for_status()
return r.json()["keys"]
def main():
ap = argparse.ArgumentParser(description="CDMPOOL DRM key extractor")
ap.add_argument("drm", choices=["wv", "pr"], help="wv=Widevine, pr=PlayReady")
ap.add_argument("--pssh", required=True, help="Base64 PSSH from the MPD")
ap.add_argument("--license", required=True, help="License server URL")
ap.add_argument("--headers", default="{}", help="JSON dict of license headers")
args = ap.parse_args()
headers = json.loads(args.headers) if args.headers else {}
fn = extract_widevine if args.drm == "wv" else extract_playready
try:
keys = fn(args.pssh, args.license, headers)
except requests.HTTPError as e:
print("HTTP error:", e, e.response.text[:500], file=sys.stderr)
sys.exit(1)
if not keys:
print("No keys returned.", file=sys.stderr)
sys.exit(2)
print(f"# {len(keys)} key(s) extracted")
for k in keys:
print(f"--key {k['kid']}:{k['key']}")
if __name__ == "__main__":
main()
Real example — Axinom Widevine test
python cdmpool.py wv \
--pssh 'AAAAW3Bzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAADsIARIQ...' \
--license 'https://drm-widevine-licensing.axtest.net/AcquireLicense' \
--headers '{"X-AxDRM-Message":"eyJhbGciOiJIUzI1NiIsIn..."}'
3. Widevine — POST /api (raw HTTP)
3.1 cURL — end-to-end
# 1) Get challenge
CHALLENGE=$(curl -s -X POST https://cdmpool.xyz/api \
-H 'Content-Type: application/json' \
-d '{
"method": "GetChallenge",
"params": {
"init": "<PSSH base64>",
"cert": "", "raw": false,
"licensetype": "STREAMING", "device": "chromecdm"
},
"token": "<YOUR_API_TOKEN>"
}')
CH_B64=$(echo "$CHALLENGE" | jq -r .challenge)
SID=$(echo "$CHALLENGE" | jq -r .session_id)
# 2) Send binary challenge to license server
echo "$CH_B64" | base64 -d > /tmp/chal.bin
curl -s -X POST '<LICENSE_URL>' \
--data-binary @/tmp/chal.bin \
-H 'X-AxDRM-Message: <JWT if required>' \
-o /tmp/lic.bin
# 3) Get keys
curl -s -X POST https://cdmpool.xyz/api \
-H 'Content-Type: application/json' \
-d "{
\"method\": \"GetKeys\",
\"params\": {
\"cdmkeyresponse\": \"$(base64 -w0 /tmp/lic.bin)\",
\"session_id\": \"$SID\"
},
\"token\": \"<YOUR_API_TOKEN>\"
}" | jq
3.2 Python — end-to-end
import base64, requests
CDMPOOL = "https://cdmpool.xyz"
TOKEN = "<YOUR_API_TOKEN>"
PSSH = "<base64 PSSH from MPD>"
LIC_URL = "<license server URL>"
HEADERS = {} # e.g. {"X-AxDRM-Message": "…"}
r = requests.post(f"{CDMPOOL}/api", json={
"method": "GetChallenge",
"params": {"init": PSSH, "cert": "", "raw": False,
"licensetype": "STREAMING", "device": "chromecdm"},
"token": TOKEN,
}).json()
challenge, sid = r["challenge"], r["session_id"]
lic = requests.post(LIC_URL,
data=base64.b64decode(challenge),
headers=HEADERS)
lic.raise_for_status()
keys = requests.post(f"{CDMPOOL}/api", json={
"method": "GetKeys",
"params": {"cdmkeyresponse": base64.b64encode(lic.content).decode(),
"session_id": sid},
"token": TOKEN,
}).json()["keys"]
for k in keys:
print(f"--key {k['kid']}:{k['key']}")
3.3 Node.js — end-to-end
const CDMPOOL = "https://cdmpool.xyz";
const TOKEN = "<YOUR_API_TOKEN>";
const PSSH = "<base64 PSSH>";
const LIC_URL = "<license URL>";
async function extract() {
const ch = await fetch(`${CDMPOOL}/api`, {
method: "POST",
headers: {"Content-Type":"application/json"},
body: JSON.stringify({
method:"GetChallenge",
params:{init:PSSH, cert:"", raw:false, licensetype:"STREAMING", device:"chromecdm"},
token:TOKEN,
})
}).then(r => r.json());
const chalBin = Buffer.from(ch.challenge, "base64");
const licResp = await fetch(LIC_URL, {method:"POST", body: chalBin});
const licBuf = Buffer.from(await licResp.arrayBuffer());
const out = await fetch(`${CDMPOOL}/api`, {
method:"POST",
headers:{"Content-Type":"application/json"},
body: JSON.stringify({
method:"GetKeys",
params:{cdmkeyresponse: licBuf.toString("base64"), session_id: ch.session_id},
token:TOKEN,
})
}).then(r => r.json());
console.log(out.keys);
}
extract();
4. PlayReady — POST /pr/api (raw HTTP)
PlayReady challenges are UTF-8 XML (not binary). The license response is XML too.
4.1 Generic example (Brightcove, Azure, etc.)
import base64, requests
CDMPOOL = "https://cdmpool.xyz"
TOKEN = "<YOUR_API_TOKEN>"
PSSH = "<base64 PSSH from MPD cenc:pssh>"
r = requests.post(f"{CDMPOOL}/pr/api", json={
"method": "GetChallenge",
"params": {"init": PSSH},
"token": TOKEN,
}).json()
challenge_xml = r["challenge"]
sid = r["session_id"]
lic = requests.post(LICENSE_URL,
data=challenge_xml.encode("utf-8"),
headers={"Content-Type":"text/xml; charset=utf-8"})
license_xml = lic.text
keys = requests.post(f"{CDMPOOL}/pr/api", json={
"method": "GetKeys",
"params": {"session_id": sid, "license_response": license_xml},
"token": TOKEN,
}).json()["keys"]
5. Chrome Extension — POST /extension
Compatible with the Chrome CDM Decryptor browser extension.
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>"
}
6. Recipes
6.1 Brightcove (Télé-Québec, TSN…)
import base64, re, requests
CDMPOOL, TOKEN = "https://cdmpool.xyz", "<TOKEN>"
PAGE = "https://telequebec.tv/regarder/en-direct/jeunesse"
UA = "Mozilla/5.0"
r = requests.get(PAGE, headers={"User-Agent": UA}).text
acc = re.search(r'brightcoveAccountId\\?"[,:\\"]*([0-9]+)', r).group(1)
pl = re.search(r'brightcovePlayerId\\?"[,:\\"]*([A-Za-z0-9]+)', r).group(1)
med = re.search(r'brightcoveMediaId\\?"[,:\\"]*([0-9]+)', r).group(1)
js = requests.get(f"https://players.brightcove.net/{acc}/{pl}_default/index.min.js").text
pol = re.search(r'BCpkAD[A-Za-z0-9._-]{40,}', js).group(0)
pb = requests.get(f"https://edge.api.brightcove.com/playback/v1/accounts/{acc}/videos/{med}",
headers={"Accept": f"application/json;pk={pol}"}).json()
for s in pb["sources"]:
ks = s.get("key_systems", {})
if "com.widevine.alpha" in ks and s["src"].endswith(".mpd"):
mpd_url = s["src"]
wv_url = ks["com.widevine.alpha"]["license_url"]
break
mpd = requests.get(mpd_url).text
pssh = re.search(r'urn:uuid:edef8ba9[^"]*"[^>]*>\s*<cenc:pssh[^>]*>([^<]+)', mpd).group(1)
c = requests.post(f"{CDMPOOL}/api", json={"method":"GetChallenge",
"params":{"init":pssh,"cert":"","raw":False,"licensetype":"STREAMING","device":"chromecdm"},
"token":TOKEN}).json()
lic = requests.post(wv_url, data=base64.b64decode(c["challenge"]))
out = requests.post(f"{CDMPOOL}/api", json={"method":"GetKeys",
"params":{"cdmkeyresponse":base64.b64encode(lic.content).decode(),
"session_id":c["session_id"]},
"token":TOKEN}).json()
print(out["keys"])
6.2 Recipe: DStv Stream (or any subscription service with a token)
For services that gate the license request behind a subscription JWT (DStv, Showmax, Canal+, Disney+, Prime EU, DAZN, RTL+, TF1+, etc.), the pattern is: login → get JWT → fetch MPD → extract PSSH → call CDMPOOL with the JWT in the license headers. The JWT is passed to the upstream license server by your own client — it is never sent to CDMPOOL.
#!/usr/bin/env python3
"""dstv_extract.py — DStv Stream key extraction via CDMPOOL."""
import base64, re, requests
CDMPOOL, TOKEN = "https://cdmpool.xyz", "<YOUR_API_TOKEN>"
DSTV_USER = "your@email.com"
DSTV_PWD = "your-dstv-password"
CHANNEL_ID = "MzcxOTM" # e.g. SuperSport channel id from the app
UA = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0 Safari/537.36")
# ---------- 1) Login to DStv to obtain the subscriber JWT ----------
auth = requests.post(
"https://login.dstv.com/api/auth/login",
headers={"User-Agent": UA, "Content-Type": "application/json"},
json={"email": DSTV_USER, "password": DSTV_PWD, "device": "web"},
).json()
JWT = auth["token"] # or auth["access_token"] depending on version
print("logged in, jwt:", JWT[:40] + "…")
# ---------- 2) Fetch the playback manifest with the JWT ----------
pb = requests.get(
f"https://api.dstv.com/live-play/{CHANNEL_ID}",
headers={"User-Agent": UA, "Authorization": f"Bearer {JWT}"},
).json()
mpd_url = pb["dash"]["manifestUrl"] # varies by tenant
wv_url = pb["dash"]["widevineLicenseUrl"] # usually https://lic.widevine.dstv.com/...
print("mpd:", mpd_url[:80], "…")
print("wv :", wv_url)
# ---------- 3) Extract PSSH from the MPD ----------
mpd = requests.get(mpd_url, headers={"User-Agent": UA,
"Authorization": f"Bearer {JWT}"}).text
pssh = re.search(
r'urn:uuid:edef8ba9[^"]*"[^>]*>\s*<cenc:pssh[^>]*>([^<]+)',
mpd).group(1)
# ---------- 4) CDMPOOL GetChallenge ----------
c = requests.post(f"{CDMPOOL}/api", json={
"method": "GetChallenge",
"params": {"init": pssh, "cert": "", "raw": False,
"licensetype": "STREAMING", "device": "chromecdm"},
"token": TOKEN,
}).json()
# ---------- 5) Send the challenge to DStv (WITH the JWT) ----------
lic = requests.post(
wv_url,
data=base64.b64decode(c["challenge"]),
headers={
"User-Agent": UA,
"Authorization": f"Bearer {JWT}",
"Content-Type": "application/octet-stream",
},
timeout=20,
)
lic.raise_for_status()
# ---------- 6) CDMPOOL GetKeys ----------
out = requests.post(f"{CDMPOOL}/api", json={
"method": "GetKeys",
"params": {"cdmkeyresponse": base64.b64encode(lic.content).decode(),
"session_id": c["session_id"]},
"token": TOKEN,
}).json()
for k in out["keys"]:
print(f"--key {k['kid']}:{k['key']}")
Notes — the exact endpoints (login URL, playback URL, JSON keys) change with each service. Inspect the network tab of the web player to find them. The 6 numbered steps above stay identical for every service:
- Showmax — login at
userapi.showmax.com/v78/session, playback atapi.showmax.com - Canal+ — login at
secure-gen-hapi.canal-plus.com, DRM token inPASS-Tokenheader - Disney+ — login via GraphQL
global.edge.bamgrid.com, token inAuthorization: BEARER … - DAZN — login at
api.dazn.com/misl/v5/SignIn, JWT inAuthorization - Prime EU — cookies +
X-AmazonAccount-Token, license atatv-ps-eu.primevideo.com/cdp/catalog/GetPlaybackResources
7. Error responses
| Endpoint | Status | Meaning |
|---|---|---|
| /api, /pr/api | 403 | Missing or invalid token |
| /api, /pr/api | 429 | Daily quota reached (5/day on Free plan) |
| /api | 400 | Malformed JSON, missing init, invalid PSSH |
| /pr/api GetKeys | 400 | Unknown session_id or license parse failure |