{
  "info": {
    "name": "KYC Age Verification v0.2 - CIBA",
    "description": "KYC Age Verification v0.2 (SEP/CAMARA), CIBA grant variant.\n\nClient authentication: PRIVATE_KEY_JWT only (RS256-signed client_assertion) - never client_secret.\n\nbase_url defaults to Germany staging (https://stg.api.telekom.com). Change only that one variable to target production or another country (see the base_url variable description).\n\nCIBA requires the subscriber to approve the authentication request out-of-band (e.g. on their device) between request 1 and request 2 completing successfully.\n\nCurrently listed for: 🇩🇪 Germany. (Source: this app's live product-catalog grant-type configuration, staging environment - see the /documentation/postman-collections page for the current matrix.)",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "item": [
    {
      "name": "1) BC-Authorize (start CIBA flow)",
      "event": [
        {
          "listen": "prerequest",
          "script": {
            "type": "text/javascript",
            "exec": [
              "if (!pm.collectionVariables.get('client_id')) {",
              "    throw new Error('Set the client_id collection variable before running this request.');",
              "}",
              "const pk = pm.collectionVariables.get('private_key');",
              "if (!pk || pk.indexOf('PASTE-YOUR-PKCS8-PRIVATE-KEY-HERE') !== -1) {",
              "    throw new Error('Set the private_key collection variable (PKCS8 PEM) before running this request - placeholder still present.');",
              "}",
              "",
              "// Pure-JS RS256 signer for Postman sandbox (no Node 'crypto').",
              "// Dependencies available in every Postman sandbox: BigInt, CryptoJS (global), atob/btoa.",
              "// In Node (for offline testing) we shim CryptoJS.SHA256 + atob/btoa below.",
              "",
              "function b64urlFromBytes(bytes) {",
              "    let bin = '';",
              "    for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);",
              "    let b64 = btoa(bin);",
              "    return b64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');",
              "}",
              "",
              "function b64urlFromString(str) {",
              "    // str is a JS string containing only ASCII/UTF-8 JSON text",
              "    const utf8 = unescape(encodeURIComponent(str));",
              "    return b64urlFromBytes(Uint8ArrayFromBinaryString(utf8));",
              "}",
              "",
              "function Uint8ArrayFromBinaryString(bin) {",
              "    const arr = new Uint8Array(bin.length);",
              "    for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);",
              "    return arr;",
              "}",
              "",
              "function pemToDerBytes(pem) {",
              "    const b64 = pem",
              "        .replace(/-----BEGIN [^-]+-----/, '')",
              "        .replace(/-----END [^-]+-----/, '')",
              "        .replace(/\\s+/g, '');",
              "    const bin = atob(b64);",
              "    return Uint8ArrayFromBinaryString(bin);",
              "}",
              "",
              "// Minimal DER parser: reads a TLV at offset, returns {tag, len, contentStart, nextOffset}",
              "function readDerTlv(bytes, offset) {",
              "    const tag = bytes[offset];",
              "    let lenByte = bytes[offset + 1];",
              "    let len, lenBytesUsed;",
              "    if ((lenByte & 0x80) === 0) {",
              "        len = lenByte;",
              "        lenBytesUsed = 1;",
              "    } else {",
              "        const numLenBytes = lenByte & 0x7f;",
              "        len = 0;",
              "        for (let i = 0; i < numLenBytes; i++) {",
              "            len = (len * 256) + bytes[offset + 2 + i];",
              "        }",
              "        lenBytesUsed = 1 + numLenBytes;",
              "    }",
              "    const contentStart = offset + 1 + lenBytesUsed;",
              "    return { tag, len, contentStart, nextOffset: contentStart + len };",
              "}",
              "",
              "function derToBigInt(bytes, start, len) {",
              "    let hex = '';",
              "    for (let i = 0; i < len; i++) {",
              "        const b = bytes[start + i];",
              "        hex += (b < 16 ? '0' : '') + b.toString(16);",
              "    }",
              "    if (hex === '') return BigInt(0);",
              "    return BigInt('0x' + hex);",
              "}",
              "",
              "// Extracts { n, d } (modulus, private exponent) from a PKCS#1 or PKCS#8 RSA private key.",
              "function extractRsaPrivateKeyParams(pem) {",
              "    const bytes = pemToDerBytes(pem);",
              "    // Top-level SEQUENCE",
              "    let tlv = readDerTlv(bytes, 0); // SEQUENCE",
              "    let cursor = tlv.contentStart;",
              "    // Both PKCS#1 and PKCS#8 start with a version INTEGER, so peek the *second*",
              "    // element to distinguish: PKCS#8 has an AlgorithmIdentifier SEQUENCE (0x30)",
              "    // next, PKCS#1 has the modulus INTEGER (0x02) next.",
              "    let versionPeek = readDerTlv(bytes, cursor);",
              "    let second = readDerTlv(bytes, versionPeek.nextOffset);",
              "    let rsaKeyStart;",
              "    if (second.tag === 0x02) {",
              "        // PKCS#1: SEQUENCE { version INTEGER, n INTEGER, e INTEGER, d INTEGER, ... }",
              "        rsaKeyStart = cursor;",
              "    } else {",
              "        // PKCS#8: SEQUENCE { version INTEGER, AlgorithmIdentifier SEQUENCE, PrivateKey OCTET STRING { PKCS#1 SEQUENCE } }",
              "        // skip version INTEGER",
              "        let versionTlv = readDerTlv(bytes, cursor);",
              "        cursor = versionTlv.nextOffset;",
              "        // skip AlgorithmIdentifier SEQUENCE",
              "        let algTlv = readDerTlv(bytes, cursor);",
              "        cursor = algTlv.nextOffset;",
              "        // OCTET STRING wrapping the PKCS#1 key",
              "        let octetTlv = readDerTlv(bytes, cursor);",
              "        // Inside the octet string is the PKCS#1 SEQUENCE",
              "        let inner = readDerTlv(bytes, octetTlv.contentStart);",
              "        rsaKeyStart = inner.contentStart;",
              "    }",
              "    // Now parse PKCS#1 fields starting at rsaKeyStart: version, n, e, d, p, q, ...",
              "    let p = rsaKeyStart;",
              "    let versionField = readDerTlv(bytes, p);",
              "    p = versionField.nextOffset;",
              "    let nField = readDerTlv(bytes, p);",
              "    const n = derToBigInt(bytes, nField.contentStart, nField.len);",
              "    p = nField.nextOffset;",
              "    let eField = readDerTlv(bytes, p);",
              "    p = eField.nextOffset;",
              "    let dField = readDerTlv(bytes, p);",
              "    const d = derToBigInt(bytes, dField.contentStart, dField.len);",
              "    // DER INTEGER may include a leading 0x00 sign-padding byte when the high",
              "    // bit of the modulus is set; the true RSA modulus byte length (k) must",
              "    // exclude that padding byte, so derive it from n's actual bit length.",
              "    const modulusByteLength = Math.ceil(n.toString(2).length / 8);",
              "    return { n, d, modulusByteLength };",
              "}",
              "",
              "// Modular exponentiation: base^exp mod m using BigInt",
              "function modPow(base, exp, mod) {",
              "    let result = BigInt(1);",
              "    base = base % mod;",
              "    while (exp > BigInt(0)) {",
              "        if (exp % BigInt(2) === BigInt(1)) {",
              "            result = (result * base) % mod;",
              "        }",
              "        exp = exp / BigInt(2);",
              "        base = (base * base) % mod;",
              "    }",
              "    return result;",
              "}",
              "",
              "// SHA-256 DigestInfo prefix for PKCS#1 v1.5 (RFC 3447)",
              "const SHA256_DIGEST_INFO_PREFIX_HEX =",
              "    '3031300d060960864801650304020105000420';",
              "",
              "function hexToBytes(hex) {",
              "    const arr = new Uint8Array(hex.length / 2);",
              "    for (let i = 0; i < arr.length; i++) {",
              "        arr[i] = parseInt(hex.substr(i * 2, 2), 16);",
              "    }",
              "    return arr;",
              "}",
              "",
              "function bytesToHex(bytes) {",
              "    let hex = '';",
              "    for (let i = 0; i < bytes.length; i++) {",
              "        hex += (bytes[i] < 16 ? '0' : '') + bytes[i].toString(16);",
              "    }",
              "    return hex;",
              "}",
              "",
              "// EMSA-PKCS1-v1_5 encode a SHA-256 digest (as hex string) into a byte array of length k (modulus byte length)",
              "function emsaPkcs1v15Encode(digestHex, k) {",
              "    const digestInfoHex = SHA256_DIGEST_INFO_PREFIX_HEX + digestHex;",
              "    const digestInfo = hexToBytes(digestInfoHex);",
              "    const tLen = digestInfo.length;",
              "    if (k < tLen + 11) {",
              "        throw new Error('RSA modulus too short for SHA-256 PKCS1v1.5 padding');",
              "    }",
              "    const psLen = k - tLen - 3;",
              "    const em = new Uint8Array(k);",
              "    em[0] = 0x00;",
              "    em[1] = 0x01;",
              "    for (let i = 0; i < psLen; i++) em[2 + i] = 0xff;",
              "    em[2 + psLen] = 0x00;",
              "    em.set(digestInfo, 3 + psLen);",
              "    return em;",
              "}",
              "",
              "function bytesToBigInt(bytes) {",
              "    return BigInt('0x' + (bytesToHex(bytes) || '0'));",
              "}",
              "",
              "function bigIntToBytes(bi, length) {",
              "    let hex = bi.toString(16);",
              "    if (hex.length % 2 !== 0) hex = '0' + hex;",
              "    let bytes = hexToBytes(hex);",
              "    if (bytes.length < length) {",
              "        const padded = new Uint8Array(length);",
              "        padded.set(bytes, length - bytes.length);",
              "        bytes = padded;",
              "    }",
              "    return bytes;",
              "}",
              "",
              "// Sign a JS string (the JWT signing input \"header.payload\") with an RSA private key PEM (PKCS#1 or PKCS#8).",
              "// Returns base64url signature string. Uses CryptoJS.SHA256 (Postman global) for the digest.",
              "function rs256Sign(signingInput, privateKeyPem) {",
              "    const { n, d, modulusByteLength } = extractRsaPrivateKeyParams(privateKeyPem);",
              "    const digestHex = CryptoJS.SHA256(signingInput).toString(CryptoJS.enc.Hex);",
              "    const em = emsaPkcs1v15Encode(digestHex, modulusByteLength);",
              "    const m = bytesToBigInt(em);",
              "    const s = modPow(m, d, n);",
              "    const sigBytes = bigIntToBytes(s, modulusByteLength);",
              "    return b64urlFromBytes(sigBytes);",
              "}",
              "",
              "// Build and sign a JWT given header/payload objects and a PEM private key.",
              "function buildSignedJwt(headerObj, payloadObj, privateKeyPem) {",
              "    const headerB64 = b64urlFromString(JSON.stringify(headerObj));",
              "    const payloadB64 = b64urlFromString(JSON.stringify(payloadObj));",
              "    const signingInput = headerB64 + '.' + payloadB64;",
              "    const sig = rs256Sign(signingInput, privateKeyPem);",
              "    return signingInput + '.' + sig;",
              "}",
              "",
              "const clientId = pm.collectionVariables.get('client_id');",
              "const privateKey = pm.collectionVariables.get('private_key');",
              "const kid = pm.collectionVariables.get('kid');",
              "const tokenEndpoint = (pm.collectionVariables.get('base_url') + '/token');",
              "const now = Math.floor(Date.now() / 1000);",
              "const header = { alg: 'RS256', typ: 'JWT' };",
              "if (kid) { header.kid = kid; }",
              "const payload = {",
              "    iss: clientId,",
              "    sub: clientId,",
              "    aud: tokenEndpoint,",
              "    iat: now,",
              "    exp: now + 300,",
              "    jti: pm.variables.replaceIn('{{$guid}}')",
              "};",
              "const clientAssertion = buildSignedJwt(header, payload, privateKey);",
              "pm.collectionVariables.set('client_assertion', clientAssertion);",
              "pm.collectionVariables.set('client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');"
            ]
          }
        },
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "const json = pm.response.json();",
              "if (json.auth_req_id) {",
              "    pm.collectionVariables.set('auth_req_id', json.auth_req_id);",
              "    pm.collectionVariables.set('interval', String(json.interval || 5));",
              "    pm.collectionVariables.set('expires_in', String(json.expires_in || 120));",
              "    pm.collectionVariables.set('poll_attempts', '0');",
              "    console.log('auth_req_id captured, interval=' + (json.interval || 5) + 's. Approve the request on the subscriber device, then run the next request (it will auto-poll).');",
              "} else {",
              "    console.error('No auth_req_id in response:', JSON.stringify(json));",
              "}",
              "pm.test('bc-authorize response has auth_req_id', function () {",
              "    pm.expect(json.auth_req_id, JSON.stringify(json)).to.be.a('string');",
              "});"
            ]
          }
        }
      ],
      "request": {
        "method": "POST",
        "header": [
          {
            "key": "Content-Type",
            "value": "application/x-www-form-urlencoded",
            "type": "text"
          }
        ],
        "body": {
          "mode": "urlencoded",
          "urlencoded": [
            {
              "key": "scope",
              "value": "{{scope}}",
              "type": "text"
            },
            {
              "key": "login_hint",
              "value": "tel:{{phone_number}}",
              "type": "text"
            },
            {
              "key": "client_id",
              "value": "{{client_id}}",
              "type": "text"
            },
            {
              "key": "client_assertion_type",
              "value": "{{client_assertion_type}}",
              "type": "text"
            },
            {
              "key": "client_assertion",
              "value": "{{client_assertion}}",
              "type": "text"
            }
          ]
        },
        "url": {
          "raw": "{{base_url}}/bc-authorize",
          "host": [
            "{{base_url}}/bc-authorize"
          ]
        },
        "description": "Starts a CIBA (Client Initiated Backchannel Authentication) flow for the subscriber tel:{{phone_number}}. Captures auth_req_id, interval and expires_in into collection variables for the next (polling) request."
      },
      "response": []
    },
    {
      "name": "2) Poll Token (CIBA, auto-retries)",
      "event": [
        {
          "listen": "prerequest",
          "script": {
            "type": "text/javascript",
            "exec": [
              "if (!pm.collectionVariables.get('client_id')) {",
              "    throw new Error('Set the client_id collection variable before running this request.');",
              "}",
              "const pk = pm.collectionVariables.get('private_key');",
              "if (!pk || pk.indexOf('PASTE-YOUR-PKCS8-PRIVATE-KEY-HERE') !== -1) {",
              "    throw new Error('Set the private_key collection variable (PKCS8 PEM) before running this request - placeholder still present.');",
              "}",
              "",
              "// Pure-JS RS256 signer for Postman sandbox (no Node 'crypto').",
              "// Dependencies available in every Postman sandbox: BigInt, CryptoJS (global), atob/btoa.",
              "// In Node (for offline testing) we shim CryptoJS.SHA256 + atob/btoa below.",
              "",
              "function b64urlFromBytes(bytes) {",
              "    let bin = '';",
              "    for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);",
              "    let b64 = btoa(bin);",
              "    return b64.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');",
              "}",
              "",
              "function b64urlFromString(str) {",
              "    // str is a JS string containing only ASCII/UTF-8 JSON text",
              "    const utf8 = unescape(encodeURIComponent(str));",
              "    return b64urlFromBytes(Uint8ArrayFromBinaryString(utf8));",
              "}",
              "",
              "function Uint8ArrayFromBinaryString(bin) {",
              "    const arr = new Uint8Array(bin.length);",
              "    for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);",
              "    return arr;",
              "}",
              "",
              "function pemToDerBytes(pem) {",
              "    const b64 = pem",
              "        .replace(/-----BEGIN [^-]+-----/, '')",
              "        .replace(/-----END [^-]+-----/, '')",
              "        .replace(/\\s+/g, '');",
              "    const bin = atob(b64);",
              "    return Uint8ArrayFromBinaryString(bin);",
              "}",
              "",
              "// Minimal DER parser: reads a TLV at offset, returns {tag, len, contentStart, nextOffset}",
              "function readDerTlv(bytes, offset) {",
              "    const tag = bytes[offset];",
              "    let lenByte = bytes[offset + 1];",
              "    let len, lenBytesUsed;",
              "    if ((lenByte & 0x80) === 0) {",
              "        len = lenByte;",
              "        lenBytesUsed = 1;",
              "    } else {",
              "        const numLenBytes = lenByte & 0x7f;",
              "        len = 0;",
              "        for (let i = 0; i < numLenBytes; i++) {",
              "            len = (len * 256) + bytes[offset + 2 + i];",
              "        }",
              "        lenBytesUsed = 1 + numLenBytes;",
              "    }",
              "    const contentStart = offset + 1 + lenBytesUsed;",
              "    return { tag, len, contentStart, nextOffset: contentStart + len };",
              "}",
              "",
              "function derToBigInt(bytes, start, len) {",
              "    let hex = '';",
              "    for (let i = 0; i < len; i++) {",
              "        const b = bytes[start + i];",
              "        hex += (b < 16 ? '0' : '') + b.toString(16);",
              "    }",
              "    if (hex === '') return BigInt(0);",
              "    return BigInt('0x' + hex);",
              "}",
              "",
              "// Extracts { n, d } (modulus, private exponent) from a PKCS#1 or PKCS#8 RSA private key.",
              "function extractRsaPrivateKeyParams(pem) {",
              "    const bytes = pemToDerBytes(pem);",
              "    // Top-level SEQUENCE",
              "    let tlv = readDerTlv(bytes, 0); // SEQUENCE",
              "    let cursor = tlv.contentStart;",
              "    // Both PKCS#1 and PKCS#8 start with a version INTEGER, so peek the *second*",
              "    // element to distinguish: PKCS#8 has an AlgorithmIdentifier SEQUENCE (0x30)",
              "    // next, PKCS#1 has the modulus INTEGER (0x02) next.",
              "    let versionPeek = readDerTlv(bytes, cursor);",
              "    let second = readDerTlv(bytes, versionPeek.nextOffset);",
              "    let rsaKeyStart;",
              "    if (second.tag === 0x02) {",
              "        // PKCS#1: SEQUENCE { version INTEGER, n INTEGER, e INTEGER, d INTEGER, ... }",
              "        rsaKeyStart = cursor;",
              "    } else {",
              "        // PKCS#8: SEQUENCE { version INTEGER, AlgorithmIdentifier SEQUENCE, PrivateKey OCTET STRING { PKCS#1 SEQUENCE } }",
              "        // skip version INTEGER",
              "        let versionTlv = readDerTlv(bytes, cursor);",
              "        cursor = versionTlv.nextOffset;",
              "        // skip AlgorithmIdentifier SEQUENCE",
              "        let algTlv = readDerTlv(bytes, cursor);",
              "        cursor = algTlv.nextOffset;",
              "        // OCTET STRING wrapping the PKCS#1 key",
              "        let octetTlv = readDerTlv(bytes, cursor);",
              "        // Inside the octet string is the PKCS#1 SEQUENCE",
              "        let inner = readDerTlv(bytes, octetTlv.contentStart);",
              "        rsaKeyStart = inner.contentStart;",
              "    }",
              "    // Now parse PKCS#1 fields starting at rsaKeyStart: version, n, e, d, p, q, ...",
              "    let p = rsaKeyStart;",
              "    let versionField = readDerTlv(bytes, p);",
              "    p = versionField.nextOffset;",
              "    let nField = readDerTlv(bytes, p);",
              "    const n = derToBigInt(bytes, nField.contentStart, nField.len);",
              "    p = nField.nextOffset;",
              "    let eField = readDerTlv(bytes, p);",
              "    p = eField.nextOffset;",
              "    let dField = readDerTlv(bytes, p);",
              "    const d = derToBigInt(bytes, dField.contentStart, dField.len);",
              "    // DER INTEGER may include a leading 0x00 sign-padding byte when the high",
              "    // bit of the modulus is set; the true RSA modulus byte length (k) must",
              "    // exclude that padding byte, so derive it from n's actual bit length.",
              "    const modulusByteLength = Math.ceil(n.toString(2).length / 8);",
              "    return { n, d, modulusByteLength };",
              "}",
              "",
              "// Modular exponentiation: base^exp mod m using BigInt",
              "function modPow(base, exp, mod) {",
              "    let result = BigInt(1);",
              "    base = base % mod;",
              "    while (exp > BigInt(0)) {",
              "        if (exp % BigInt(2) === BigInt(1)) {",
              "            result = (result * base) % mod;",
              "        }",
              "        exp = exp / BigInt(2);",
              "        base = (base * base) % mod;",
              "    }",
              "    return result;",
              "}",
              "",
              "// SHA-256 DigestInfo prefix for PKCS#1 v1.5 (RFC 3447)",
              "const SHA256_DIGEST_INFO_PREFIX_HEX =",
              "    '3031300d060960864801650304020105000420';",
              "",
              "function hexToBytes(hex) {",
              "    const arr = new Uint8Array(hex.length / 2);",
              "    for (let i = 0; i < arr.length; i++) {",
              "        arr[i] = parseInt(hex.substr(i * 2, 2), 16);",
              "    }",
              "    return arr;",
              "}",
              "",
              "function bytesToHex(bytes) {",
              "    let hex = '';",
              "    for (let i = 0; i < bytes.length; i++) {",
              "        hex += (bytes[i] < 16 ? '0' : '') + bytes[i].toString(16);",
              "    }",
              "    return hex;",
              "}",
              "",
              "// EMSA-PKCS1-v1_5 encode a SHA-256 digest (as hex string) into a byte array of length k (modulus byte length)",
              "function emsaPkcs1v15Encode(digestHex, k) {",
              "    const digestInfoHex = SHA256_DIGEST_INFO_PREFIX_HEX + digestHex;",
              "    const digestInfo = hexToBytes(digestInfoHex);",
              "    const tLen = digestInfo.length;",
              "    if (k < tLen + 11) {",
              "        throw new Error('RSA modulus too short for SHA-256 PKCS1v1.5 padding');",
              "    }",
              "    const psLen = k - tLen - 3;",
              "    const em = new Uint8Array(k);",
              "    em[0] = 0x00;",
              "    em[1] = 0x01;",
              "    for (let i = 0; i < psLen; i++) em[2 + i] = 0xff;",
              "    em[2 + psLen] = 0x00;",
              "    em.set(digestInfo, 3 + psLen);",
              "    return em;",
              "}",
              "",
              "function bytesToBigInt(bytes) {",
              "    return BigInt('0x' + (bytesToHex(bytes) || '0'));",
              "}",
              "",
              "function bigIntToBytes(bi, length) {",
              "    let hex = bi.toString(16);",
              "    if (hex.length % 2 !== 0) hex = '0' + hex;",
              "    let bytes = hexToBytes(hex);",
              "    if (bytes.length < length) {",
              "        const padded = new Uint8Array(length);",
              "        padded.set(bytes, length - bytes.length);",
              "        bytes = padded;",
              "    }",
              "    return bytes;",
              "}",
              "",
              "// Sign a JS string (the JWT signing input \"header.payload\") with an RSA private key PEM (PKCS#1 or PKCS#8).",
              "// Returns base64url signature string. Uses CryptoJS.SHA256 (Postman global) for the digest.",
              "function rs256Sign(signingInput, privateKeyPem) {",
              "    const { n, d, modulusByteLength } = extractRsaPrivateKeyParams(privateKeyPem);",
              "    const digestHex = CryptoJS.SHA256(signingInput).toString(CryptoJS.enc.Hex);",
              "    const em = emsaPkcs1v15Encode(digestHex, modulusByteLength);",
              "    const m = bytesToBigInt(em);",
              "    const s = modPow(m, d, n);",
              "    const sigBytes = bigIntToBytes(s, modulusByteLength);",
              "    return b64urlFromBytes(sigBytes);",
              "}",
              "",
              "// Build and sign a JWT given header/payload objects and a PEM private key.",
              "function buildSignedJwt(headerObj, payloadObj, privateKeyPem) {",
              "    const headerB64 = b64urlFromString(JSON.stringify(headerObj));",
              "    const payloadB64 = b64urlFromString(JSON.stringify(payloadObj));",
              "    const signingInput = headerB64 + '.' + payloadB64;",
              "    const sig = rs256Sign(signingInput, privateKeyPem);",
              "    return signingInput + '.' + sig;",
              "}",
              "",
              "const clientId = pm.collectionVariables.get('client_id');",
              "const privateKey = pm.collectionVariables.get('private_key');",
              "const kid = pm.collectionVariables.get('kid');",
              "const tokenEndpoint = (pm.collectionVariables.get('base_url') + '/token');",
              "const now = Math.floor(Date.now() / 1000);",
              "const header = { alg: 'RS256', typ: 'JWT' };",
              "if (kid) { header.kid = kid; }",
              "const payload = {",
              "    iss: clientId,",
              "    sub: clientId,",
              "    aud: tokenEndpoint,",
              "    iat: now,",
              "    exp: now + 300,",
              "    jti: pm.variables.replaceIn('{{$guid}}')",
              "};",
              "const clientAssertion = buildSignedJwt(header, payload, privateKey);",
              "pm.collectionVariables.set('client_assertion', clientAssertion);",
              "pm.collectionVariables.set('client_assertion_type', 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');"
            ]
          }
        },
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "const MAX_ATTEMPTS = 24; // ~2 minutes at the default 5s interval",
              "let json = {};",
              "try { json = pm.response.json(); } catch (e) { json = {}; }",
              "const attempts = parseInt(pm.collectionVariables.get('poll_attempts') || '0', 10);",
              "const intervalSeconds = parseInt(pm.collectionVariables.get('interval') || '5', 10);",
              "",
              "if (json.access_token) {",
              "    pm.collectionVariables.set('access_token', json.access_token);",
              "    console.log('access_token captured after ' + attempts + ' poll(s). Advancing to the service request.');",
              "    postman.setNextRequest('POST KYC Age Verification v0.2');",
              "} else if (json.error === 'authorization_pending' || json.error === 'slow_down') {",
              "    const nextAttempts = attempts + 1;",
              "    pm.collectionVariables.set('poll_attempts', String(nextAttempts));",
              "    if (json.error === 'slow_down') {",
              "        pm.collectionVariables.set('interval', String(intervalSeconds + 5));",
              "    }",
              "    if (nextAttempts >= MAX_ATTEMPTS) {",
              "        console.error('CIBA polling gave up after ' + nextAttempts + ' attempts.');",
              "        postman.setNextRequest(null);",
              "    } else {",
              "        console.log('CIBA pending (' + json.error + '), attempt ' + nextAttempts + '/' + MAX_ATTEMPTS + '. Waiting ' + intervalSeconds + 's before retrying.');",
              "        setTimeout(function () {",
              "            postman.setNextRequest('2) Poll Token (CIBA, auto-retries)');",
              "        }, intervalSeconds * 1000);",
              "    }",
              "} else {",
              "    console.error('CIBA polling stopped due to error:', JSON.stringify(json));",
              "    postman.setNextRequest(null);",
              "}"
            ]
          }
        }
      ],
      "request": {
        "method": "POST",
        "header": [
          {
            "key": "Content-Type",
            "value": "application/x-www-form-urlencoded",
            "type": "text"
          }
        ],
        "body": {
          "mode": "urlencoded",
          "urlencoded": [
            {
              "key": "grant_type",
              "value": "urn:openid:params:grant-type:ciba",
              "type": "text"
            },
            {
              "key": "auth_req_id",
              "value": "{{auth_req_id}}",
              "type": "text"
            },
            {
              "key": "client_id",
              "value": "{{client_id}}",
              "type": "text"
            },
            {
              "key": "client_assertion_type",
              "value": "{{client_assertion_type}}",
              "type": "text"
            },
            {
              "key": "client_assertion",
              "value": "{{client_assertion}}",
              "type": "text"
            }
          ]
        },
        "url": {
          "raw": "{{base_url}}/token",
          "host": [
            "{{base_url}}/token"
          ]
        },
        "description": "Polls the token endpoint for the outcome of the CIBA request. A fresh client_assertion is signed on every poll. Auto-retries on authorization_pending/slow_down (respecting the server's interval), capped at ~24 attempts (~2 minutes), then auto-advances to 'POST KYC Age Verification v0.2' once access_token is captured. Run request 1 first."
      },
      "response": []
    },
    {
      "name": "POST KYC Age Verification v0.2",
      "event": [],
      "request": {
        "method": "POST",
        "header": [
          {
            "key": "Authorization",
            "value": "Bearer {{access_token}}",
            "type": "text"
          },
          {
            "key": "Content-Type",
            "value": "application/json",
            "type": "text"
          },
          {
            "key": "x-correlator",
            "value": "{{$guid}}",
            "type": "text"
          }
        ],
        "body": {
          "mode": "raw",
          "raw": "{\n  \"ageThreshold\": {{age_threshold}}\n}",
          "options": {
            "raw": {
              "language": "json"
            }
          }
        },
        "url": {
          "raw": "{{base_url}}/kyc-age-verification/v0.2/verify",
          "host": [
            "{{base_url}}/kyc-age-verification/v0.2/verify"
          ]
        },
        "description": "Calls the KYC Age Verification v0.2 resource with ageThreshold from the age_threshold variable. Field name copied verbatim from CheckKycAgeVerificationV02 in SEPAPIs.cs (which validates 0-125)."
      },
      "response": []
    },
    {
      "name": "Health Check",
      "event": [],
      "request": {
        "method": "GET",
        "header": [
          {
            "key": "Authorization",
            "value": "Bearer {{access_token}}",
            "type": "text"
          },
          {
            "key": "x-correlator",
            "value": "{{$guid}}",
            "type": "text"
          }
        ],
        "url": {
          "raw": "{{base_url}}/kyc-age-verification/v0.2/health",
          "host": [
            "{{base_url}}/kyc-age-verification/v0.2/health"
          ]
        },
        "description": "Health check endpoint for this API. Returns HTTP success if the service is reachable."
      },
      "response": []
    }
  ],
  "variable": [
    {
      "key": "base_url",
      "value": "https://stg.api.telekom.com",
      "type": "string",
      "description": "Single variable driving every request in this collection. Default is Germany staging. Change ONLY this variable to switch environment/country:\n- Germany production: https://api.telekom.com\n- Austria staging: https://at.stg.api.telekom.com | production: https://at.api.telekom.com\n- Poland staging: https://pl.stg.api.telekom.com | production: https://pl.api.telekom.com\n- Greece staging: https://gr.stg.api.telekom.com | production: https://gr.api.telekom.com\nSee the /documentation/endpoints page in the MBAPI app for the full country/environment reference table. Every request and script in this collection builds the full endpoint path directly from base_url (e.g. {{base_url}}/token) rather than through a separate derived variable, because Postman does not resolve nested variable references when read inside pre-request/test scripts via pm.collectionVariables.get() - only base_url itself needs to be correct."
    },
    {
      "key": "scope",
      "value": "openid kyc-age-verification:verify dpv:FraudPreventionAndDetection",
      "type": "string",
      "description": "openid kyc-age-verification:verify dpv:FraudPreventionAndDetection - openid is prefixed because CIBA (urn:openid:params:grant-type:ciba) always requires the openid scope for the backchannel authentication request to succeed."
    },
    {
      "key": "client_id",
      "value": "",
      "type": "string",
      "description": "REQUIRED. Your registered OAuth client ID (PRIVATE_KEY_JWT client authentication)."
    },
    {
      "key": "private_key",
      "value": "-----BEGIN PRIVATE KEY-----\nPASTE-YOUR-PKCS8-PRIVATE-KEY-HERE\n-----END PRIVATE KEY-----",
      "type": "string",
      "description": "REQUIRED. Your own PKCS8 PEM RSA private key. Never share this key - replace the placeholder before running any request."
    },
    {
      "key": "kid",
      "value": "",
      "type": "string",
      "description": "Optional key ID matching a key in your JWKS - leave blank if your JWKS has only one key."
    },
    {
      "key": "phone_number",
      "value": "+491702049821",
      "type": "string",
      "description": "Standard test MSISDN for this API from this app's live DE staging standard configuration (SepStandardApiConfigurations table) - not a generic sample number."
    },
    {
      "key": "age_threshold",
      "value": "18",
      "type": "string",
      "description": "Age threshold to verify against (0-125). Matches this app's CheckKycAgeVerificationV02 default of 18."
    },
    {
      "key": "client_assertion_type",
      "value": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
      "type": "string",
      "description": "Fixed value for private_key_jwt client authentication."
    },
    {
      "key": "client_assertion",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request's pre-request script - the signed client_assertion JWT. Do not edit manually."
    },
    {
      "key": "access_token",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request. Do not edit manually."
    },
    {
      "key": "auth_req_id",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the bc-authorize request. Do not edit manually."
    },
    {
      "key": "interval",
      "value": "5",
      "type": "string",
      "description": "Polling interval in seconds - auto-populated/updated by bc-authorize and polling responses."
    },
    {
      "key": "expires_in",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the bc-authorize request."
    },
    {
      "key": "poll_attempts",
      "value": "0",
      "type": "string",
      "description": "Auto-populated poll counter - resets to 0 on each new bc-authorize."
    }
  ]
}
