{
  "info": {
    "name": "Number Verification v1 - Authorization Code",
    "description": "CAMARA/SEP Number Verification v1 (NV1), resource path number-verification/v1/verify, using the AUTHORIZATION_CODE grant (the only grant type this app supports for NV1) with PRIVATE_KEY_JWT client authentication (RS256 client_assertion signed with your own private key - never a client secret).\n\nNV1 is a NETWORK-BASED / silent authorize flow: the /authorize step must be opened in a real mobile browser on the subscriber's cellular data connection (not Wi-Fi, not inside Postman) so the mobile network operator can silently identify the subscriber and redirect back with an authorization code - there is no login/consent screen and no phone number field in the API calls themselves.\n\nHow to use this collection:\n1. Fill in client_id, private_key (PKCS8 PEM), and optionally kid in the collection variables.\n2. Run request 1 to build the /authorize URL, then open the generated URL (see the Postman console log, or copy request 1's URL) in a real mobile browser on cellular data. Approve/observe the redirect back to your redirect_uri and copy the `code` query parameter value.\n3. Paste that code into the auth_code collection variable.\n4. Run request 2 to exchange the code for an access token (this signs a PRIVATE_KEY_JWT client_assertion locally in the pre-request script - your private key never leaves your machine/Postman instance).\n5. Run request 3 to call Number Verification v1 Verify with the resulting access token.\n\nSwitching country/environment: change ONLY the base_url collection variable. Defaults to Germany staging (https://stg.api.telekom.com). Other options: Germany production https://api.telekom.com; Austria staging https://at.stg.api.telekom.com / production https://at.api.telekom.com; Poland staging https://pl.stg.api.telekom.com / production https://pl.api.telekom.com; Greece staging https://gr.stg.api.telekom.com / production https://gr.api.telekom.com. See this app's /documentation/endpoints page for the full reference.\n\nCurrently listed for: 🇩🇪 Germany, 🇬🇷 Greece, 🇵🇱 Poland, 🇦🇹 Austria. (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"
  },
  "variable": [
    {
      "key": "base_url",
      "value": "https://stg.api.telekom.com",
      "description": "Single variable that drives every endpoint in this collection. Default is Germany staging. Change ONLY this variable to switch environment/country: Germany production https://api.telekom.com; Austria staging https://at.stg.api.telekom.com / production https://at.api.telekom.com; Poland staging https://pl.stg.api.telekom.com / production https://pl.api.telekom.com; Greece staging https://gr.stg.api.telekom.com / production https://gr.api.telekom.com. See the app's /documentation/endpoints page for the full reference. 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 (a variable whose value itself contains {{...}}) when read inside pre-request/test scripts via pm.collectionVariables.get() - only base_url itself needs to be correct. "
    },
    {
      "key": "client_id",
      "value": "",
      "description": "Your registered client ID for Number Verification v1. Fill this in - ships empty."
    },
    {
      "key": "private_key",
      "value": "-----BEGIN PRIVATE KEY-----\nPASTE-YOUR-PKCS8-PRIVATE-KEY-HERE\n-----END PRIVATE KEY-----",
      "description": "Your PKCS8 PEM RSA private key used to sign the PRIVATE_KEY_JWT client_assertion. Must be a PKCS8 (or PKCS1) PEM RSA private key. Ships as a placeholder - the pre-request script will refuse to run and raise a clear error if you forget to replace PASTE-YOUR-PKCS8-PRIVATE-KEY-HERE. Never share this value; it never leaves your Postman instance."
    },
    {
      "key": "kid",
      "value": "",
      "description": "Optional key ID matching a key in your JWKS - leave blank if your JWKS has only one key."
    },
    {
      "key": "redirect_uri",
      "value": "https://yourapp.com/callback",
      "description": "The redirect URI registered for your client, used in the /authorize request and the token exchange."
    },
    {
      "key": "auth_code",
      "value": "",
      "description": "Paste the `code` query parameter value here after completing the browser-based authorize step (request 1)."
    },
    {
      "key": "phone_number",
      "value": "+491704042076",
      "description": "Standard test MSISDN for this API from this app's live DE staging standard configuration (SepStandardApiConfigurations table, per-API/country/environment) - not a generic sample number."
    },
    {
      "key": "state",
      "value": "",
      "description": "Randomly generated by request 1's pre-request script - CSRF protection value echoed back on redirect."
    },
    {
      "key": "nonce",
      "value": "",
      "description": "Randomly generated by request 1's pre-request script - OIDC replay protection value."
    },
    {
      "key": "access_token",
      "value": "",
      "description": "Populated automatically by request 2's test script after a successful token exchange."
    }
  ],
  "item": [
    {
      "name": "1 - Build Authorize URL (open in browser)",
      "event": [
        {
          "listen": "prerequest",
          "script": {
            "type": "text/javascript",
            "exec": [
              "// Generate fresh state/nonce for this authorize attempt.",
              "function randomHex(len) {",
              "    var chars = 'abcdef0123456789';",
              "    var out = '';",
              "    for (var i = 0; i < len; i++) out += chars.charAt(Math.floor(Math.random() * chars.length));",
              "    return out;",
              "}",
              "var state = randomHex(24);",
              "var nonce = randomHex(24);",
              "pm.collectionVariables.set('state', state);",
              "pm.collectionVariables.set('nonce', nonce);",
              "",
              "var base = (pm.collectionVariables.get('base_url') + '/authorize');",
              "var clientId = pm.collectionVariables.get('client_id');",
              "var redirectUri = pm.collectionVariables.get('redirect_uri');",
              "var scope = 'openid number-verification:verify dpv:FraudPreventionAndDetection';",
              "",
              "var url = base",
              "    + '?response_type=code'",
              "    + '&client_id=' + encodeURIComponent(clientId)",
              "    + '&redirect_uri=' + encodeURIComponent(redirectUri)",
              "    + '&scope=' + encodeURIComponent(scope)",
              "    + '&state=' + encodeURIComponent(state)",
              "    + '&nonce=' + encodeURIComponent(nonce);",
              "",
              "pm.collectionVariables.set('generated_authorize_url', url);",
              "console.log('--- Number Verification v1: Authorize URL ---');",
              "console.log('Open this URL in a REAL MOBILE BROWSER on CELLULAR DATA (not Wi-Fi, not inside Postman):');",
              "console.log(url);",
              "console.log('After it redirects to your redirect_uri, copy the \"code\" query parameter value into the auth_code collection variable.');"
            ]
          }
        }
      ],
      "request": {
        "method": "GET",
        "header": [],
        "url": {
          "raw": "{{base_url}}/authorize?response_type=code&client_id={{client_id}}&redirect_uri={{redirect_uri}}&scope=openid%20number-verification%3Averify%20dpv%3AFraudPreventionAndDetection&state={{state}}&nonce={{nonce}}",
          "host": [
            "{{base_url}}/authorize"
          ],
          "query": [
            {
              "key": "response_type",
              "value": "code"
            },
            {
              "key": "client_id",
              "value": "{{client_id}}"
            },
            {
              "key": "redirect_uri",
              "value": "{{redirect_uri}}"
            },
            {
              "key": "scope",
              "value": "openid number-verification:verify dpv:FraudPreventionAndDetection"
            },
            {
              "key": "state",
              "value": "{{state}}"
            },
            {
              "key": "nonce",
              "value": "{{nonce}}"
            }
          ]
        },
        "description": "This request is FOR REFERENCE / COPY-PASTE ONLY - it is not meant to succeed inside Postman. Number Verification v1 uses a NETWORK-BASED, silent authorize flow: it only works when opened in a real mobile browser on the subscriber's cellular data connection, so the mobile network operator can silently identify the subscriber's phone number and redirect back with an authorization code (there is no login/consent screen). Running this GET from Postman will likely fail, hang, or be rejected because Postman is not a mobile browser on a cellular connection.\n\nSteps:\n1. Send this request once so the pre-request script logs the full authorize URL to the Postman Console (View > Show Postman Console).\n2. Copy that URL from the console (or use the Send button's generated URL) and open it in a real mobile browser on cellular data.\n3. After the silent redirect completes, your browser will land on redirect_uri with a `code` query parameter (and the same `state` you generated).\n4. Copy the `code` value and paste it into this collection's `auth_code` variable, then run request 2."
      }
    },
    {
      "name": "2 - Exchange Code for Token",
      "event": [
        {
          "listen": "prerequest",
          "script": {
            "type": "text/javascript",
            "exec": [
              "// Pure-JS RS256 signer for Postman sandbox (no Node 'crypto').",
              "// Dependencies available in every Postman sandbox: BigInt, CryptoJS (global), atob/btoa.",
              "",
              "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;",
              "}",
              "",
              "// --- Build the PRIVATE_KEY_JWT client_assertion for this token exchange ---",
              "var privateKey = pm.collectionVariables.get('private_key');",
              "if (!privateKey || privateKey.indexOf('PASTE-YOUR-PKCS8-PRIVATE-KEY-HERE') !== -1) {",
              "    throw new Error('Set the private_key collection variable to your real PKCS8 PEM RSA private key before running this request.');",
              "}",
              "",
              "var clientId = pm.collectionVariables.get('client_id');",
              "if (!clientId) {",
              "    throw new Error('Set the client_id collection variable before running this request.');",
              "}",
              "",
              "var tokenEndpoint = (pm.collectionVariables.get('base_url') + '/token');",
              "var kid = pm.collectionVariables.get('kid');",
              "var nowSeconds = Math.floor(Date.now() / 1000);",
              "",
              "var header = { alg: 'RS256' };",
              "if (kid) { header.kid = kid; }",
              "",
              "var payload = {",
              "    iss: clientId,",
              "    sub: clientId,",
              "    aud: tokenEndpoint,",
              "    iat: nowSeconds,",
              "    exp: nowSeconds + 300,",
              "    jti: (function () {",
              "        var chars = 'abcdef0123456789';",
              "        var out = '';",
              "        for (var i = 0; i < 32; i++) out += chars.charAt(Math.floor(Math.random() * chars.length));",
              "        return out;",
              "    })()",
              "};",
              "",
              "var clientAssertion = buildSignedJwt(header, payload, privateKey);",
              "pm.collectionVariables.set('client_assertion', clientAssertion);"
            ]
          }
        },
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "pm.test('Token exchange succeeded', function () {",
              "    pm.response.to.have.status(200);",
              "});",
              "",
              "var json = {};",
              "try { json = pm.response.json(); } catch (e) {}",
              "",
              "if (json && json.access_token) {",
              "    pm.collectionVariables.set('access_token', json.access_token);",
              "    console.log('Captured access_token into collection variable access_token.');",
              "} else {",
              "    console.log('No access_token found in response - check the response body for errors.');",
              "}"
            ]
          }
        }
      ],
      "request": {
        "method": "POST",
        "header": [
          {
            "key": "Content-Type",
            "value": "application/x-www-form-urlencoded"
          }
        ],
        "body": {
          "mode": "urlencoded",
          "urlencoded": [
            {
              "key": "grant_type",
              "value": "authorization_code"
            },
            {
              "key": "code",
              "value": "{{auth_code}}"
            },
            {
              "key": "redirect_uri",
              "value": "{{redirect_uri}}"
            },
            {
              "key": "client_id",
              "value": "{{client_id}}"
            },
            {
              "key": "client_assertion_type",
              "value": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
            },
            {
              "key": "client_assertion",
              "value": "{{client_assertion}}"
            }
          ]
        },
        "url": {
          "raw": "{{base_url}}/token",
          "host": [
            "{{base_url}}/token"
          ]
        },
        "description": "Exchanges the authorization code you pasted into auth_code for an access token, authenticating with PRIVATE_KEY_JWT (a locally-signed RS256 client_assertion) instead of a client secret. The pre-request script signs the JWT entirely in pure JavaScript (BigInt + CryptoJS.SHA256 + atob/btoa) - no Node crypto module is used, so this works in every Postman sandbox version. The signed access_token is captured into the access_token collection variable by the test script for use in request 3."
      }
    },
    {
      "name": "3 - Verify Number",
      "event": [
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "pm.test('Verify call succeeded', function () {",
              "    pm.response.to.have.status(200);",
              "});"
            ]
          }
        }
      ],
      "request": {
        "method": "POST",
        "header": [
          {
            "key": "Authorization",
            "value": "Bearer {{access_token}}"
          },
          {
            "key": "Content-Type",
            "value": "application/json"
          },
          {
            "key": "x-correlator",
            "value": "{{$guid}}"
          }
        ],
        "body": {
          "mode": "raw",
          "raw": "{\n  \"phoneNumber\": \"{{phone_number}}\"\n}",
          "options": {
            "raw": {
              "language": "json"
            }
          }
        },
        "url": {
          "raw": "{{base_url}}/number-verification/v1/verify",
          "host": [
            "{{base_url}}"
          ],
          "path": [
            "number-verification",
            "v1",
            "verify"
          ]
        },
        "description": "Calls Number Verification v1's verify endpoint with the access token obtained via the network-based authorization_code flow in request 2. The phoneNumber field is included per the CAMARA NV1 spec/SEPAPIs.cs's ExecuteNumberVerificationV1Verify body shape - the API verifies this number against the subscriber identity implicitly established during the silent /authorize redirect (request 1), it is not a lookup by arbitrary number."
      }
    }
  ]
}
