{
  "info": {
    "name": "TMF Onboarding (App Owner + Application + Product Order) - Private Key JWT",
    "description": "Full TMF931 onboarding flow: create an Application Owner, create an Application under that owner, then order the SEP CAMARA API product for that application. Uses PRIVATE_KEY_JWT client authentication for the onboarding API token instead of a client secret - RS256 signing happens entirely inside this collection's pre-request script using pure JavaScript (no external tools/plugins, no client secret ever transmitted).\n\nUse this instead of the separate 'Client Secret' collection if your Channel Partner client registration for the TMF onboarding API has been set up for the 'Signed JWT' authentication method with the DT MACE team.\n\nRun the requests in order, top to bottom - each captures the id it needs into a collection variable for the next request (owner_id, app_id, order_id).\n\nSwitching environment: change ONLY the host variable. Defaults to the staging onboarding host (https://api-dc71.lotusflare.com). Production onboarding host is https://sep.api.telekom.com. The realm variable controls which country/operator you onboard against (dtmace = Germany, dtmaceaustria = Austria, dtmacepoland = Poland, dtmacegreece = Greece) - see this app's /documentation/endpoints page for the full reference.\n\nNote: this creates a real Application Owner, Application and Product Order against the TMF platform - there is no sandbox/dry-run mode. Use test/sample values you're comfortable with, and a product_offering_id you were actually approved for.",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "item": [
    {
      "name": "1) Get Access Token (Client Credentials + Private Key JWT)",
      "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.",
              "// 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;",
              "}",
              "",
              "if (!pm.collectionVariables.get('cp_client_id')) {",
              "    throw new Error('Set the cp_client_id collection variable before running this request.');",
              "}",
              "const pk = pm.collectionVariables.get('cp_private_key');",
              "if (!pk || pk.indexOf('PASTE-YOUR-PKCS8-PRIVATE-KEY-HERE') !== -1) {",
              "    throw new Error('Set the cp_private_key collection variable (PKCS8 PEM) before running this request - placeholder still present.');",
              "}",
              "",
              "const clientId = pm.collectionVariables.get('cp_client_id');",
              "const privateKey = pm.collectionVariables.get('cp_private_key');",
              "const kid = pm.collectionVariables.get('cp_kid');",
              "const tokenEndpoint = (pm.collectionVariables.get('host') + '/realms/' + pm.collectionVariables.get('realm') + '/protocol/openid-connect/token');",
              "",
              "const header = { alg: 'RS256', typ: 'JWT' };",
              "if (kid) { header.kid = kid; }",
              "",
              "const nowSeconds = Math.floor(Date.now() / 1000);",
              "const payload = {",
              "    iss: clientId,",
              "    sub: clientId,",
              "    aud: tokenEndpoint,",
              "    iat: nowSeconds,",
              "    exp: nowSeconds + 300,",
              "    jti: pm.variables.replaceIn('{{$guid}}')",
              "};",
              "",
              "const clientAssertion = buildSignedJwt(header, payload, privateKey);",
              "pm.collectionVariables.set('client_assertion', clientAssertion);"
            ]
          }
        },
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "const json = pm.response.json();",
              "if (json.access_token) {",
              "    pm.collectionVariables.set('access_token', json.access_token);",
              "    console.log('access_token captured.');",
              "} else {",
              "    console.error('No access_token in response:', JSON.stringify(json));",
              "}",
              "pm.test('Token response has access_token', function () {",
              "    pm.expect(json.access_token, 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": "grant_type",
              "value": "client_credentials",
              "type": "text"
            },
            {
              "key": "client_id",
              "value": "{{cp_client_id}}",
              "type": "text"
            },
            {
              "key": "client_assertion_type",
              "value": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
              "type": "text"
            },
            {
              "key": "client_assertion",
              "value": "{{client_assertion}}",
              "type": "text"
            }
          ]
        },
        "url": {
          "raw": "{{host}}/realms/{{realm}}/protocol/openid-connect/token",
          "host": [
            "{{host}}/realms/{{realm}}/protocol/openid-connect/token"
          ]
        },
        "description": "Obtains an access token for the TMF931 onboarding/ordering API using PRIVATE_KEY_JWT client authentication (grant_type=client_credentials + a self-signed client_assertion) instead of a client secret.\n\nUse this if your Channel Partner client registration for the TMF onboarding API (Keycloak realm client) has been configured for the 'Signed JWT' client authentication method - confirm with the DT MACE team first.\n\nCaptures access_token into a collection variable for the requests that follow."
      },
      "response": []
    },
    {
      "name": "2) Create Application Owner",
      "event": [
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "const json = pm.response.json();",
              "if (json.id) {",
              "    pm.collectionVariables.set('owner_id', json.id);",
              "    console.log('Application Owner id captured: ' + json.id);",
              "} else {",
              "    console.error('No id in Application Owner response:', JSON.stringify(json));",
              "}",
              "pm.test('Application Owner response has id', function () {",
              "    pm.expect(json.id, JSON.stringify(json)).to.be.a('string');",
              "});"
            ]
          }
        }
      ],
      "request": {
        "method": "POST",
        "header": [
          {
            "key": "Authorization",
            "value": "Bearer {{access_token}}",
            "type": "text"
          },
          {
            "key": "Content-Type",
            "value": "application/json",
            "type": "text"
          }
        ],
        "body": {
          "mode": "raw",
          "raw": "{\n    \"@type\": \"ApplicationOwner\",\n    \"@baseType\": \"PartyRole\",\n    \"name\": \"{{owner_name}}\",\n    \"description\": \"Postman test Application Owner, created via the TMF931 onboarding API.\",\n    \"engagedParty\": {\n        \"@type\": \"ApplicationOwnerOrganization\",\n        \"@baseType\": \"Organization\",\n        \"name\": \"{{owner_name}} Org\",\n        \"tradingName\": \"Postman Test Org\",\n        \"isLegalEntity\": true,\n        \"organizationType\": \"dpv:ForProfitOrganisation\",\n        \"privacyPolicyURL\": \"https://example.com/privacy\",\n        \"taxNumber\": \"00.000.000/0000-00\",\n        \"organizationIdentification\": [\n            {\n                \"@type\": \"OrganizationIdentification\",\n                \"identificationType\": \"companyRegistration\",\n                \"identificationId\": \"00.000.000/0000-00\",\n                \"issuingAuthority\": \"Federal\"\n            }\n        ],\n        \"registeredGeographicAddress\": {\n            \"@type\": \"LightGeographicAddress\",\n            \"@baseType\": \"GeographicAddress\",\n            \"streetNr\": \"123\",\n            \"streetName\": \"Avenida Paulista\",\n            \"locality\": \"Paraíso\",\n            \"city\": \"São Paulo\",\n            \"stateOrProvince\": \"São Paulo\",\n            \"countryCode\": {\n                \"@type\": \"ISO31661Alpha2StandardIdentifier\",\n                \"@baseType\": \"StandardIdentifier\",\n                \"value\": \"BR\"\n            },\n            \"postcode\": \"01311-000\",\n            \"geographicSubAddress\": {\n                \"@type\": \"LightGeographicSubAddress\",\n                \"@baseType\": \"GeographicSubAddress\",\n                \"buildingName\": \"Fogo\",\n                \"levelNumber\": \"Planta 4\"\n            }\n        },\n        \"contactMedium\": [\n            {\n                \"@type\": \"EmailContactMedium\",\n                \"@baseType\": \"ContactMedium\",\n                \"id\": \"1\",\n                \"preferred\": true,\n                \"emailAddress\": \"integrationtest@example.de\",\n                \"contactType\": \"professional\"\n            }\n        ],\n        \"privacyManager\": {\n            \"@type\": \"ApplicationOwnerRelatedOrganization\",\n            \"name\": \"Privacy Office\",\n            \"organizationType\": \"department\",\n            \"contactMedium\": [\n                {\n                    \"@type\": \"EmailContactMedium\",\n                    \"id\": \"1\",\n                    \"preferred\": true,\n                    \"@baseType\": \"ContactMedium\",\n                    \"emailAddress\": \"integrationtest@example.de\"\n                }\n            ]\n        }\n    }\n}",
          "options": {
            "raw": {
              "language": "json"
            }
          }
        },
        "url": {
          "raw": "{{host}}/{{base_path}}/applicationOwner",
          "host": [
            "{{host}}/{{base_path}}/applicationOwner"
          ]
        },
        "description": "Creates a TMF ApplicationOwner (party role) - the organization/team that will own the SEP CAMARA application. Response id is captured into owner_id for the next requests. Field names and sample values match this app's TMF931Controller.cs CreateApplicationOwner method verbatim."
      },
      "response": []
    },
    {
      "name": "3) Create Application",
      "event": [
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "const json = pm.response.json();",
              "if (json.id) {",
              "    pm.collectionVariables.set('app_id', json.id);",
              "    console.log('Application id captured: ' + json.id);",
              "} else {",
              "    console.error('No id in Application response:', JSON.stringify(json));",
              "}",
              "pm.test('Application response has id', function () {",
              "    pm.expect(json.id, JSON.stringify(json)).to.be.a('string');",
              "});"
            ]
          }
        }
      ],
      "request": {
        "method": "POST",
        "header": [
          {
            "key": "Authorization",
            "value": "Bearer {{access_token}}",
            "type": "text"
          },
          {
            "key": "Content-Type",
            "value": "application/json",
            "type": "text"
          }
        ],
        "body": {
          "mode": "raw",
          "raw": "{\n    \"@type\": \"Application\",\n    \"name\": \"{{app_name}}\",\n    \"description\": \"Postman test Application, created via the TMF931 onboarding API.\",\n    \"category\": \"games\",\n    \"commercialName\": \"Postman Test App Commercial Name\",\n    \"applicationOwner\": {\n        \"id\": \"{{owner_id}}\",\n        \"@type\": \"PartyRoleRef\"\n    },\n    \"jwksUri\": \"{{jwks_uri}}\",\n    \"redirectUrl\": [\n        \"{{redirect_url}}\"\n    ],\n    \"digitalIdentity\": {\n        \"@type\": \"ApiDigitalIdentity\",\n        \"clientId\": \"{{requested_client_id}}\"\n    }\n}",
          "options": {
            "raw": {
              "language": "json"
            }
          }
        },
        "url": {
          "raw": "{{host}}/{{base_path}}/application",
          "host": [
            "{{host}}/{{base_path}}/application"
          ]
        },
        "description": "Creates a TMF Application under the Application Owner created in the previous request (owner_id). requested_client_id becomes the client_id your new SEP CAMARA application will use to obtain access tokens for service API calls. redirect_url/jwks_uri should point at your own infrastructure. Response id is captured into app_id for the next request. Field names match this app's TMF931Controller.cs CreateApplication method verbatim."
      },
      "response": []
    },
    {
      "name": "4) Create Product Order",
      "event": [
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "const json = pm.response.json();",
              "if (json.id) {",
              "    pm.collectionVariables.set('order_id', json.id);",
              "    console.log('Product Order id captured: ' + json.id);",
              "} else {",
              "    console.error('No id in Product Order response:', JSON.stringify(json));",
              "}",
              "pm.test('Product Order response has id', function () {",
              "    pm.expect(json.id, JSON.stringify(json)).to.be.a('string');",
              "});"
            ]
          }
        }
      ],
      "request": {
        "method": "POST",
        "header": [
          {
            "key": "Authorization",
            "value": "Bearer {{access_token}}",
            "type": "text"
          },
          {
            "key": "Content-Type",
            "value": "application/json",
            "type": "text"
          }
        ],
        "body": {
          "mode": "raw",
          "raw": "{\n    \"productOrderItem\": [\n        {\n            \"approvedProductOfferingTermOrCondition\": [\n                {\n                    \"productOfferingTermOrConditionSpecRef\": {\n                        \"id\": \"1\",\n                        \"name\": \"device_location_verification_terms\",\n                        \"version\": \"1.1\",\n                        \"@type\": \"ProductOfferingTermOrConditionSpecRef\"\n                    },\n                    \"name\": \"Device Location Term or Condition\",\n                    \"productOfferingTermOrConditionApproval\": {\n                        \"approvalDate\": \"{{$isoTimestamp}}\",\n                        \"authorization\": [\n                            {\n                                \"@type\": \"ApiAuthorization\",\n                                \"name\": \"Application Owner representative\",\n                                \"approver\": {\n                                    \"partyOrPartyRole\": {\n                                        \"@type\": \"PartyRoleRef\",\n                                        \"id\": \"{{owner_id}}\"\n                                    },\n                                    \"@type\": \"RelatedPartyRefOrPartyRoleRef\",\n                                    \"role\": \"applicationOwner\"\n                                }\n                            }\n                        ]\n                    },\n                    \"@type\": \"ProductOfferingTermOrCondition\",\n                    \"purposeReason\": \"legal entity\"\n                }\n            ],\n            \"productAction\": {\n                \"purpose\": \"{{purpose}}\",\n                \"@baseType\": \"ApiProductActionAdd\",\n                \"targetApplication\": {\n                    \"@type\": \"ApplicationRef\",\n                    \"id\": \"{{app_id}}\"\n                },\n                \"@type\": \"ApiProductActionAdd\"\n            },\n            \"@baseType\": \"ProductOrderItem\",\n            \"productOffering\": {\n                \"@type\": \"ProductOfferingRef\",\n                \"id\": \"{{product_offering_id}}\",\n                \"name\": \"{{product_offering_name}}\"\n            },\n            \"@type\": \"ApiProductOrderItemAdd\",\n            \"id\": \"orderItemId\"\n        }\n    ],\n    \"@baseType\": \"ProductOrder\",\n    \"@type\": \"ApiProductOrder\"\n}",
          "options": {
            "raw": {
              "language": "json"
            }
          }
        },
        "url": {
          "raw": "{{host}}/{{base_path}}/apiProductOrder",
          "host": [
            "{{host}}/{{base_path}}/apiProductOrder"
          ]
        },
        "description": "Orders the SEP CAMARA API product (product_offering_id, provided by the DT MACE team) for the Application created in the previous request (app_id), approved on behalf of the Application Owner (owner_id). approvalDate uses Postman's {{$isoTimestamp}} dynamic variable so it's always current. Response id is captured into order_id. Field names/shape match this app's TMF931Controller.cs OrderProduct method verbatim."
      },
      "response": []
    },
    {
      "name": "5) Get Product Order Status (optional)",
      "event": [
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "const json = pm.response.json();",
              "console.log('Order state: ' + json.state);",
              "pm.test('Order status request succeeded', function () {",
              "    pm.response.to.have.status(200);",
              "});"
            ]
          }
        }
      ],
      "request": {
        "method": "GET",
        "header": [
          {
            "key": "Authorization",
            "value": "Bearer {{access_token}}",
            "type": "text"
          }
        ],
        "url": {
          "raw": "{{host}}/{{base_path}}/apiProductOrder/{{order_id}}",
          "host": [
            "{{host}}/{{base_path}}/apiProductOrder/{{order_id}}"
          ]
        },
        "description": "Optional: checks the status/state of the Product Order created in the previous request (order_id). Useful to confirm the order completed (e.g. state = 'completed') before trying to use the new application's credentials."
      },
      "response": []
    }
  ],
  "variable": [
    {
      "key": "host",
      "value": "https://api-dc71.lotusflare.com",
      "type": "string",
      "description": "TMF onboarding host. Default is the staging onboarding host used by this app's TMF931Controller.cs. Change ONLY this variable to switch to production: https://sep.api.telekom.com. The path (base_path) and realm are the same across environments; only the host differs. See this app's /documentation/endpoints page for the full onboarding endpoint reference."
    },
    {
      "key": "base_path",
      "value": "tmf-api/openGatewayOperateAPIOnboardingAndOrdering/v5",
      "type": "string",
      "description": "Fixed TMF931 API base path - do not change unless the onboarding API version changes."
    },
    {
      "key": "realm",
      "value": "dtmace",
      "type": "string",
      "description": "Keycloak realm (operator name) used for the token endpoint, and for the TMF onboarding channel partner credentials. Germany = dtmace. Other countries: Austria = dtmaceaustria, Poland = dtmacepoland, Greece = dtmacegreece. See this app's /documentation/endpoints page for the full per-country realm reference."
    },
    {
      "key": "token_endpoint_description_only",
      "value": "",
      "type": "string",
      "disabled": true,
      "description": "Not used directly - kept only so the token endpoint formula is documented: {{host}}/realms/{{realm}}/protocol/openid-connect/token."
    },
    {
      "key": "cp_client_id",
      "value": "",
      "type": "string",
      "description": "REQUIRED. Your Channel Partner client ID for the TMF931 onboarding API itself (this authenticates YOU as the channel partner calling the onboarding/ordering API - it is separate from requested_client_id below, which is the client ID your new SEP CAMARA application will use for service API calls)."
    },
    {
      "key": "cp_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 used to sign the client_assertion JWT for the TMF onboarding token endpoint. Replace the placeholder - never share this key."
    },
    {
      "key": "cp_kid",
      "value": "",
      "type": "string",
      "description": "Optional key ID matching a key in the JWKS you registered for your Channel Partner client. Leave blank if you only have one key."
    },
    {
      "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": "owner_name",
      "value": "Postman Test Owner",
      "type": "string",
      "description": "Sample Application Owner name. Change to your own organization/team name if you want a recognizable owner in the TMF platform."
    },
    {
      "key": "app_name",
      "value": "Postman Test App",
      "type": "string",
      "description": "Sample Application name for the new TMF Application resource."
    },
    {
      "key": "requested_client_id",
      "value": "",
      "type": "string",
      "description": "REQUIRED. The client_id you want your new SEP CAMARA application to use for service API calls (KYC Match, Sim Swap, etc.) - this becomes the digitalIdentity.clientId of the created Application."
    },
    {
      "key": "redirect_url",
      "value": "https://yourapp.com/redirect",
      "type": "string",
      "description": "Your application's OAuth redirect URI, used for Authorization Code / CIBA flows on the resulting SEP CAMARA application. Point this at your own infrastructure."
    },
    {
      "key": "jwks_uri",
      "value": "https://yourapp.com/.well-known/jwks.json",
      "type": "string",
      "description": "Your application's public JWKS endpoint (HTTPS), used if the resulting SEP CAMARA application authenticates with private_key_jwt. Point this at your own infrastructure - send the URI to the DT MACE team for whitelisting."
    },
    {
      "key": "product_offering_id",
      "value": "",
      "type": "string",
      "description": "REQUIRED. The Product Offering ID for the SEP CAMARA API you were approved for - provided by the DT MACE team during onboarding, not guessable."
    },
    {
      "key": "product_offering_name",
      "value": "offering-name",
      "type": "string",
      "description": "Sample product offering name (matches this app's TMF931Controller.cs default parameter value) - descriptive only, does not need to match a real catalog name."
    },
    {
      "key": "purpose",
      "value": "dpv:FraudPreventionAndDetection",
      "type": "string",
      "description": "GDPR/DPV purpose for the product order - dpv:FraudPreventionAndDetection is this app's TMF931Controller.cs default. Change to match the actual purpose of your use case if different."
    },
    {
      "key": "access_token",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the token request. Do not edit manually."
    },
    {
      "key": "owner_id",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the Create Application Owner request. Do not edit manually."
    },
    {
      "key": "app_id",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the Create Application request. Do not edit manually."
    },
    {
      "key": "order_id",
      "value": "",
      "type": "string",
      "description": "Auto-populated by the Create Product Order request. Do not edit manually."
    }
  ]
}
