API Documentation

Quick start Python client Widevine (raw) PlayReady (raw) Chrome Extension Recipes: Brightcove & DStv Errors

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:

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:

7. Error responses

EndpointStatusMeaning
/api, /pr/api403Missing or invalid token
/api, /pr/api429Daily quota reached (5/day on Free plan)
/api400Malformed JSON, missing init, invalid PSSH
/pr/api GetKeys400Unknown session_id or license parse failure