{
  "info": {
    "name": "Slot Preview AI API",
    "description": "Generated from the public OpenAPI document (packages/openapi-tools, buildPostman): exactly the operations offered to integrators, and not a single internal one. The base address comes from spec.servers[0].url, not from a literal: paths carry the version prefix. To point the collection at your own stand, change base_url in the environment.\n\nHOW TO START. 1) Import the 'Slot Preview AI' environment and put a spai_test_... key into api_key. 2) Run '0. Key check'. 3) Run the 'Free' folder as a whole: it never spends tokens.\n\nTHE KEY GOES INTO THE api_key VARIABLE AND NOWHERE ELSE. There is no 'Authorization' tab on the collection or on any request (all are 'No Auth'), and that is part of the lock, not an omission: the auth helper signs the request AFTER pre-request scripts and replaces the header entirely, so a key typed into the 'Token' field is invisible to the lock by design. Authorization is a manual header, Authorization: Bearer {{api_key}}, which the lock script can read.\n\nFOLDERS ARE SPLIT BY PRICE AND BY REVERSIBILITY, NOT BY TOPIC. 'Spends tokens' and 'Destructive' are locked: with a live key (spai_live_...) the run STOPS instead of spending. It is a refusal, not a warning.\n\nRE-RUNNING THE PAID FOLDER CHARGES A SECOND TIME: a new Idempotency-Key is generated on every run.\n\nIDENTIFIERS CHAIN THEMSELVES. Lists and create endpoints store identifiers in collection variables, and the next request substitutes them. A request with nothing to substitute (the entity is not created yet) is SKIPPED by its own script, which logs which variable is empty and which request fills it, instead of a 404 that would look like a product failure.",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "event": [
    {
      "listen": "prerequest",
      "script": {
        "exec": [
          "// The client must send Idempotency-Key on POST/PATCH itself.",
          "// WARNING: a NEW key is generated on every run, so re-running the paid folder",
          "// charges tokens a SECOND time. That is by design, not an oversight: idempotency",
          "// protects against repeating ONE request, not against re-running the collection.",
          "if (['POST', 'PATCH'].includes(pm.request.method) && !pm.request.headers.has('Idempotency-Key')) {",
          "    pm.request.headers.add({ key: 'Idempotency-Key', value: pm.variables.replaceIn('{{$guid}}') });",
          "}"
        ],
        "type": "text/javascript"
      }
    }
  ],
  "variable": [
    {
      "key": "archive_kind",
      "value": ""
    },
    {
      "key": "batch_id",
      "value": ""
    },
    {
      "key": "export_job_id",
      "value": ""
    },
    {
      "key": "export_set_id",
      "value": ""
    },
    {
      "key": "file_filename",
      "value": ""
    },
    {
      "key": "format",
      "value": ""
    },
    {
      "key": "game_id",
      "value": ""
    },
    {
      "key": "master_id",
      "value": ""
    },
    {
      "key": "master_job_id",
      "value": ""
    },
    {
      "key": "webhook_delivery_id",
      "value": ""
    },
    {
      "key": "webhook_endpoint_id",
      "value": ""
    }
  ],
  "item": [
    {
      "name": "0. Key check",
      "description": "The first item of the collection: without it an empty api_key produces forty red rows in a row and looks like 'nothing works'. The endpoint is free and answers 200 for any valid key. It also stores the organisation id in the organization_id variable.",
      "item": [
        {
          "name": "GET /v1/me (key check)",
          "description": "Check the API key\n\nThe first call of any integration. Tells you which organization the key belongs to, the plan, the token balance, the scopes the key was issued with, and — most importantly — its `environment`: `test` keys never touch a paid provider, `live` keys spend real money. There is no other programmatic way to tell a `spai_test_...` key from a `spai_live_...` one. A key with a daily token cap also gets `daily_token_cap`, `daily_tokens_used` and `daily_cap_resets_at`: the cap counts only charges, per UTC day, and a refund gives the tokens back to the day of the charge. Requires the `read` scope. This call never spends tokens.\n\nPrice: 0 tokens (the endpoint declares it with the X-Tokens-Charged header).",
          "request": {
            "method": "GET",
            "auth": {
              "type": "noauth"
            },
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/me",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "me"
              ]
            }
          },
          "response": [],
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "// The first item of the collection is the key check. Without it an empty api_key",
                  "// produces forty red rows in a row and the owner decides that nothing works.",
                  "// THE KEY IS LOOKED UP WHEREVER POSTMAN WOULD SUBSTITUTE IT FROM, NOT IN TWO SCOPES OUT OF FIVE.",
                  "// {{api_key}} resolves along the chain local > data > environment > collection > global.",
                  "// A lock that read only environment and collectionVariables stayed silent on a live key",
                  "// placed in Globals with no environment selected: 34 paid requests went out",
                  "// with Authorization: Bearer spai_live_... (acceptance run, 30.08).",
                  "const spaiKeyCandidates = [",
                  "    pm.variables.get('api_key'),",
                  "    (typeof pm.iterationData !== 'undefined' && pm.iterationData && pm.iterationData.get) ? pm.iterationData.get('api_key') : undefined,",
                  "    pm.environment.get('api_key'),",
                  "    pm.collectionVariables.get('api_key'),",
                  "    (typeof pm.globals !== 'undefined' && pm.globals && pm.globals.get) ? pm.globals.get('api_key') : undefined,",
                  "    pm.variables.replaceIn('{{api_key}}'),",
                  "    // The last candidate is the ASSEMBLED request header. It catches a key typed",
                  "    // literally into the header instead of the variable.",
                  "    // WHAT IT DID NOT CATCH, AND WHY THERE IS NO PROMISE OF 'no sixth scope' HERE ANY MORE:",
                  "    // while the collection had an auth helper, its signature ran AFTER this script and",
                  "    // replaced the header entirely: the script read the template, the wire carried the",
                  "    // helper token. Measured with real Newman: 22-27 paid requests went out with",
                  "    // Bearer spai_live_... while the lock stayed silent. That class is closed not here",
                  "    // but by having no auth helper on the collection or any request (auth: noauth).",
                  "    (pm.request && pm.request.headers && pm.request.headers.get)",
                  "        ? String(pm.variables.replaceIn(String(pm.request.headers.get('Authorization') || ''))).replace(/^Bearer\\s+/i, '')",
                  "        : '',",
                  "].map(function (v) { return (v === undefined || v === null) ? '' : String(v); })",
                  "  // Nested substitution: api_key = {{my_real_key}} is a second pass through the same resolver.",
                  "  .map(function (v) { return v.indexOf('{{') === -1 ? v : String(pm.variables.replaceIn(v)); })",
                  "  // Surrounding whitespace is not part of a key, and 'spai_live_...' with a leading",
                  "  // space would slip past a prefix comparison.",
                  "  .map(function (v) { return v.trim(); });",
                  "// An unresolved placeholder ('{{api_key}}') is not a key.",
                  "const spaiKeys = spaiKeyCandidates.filter(function (v) { return v !== '' && v.indexOf('{{') !== 0; });",
                  "const key = spaiKeys.length > 0 ? spaiKeys[0] : '';",
                  "// Fail-closed three times over: a key found in ANY scope (not only the one that won",
                  "// the chain) counts as live; case is ignored; whitespace is trimmed.",
                  "// A spurious refusal is loud, a spurious spend is silent.",
                  "const keyIsLive = spaiKeys.some(function (v) { return v.toLowerCase().indexOf('spai_live_') === 0; });",
                  "pm.test('api_key is set', function () {",
                  "    pm.expect(key, 'the api_key variable is empty. Put a spai_test_... key into the \\'Slot Preview AI\\' environment.').to.not.eql('');",
                  "});",
                  "pm.test('the key answers 200 on GET /v1/me', function () {",
                  "    pm.response.to.have.status(200);",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ]
        }
      ]
    },
    {
      "name": "1. Free (spai_test_... key): 35",
      "description": "GET requests and endpoints that declared themselves free with X-Tokens-Charged: 0 and do not start paid background work. The whole folder runs without spending tokens.\n\nThe header is set by the guard BEFORE the handler: it is a declaration, not proof. Proof of no spend is a zero delta in provider_call before and after the run.\n\nWHAT COUNTS AS GREEN HERE. A response below 400 for everything except stub endpoints: those have no documented successful response, and 501 is their contract until implemented. A request with nothing to substitute into the address is skipped and says so in the console.",
      "event": [
        {
          "listen": "test",
          "script": {
            "exec": [
              "// The free folder asserts that nothing was charged.",
              "// A BOUNDARY WORTH KNOWING: the X-Tokens-Charged header is set by the guard BEFORE",
              "// the handler, so it is a DECLARATION, not proof. Proof of no spend is a zero",
              "// delta in the paid-call ledger:",
              "//   SELECT count(*), COALESCE(SUM(cost_usd_micros),0) FROM provider_call",
              "// before and after the run.",
              "if (pm.response.headers.has('X-Tokens-Charged')) {",
              "    pm.test('X-Tokens-Charged: 0 (a declaration, not proof)', function () {",
              "        pm.expect(pm.response.headers.get('X-Tokens-Charged')).to.eql('0');",
              "    });",
              "}"
            ],
            "type": "text/javascript"
          }
        }
      ],
      "item": [
        {
          "name": "GET /v1/backgrounds",
          "description": "List backgrounds\n\nThe allowed values for `art.background`, which is required on POST /v1/masters and POST /v1/batches. Returned in canonical display order. Requires the `read` scope. This call never spends tokens.\n\nPrice: 0 tokens (the endpoint declares it with the X-Tokens-Charged header).",
          "request": {
            "method": "GET",
            "auth": {
              "type": "noauth"
            },
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/backgrounds",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "backgrounds"
              ]
            }
          },
          "response": [],
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "if (pm.variables.get('spai_skipped') === '1') {",
                  "    // The request was not sent: nothing to assert.",
                  "} else {",
                  "    pm.test('successful response (< 400)', function () {",
                  "        pm.expect(pm.response.code).to.be.below(400);",
                  "    });",
                  "}"
                ],
                "type": "text/javascript"
              }
            }
          ]
        },
        {
          "name": "GET /v1/batches",
          "description": "List batches\n\nAll batches of your organization, newest first. Requires the `read` scope. This call never spends tokens.\n\nPrice: 0 tokens (the endpoint declares it with the X-Tokens-Charged header).",
          "request": {
            "method": "GET",
            "auth": {
              "type": "noauth"
            },
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/batches",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "batches"
              ]
            }
          },
          "response": [],
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "if (pm.variables.get('spai_skipped') === '1') {",
                  "    // The request was not sent: nothing to assert.",
                  "} else {",
                  "    pm.test('successful response (< 400)', function () {",
                  "        pm.expect(pm.response.code).to.be.below(400);",
                  "    });",
                  "}",
                  "// Identifiers are chained through variables: the next request substitutes",
                  "// them itself, nobody copies them by hand.",
                  "if (pm.response.code < 300) {",
                  "    let body = null;",
                  "    try { body = pm.response.json(); } catch (e) { body = null; }",
                  "    const dig = function (node, segments) {",
                  "        for (const segment of segments) {",
                  "            if (node === null || node === undefined) return undefined;",
                  "            node = Array.isArray(node) ? node[Number(segment)] : node[segment];",
                  "        }",
                  "        return node;",
                  "    };",
                  "    // List shapes differ between endpoints (data, items, packs), so the FIRST",
                  "    // array of objects in the body is taken rather than a known field name:",
                  "    // knowing the field here would be one more copy of the contract.",
                  "    //",
                  "    // AN ELEMENT STILL IN PROGRESS OR FAILED IS NOT TAKEN. A master with status pending",
                  "    // is an ordered but not yet drawn picture: the next request (composition,",
                  "    // export estimate) would answer 404/400 for it, and that would look like a",
                  "    // product failure. The status list is closed; an unknown status counts as",
                  "    // ready, because the cost of that mistake is a red row, not a lost chain.",
                  "    const NOT_READY = ['pending', 'queued', 'running', 'processing', 'failed', 'error', 'cancelled', 'canceled'];",
                  "    const pickReady = function (arr) {",
                  "        const ready = arr.filter(function (e) { return !e || typeof e.status !== 'string' || NOT_READY.indexOf(e.status) === -1; });",
                  "        return ready.length > 0 ? ready[0] : undefined;",
                  "    };",
                  "    const firstList = function (node) {",
                  "        if (Array.isArray(node)) return node.length > 0 && typeof node[0] === 'object' ? pickReady(node) : undefined;",
                  "        if (!node || typeof node !== 'object') return undefined;",
                  "        for (const value of Object.values(node)) {",
                  "            if (!Array.isArray(value) || value.length === 0 || typeof value[0] !== 'object') continue;",
                  "            return pickReady(value);",
                  "        }",
                  "        return undefined;",
                  "    };",
                  "    const stores = [\n    { var: \"batch_id\", list: \"id\" },\n    { var: \"batch_id\", list: \"id\" },\n    { var: \"batch_id\", list: \"id\" },\n    { var: \"batch_id\", list: \"id\" },\n    { var: \"batch_id\", list: \"id\" },\n    { var: \"batch_id\", list: \"id\" },\n    { var: \"batch_id\", list: \"id\" },\n    { var: \"batch_id\", list: \"id\" },\n    ];",
                  "    for (const store of stores) {",
                  "        const source = store.list ? firstList(store.in ? dig(body, store.in) : body) : body;",
                  "        const value = store.list ? (source ? source[store.list] : undefined) : dig(source, store.path);",
                  "        // An empty value does NOT overwrite: a list may be empty while the variable",
                  "        // was filled earlier and is still valid.",
                  "        if (value !== undefined && value !== null && String(value) !== '') pm.collectionVariables.set(store.var, String(value));",
                  "    }",
                  "}"
                ],
                "type": "text/javascript"
              }
            }
          ]
        },
        {
          "name": "GET /v1/batches/{id}",
          "description": "Get one batch\n\nState of the batch together with its items: what is ready, what failed to match the catalogue, how much is still held and how much has been returned. Poll this to follow a batch, or subscribe to webhooks instead. LARGE BATCHES: page the items. Pass any of `items_limit` (1–100, default 50), `items_after` (the `items_next_after` of the previous page) or `items_status` (comma-separated item statuses) and the response carries one page of items in `index` order, `items_next_after` and `items_counts` (items per status across the whole batch). Only paged items carry `master_id`, `render_url`, `download_url` and `tokens_refunded`. Without any `items_*` parameter the response is unchanged: every item, no page fields. Requires the `read` scope. This call never spends tokens.\n\nPrice: 0 tokens (the endpoint declares it with the X-Tokens-Charged header).\n\nPATH VARIABLES: {{batch_id}} <- GET /v1/batches (first element of the list) or POST /v1/batches (the created entity)",
          "request": {
            "method": "GET",
            "auth": {
              "type": "noauth"
            },
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/batches/{{batch_id}}",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "batches",
                "{{batch_id}}"
              ],
              "query": [
                {
                  "key": "items_limit",
                  "value": "",
                  "disabled": true,
                  "description": ""
                },
                {
                  "key": "items_after",
                  "value": "",
                  "disabled": true,
                  "description": ""
                },
                {
                  "key": "items_status",
                  "value": "",
                  "disabled": true,
                  "description": ""
                }
              ]
            }
          },
          "response": [],
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "exec": [
                  "// A SKIP INSTEAD OF A RED ROW. Some entities do not exist yet on a fresh stand:",
                  "// they are created by the paid folder, and the free folder runs first.",
                  "// A request with an empty variable in the address would answer 404 or 400,",
                  "// i.e. it would look like a product failure rather than an unfilled chain.",
                  "const need = [{\"name\":\"batch_id\",\"from\":\"GET /v1/batches (first element of the list) or POST /v1/batches (the created entity)\"}];",
                  "const missing = need.filter(function (v) { return String(pm.variables.get(v.name) || '').trim() === ''; });",
                  "pm.variables.set('spai_skipped', missing.length > 0 ? '1' : '');",
                  "if (missing.length > 0) {",
                  "    console.log('SKIP ' + pm.info.requestName + ': not filled: ' + missing.map(function (v) { return v.name + ' <- ' + v.from; }).join('; '));",
                  "    if (typeof pm.execution !== 'undefined' && pm.execution.skipRequest) { pm.execution.skipRequest(); }",
                  "}"
                ],
                "type": "text/javascript"
              }
            },
            {
              "listen": "test",
              "script": {
                "exec": [
                  "if (pm.variables.get('spai_skipped') === '1') {",
                  "    // The request was not sent: nothing to assert.",
                  "} else {",
                  "    pm.test('successful response (< 400)', function () {",
                  "        pm.expect(pm.response.code).to.be.below(400);",
                  "    });",
                  "}"
                ],
                "type": "text/javascript"
              }
            }
          ]
        },
        {
          "name": "GET /v1/batches/{id}/archives/{kind}/download",
          "description": "Download a batch ZIP from this API\n\nThe ready archive of the current content (`kind`: `sets` or `masters`) as a binary stream with `Content-Disposition: attachment`, served by this API instead of the storage host. 404 until POST /v1/batches/{id}/archives reports it ready, and for an archive of content that has changed since. Free, like every download. Requires the `read` scope. This call never spends tokens.\n\nPrice: 0 tokens (the endpoint declares it with the X-Tokens-Charged header).\n\nPATH VARIABLES: {{batch_id}} <- GET /v1/batches (first element of the list) or POST /v1/batches (the created entity); {{archive_kind}} <- POST /v1/batches/{id}/archives (the created entity)",
          "request": {
            "method": "GET",
            "auth": {
              "type": "noauth"
            },
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/batches/{{batch_id}}/archives/{{archive_kind}}/download",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "batches",
                "{{batch_id}}",
                "archives",
                "{{archive_kind}}",
                "download"
              ]
            }
          },
          "response": [],
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "exec": [
                  "// A SKIP INSTEAD OF A RED ROW. Some entities do not exist yet on a fresh stand:",
                  "// they are created by the paid folder, and the free folder runs first.",
                  "// A request with an empty variable in the address would answer 404 or 400,",
                  "// i.e. it would look like a product failure rather than an unfilled chain.",
                  "const need = [{\"name\":\"batch_id\",\"from\":\"GET /v1/batches (first element of the list) or POST /v1/batches (the created entity)\"},{\"name\":\"archive_kind\",\"from\":\"POST /v1/batches/{id}/archives (the created entity)\"}];",
                  "const missing = need.filter(function (v) { return String(pm.variables.get(v.name) || '').trim() === ''; });",
                  "pm.variables.set('spai_skipped', missing.length > 0 ? '1' : '');",
                  "if (missing.length > 0) {",
                  "    console.log('SKIP ' + pm.info.requestName + ': not filled: ' + missing.map(function (v) { return v.name + ' <- ' + v.from; }).join('; '));",
                  "    if (typeof pm.execution !== 'undefined' && pm.execution.skipRequest) { pm.execution.skipRequest(); }",
                  "}"
                ],
                "type": "text/javascript"
              }
            },
            {
              "listen": "test",
              "script": {
                "exec": [
                  "if (pm.variables.get('spai_skipped') === '1') {",
                  "    // The request was not sent: nothing to assert.",
                  "} else {",
                  "    pm.test('successful response (< 400)', function () {",
                  "        pm.expect(pm.response.code).to.be.below(400);",
                  "    });",
                  "}"
                ],
                "type": "text/javascript"
              }
            }
          ]
        },
        {
          "name": "GET /v1/credits",
          "description": "Get the token balance\n\n`balance` is what you own: `available` plus `reserved`. `available` is what a new paid call can spend right now, the same figure the server checks before it takes tokens. `reserved` is already set aside for work in progress (a job, an export, the unfinished part of a batch); it is not part of `available`, and it is either charged or returned to `available` when that work settles. Tokens of an expired lot (see `lots`) are counted nowhere, even before the expiry entry reaches the ledger. `spending_frozen` means new paid calls will be refused, and `next_expiry_at` is when the nearest batch of tokens expires. Check `available` before an expensive call: there is no programmatic way to top up, the balance is topped up from the web dashboard. Requires the `read` scope. This call never spends tokens.\n\nPrice: 0 tokens (the endpoint declares it with the X-Tokens-Charged header).",
          "request": {
            "method": "GET",
            "auth": {
              "type": "noauth"
            },
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/credits",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "credits"
              ]
            }
          },
          "response": [],
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "if (pm.variables.get('spai_skipped') === '1') {",
                  "    // The request was not sent: nothing to assert.",
                  "} else {",
                  "    pm.test('successful response (< 400)', function () {",
                  "        pm.expect(pm.response.code).to.be.below(400);",
                  "    });",
                  "}"
                ],
                "type": "text/javascript"
              }
            }
          ]
        },
        {
          "name": "GET /v1/credits/ledger",
          "description": "List token ledger entries\n\nAppend-only history of every token movement: purchases, grants, holds, captures, releases, adjustments and expiries. This is where you reconcile what a job actually cost — rows are never rewritten. Cursor-based: pass `limit`, then `next_cursor` as `cursor`. Filter with `entry_type` (comma-separated, closed list — see the parameter; an unknown value is a 400). Requires the `read` scope. This call never spends tokens.\n\nPrice: 0 tokens (the endpoint declares it with the X-Tokens-Charged header).",
          "request": {
            "method": "GET",
            "auth": {
              "type": "noauth"
            },
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/credits/ledger",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "credits",
                "ledger"
              ],
              "query": [
                {
                  "key": "limit",
                  "value": "",
                  "disabled": true,
                  "description": ""
                },
                {
                  "key": "cursor",
                  "value": "",
                  "disabled": true,
                  "description": ""
                },
                {
                  "key": "entry_type",
                  "value": "",
                  "disabled": true,
                  "description": "Comma-separated list of entry types to include. Allowed values: purchase, grant, hold, capture, release, adjustment, expiry. Any other value (including a different letter case) is rejected with 400 validation_failed. Empty value or empty items are ignored; omitted means all types."
                }
              ]
            }
          },
          "response": [],
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "if (pm.variables.get('spai_skipped') === '1') {",
                  "    // The request was not sent: nothing to assert.",
                  "} else {",
                  "    pm.test('successful response (< 400)', function () {",
                  "        pm.expect(pm.response.code).to.be.below(400);",
                  "    });",
                  "}"
                ],
                "type": "text/javascript"
              }
            }
          ]
        },
        {
          "name": "GET /v1/exports/jobs/{jobId}",
          "description": "Poll an export job\n\nThe polling half of POST /v1/masters/{id}/export. `progress[]` shows per-format readiness; `export_set_id` appears once the set is finished — that is your signal to call GET /v1/exports/{exportSetId} for the file URLs. Requires the `read` scope. This call never spends tokens.\n\nPrice: 0 tokens (the endpoint declares it with the X-Tokens-Charged header).\n\nPATH VARIABLES: {{export_job_id}} <- POST /v1/masters/{id}/export (job id)",
          "request": {
            "method": "GET",
            "auth": {
              "type": "noauth"
            },
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/exports/jobs/{{export_job_id}}",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "exports",
                "jobs",
                "{{export_job_id}}"
              ]
            }
          },
          "response": [],
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "exec": [
                  "// A SKIP INSTEAD OF A RED ROW. Some entities do not exist yet on a fresh stand:",
                  "// they are created by the paid folder, and the free folder runs first.",
                  "// A request with an empty variable in the address would answer 404 or 400,",
                  "// i.e. it would look like a product failure rather than an unfilled chain.",
                  "const need = [{\"name\":\"export_job_id\",\"from\":\"POST /v1/masters/{id}/export (job id)\"}];",
                  "const missing = need.filter(function (v) { return String(pm.variables.get(v.name) || '').trim() === ''; });",
                  "pm.variables.set('spai_skipped', missing.length > 0 ? '1' : '');",
                  "if (missing.length > 0) {",
                  "    console.log('SKIP ' + pm.info.requestName + ': not filled: ' + missing.map(function (v) { return v.name + ' <- ' + v.from; }).join('; '));",
                  "    if (typeof pm.execution !== 'undefined' && pm.execution.skipRequest) { pm.execution.skipRequest(); }",
                  "}"
                ],
                "type": "text/javascript"
              }
            },
            {
              "listen": "test",
              "script": {
                "exec": [
                  "if (pm.variables.get('spai_skipped') === '1') {",
                  "    // The request was not sent: nothing to assert.",
                  "} else {",
                  "    pm.test('successful response (< 400)', function () {",
                  "        pm.expect(pm.response.code).to.be.below(400);",
                  "    });",
                  "}"
                ],
                "type": "text/javascript"
              }
            }
          ]
        },
        {
          "name": "GET /v1/exports/{exportSetId}",
          "description": "Get a finished export set\n\nThe end of the chain: `files[].url` per format, `zip_url` for the whole set, plus `formats_ready`, `formats_expected` and `missing[]` if the set is partial. The URLs are pre-signed and expire, so download rather than store them — re-read this operation for fresh links, which is always free. `overlay_config_id` in the response is the caption the files are built with; `null` means art without a caption. Requires the `read` scope. This call never spends tokens.\n\nPrice: 0 tokens (the endpoint declares it with the X-Tokens-Charged header).\n\nPATH VARIABLES: {{export_set_id}} <- POST /v1/masters/{id}/export",
          "request": {
            "method": "GET",
            "auth": {
              "type": "noauth"
            },
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/exports/{{export_set_id}}",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "exports",
                "{{export_set_id}}"
              ]
            }
          },
          "response": [],
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "exec": [
                  "// A SKIP INSTEAD OF A RED ROW. Some entities do not exist yet on a fresh stand:",
                  "// they are created by the paid folder, and the free folder runs first.",
                  "// A request with an empty variable in the address would answer 404 or 400,",
                  "// i.e. it would look like a product failure rather than an unfilled chain.",
                  "const need = [{\"name\":\"export_set_id\",\"from\":\"POST /v1/masters/{id}/export\"}];",
                  "const missing = need.filter(function (v) { return String(pm.variables.get(v.name) || '').trim() === ''; });",
                  "pm.variables.set('spai_skipped', missing.length > 0 ? '1' : '');",
                  "if (missing.length > 0) {",
                  "    console.log('SKIP ' + pm.info.requestName + ': not filled: ' + missing.map(function (v) { return v.name + ' <- ' + v.from; }).join('; '));",
                  "    if (typeof pm.execution !== 'undefined' && pm.execution.skipRequest) { pm.execution.skipRequest(); }",
                  "}"
                ],
                "type": "text/javascript"
              }
            },
            {
              "listen": "test",
              "script": {
                "exec": [
                  "if (pm.variables.get('spai_skipped') === '1') {",
                  "    // The request was not sent: nothing to assert.",
                  "} else {",
                  "    pm.test('successful response (< 400)', function () {",
                  "        pm.expect(pm.response.code).to.be.below(400);",
                  "    });",
                  "}"
                ],
                "type": "text/javascript"
              }
            }
          ]
        },
        {
          "name": "GET /v1/exports/{exportSetId}/archive",
          "description": "Download the set archive from this API\n\nThe same ZIP that `zip_url` points at, served by this API instead of the storage host: a binary stream with `Content-Disposition: attachment`. Use it when a cross-origin redirect to storage is inconvenient (browsers, extensions, corporate proxies). Downloading is always free. `zip_url` stays in the response body for clients that prefer to fetch from storage directly. Requires the `read` scope. This call never spends tokens.\n\nPrice: 0 tokens (the endpoint declares it with the X-Tokens-Charged header).\n\nPATH VARIABLES: {{export_set_id}} <- POST /v1/masters/{id}/export",
          "request": {
            "method": "GET",
            "auth": {
              "type": "noauth"
            },
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/exports/{{export_set_id}}/archive",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "exports",
                "{{export_set_id}}",
                "archive"
              ]
            }
          },
          "response": [],
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "exec": [
                  "// A SKIP INSTEAD OF A RED ROW. Some entities do not exist yet on a fresh stand:",
                  "// they are created by the paid folder, and the free folder runs first.",
                  "// A request with an empty variable in the address would answer 404 or 400,",
                  "// i.e. it would look like a product failure rather than an unfilled chain.",
                  "const need = [{\"name\":\"export_set_id\",\"from\":\"POST /v1/masters/{id}/export\"}];",
                  "const missing = need.filter(function (v) { return String(pm.variables.get(v.name) || '').trim() === ''; });",
                  "pm.variables.set('spai_skipped', missing.length > 0 ? '1' : '');",
                  "if (missing.length > 0) {",
                  "    console.log('SKIP ' + pm.info.requestName + ': not filled: ' + missing.map(function (v) { return v.name + ' <- ' + v.from; }).join('; '));",
                  "    if (typeof pm.execution !== 'undefined' && pm.execution.skipRequest) { pm.execution.skipRequest(); }",
                  "}"
                ],
                "type": "text/javascript"
              }
            },
            {
              "listen": "test",
              "script": {
                "exec": [
                  "if (pm.variables.get('spai_skipped') === '1') {",
                  "    // The request was not sent: nothing to assert.",
                  "} else {",
                  "    pm.test('successful response (< 400)', function () {",
                  "        pm.expect(pm.response.code).to.be.below(400);",
                  "    });",
                  "}"
                ],
                "type": "text/javascript"
              }
            }
          ]
        },
        {
          "name": "GET /v1/exports/{exportSetId}/files/{filename}",
          "description": "Download one file of the set from this API\n\nOne file of the set by the `filename` this API gave it — a format frame (`tile-3x4.png`) or an operator size (`tile-3x4-427x575.png`). A binary stream with `Content-Disposition: attachment`, served by this API rather than by the storage host. Free, like every download. The pre-signed `files[].url` and `files[].sizes[].url` stay in the response body. Requires the `read` scope. This call never spends tokens.\n\nPrice: 0 tokens (the endpoint declares it with the X-Tokens-Charged header).\n\nPATH VARIABLES: {{export_set_id}} <- POST /v1/masters/{id}/export; {{file_filename}} <- fill in manually",
          "request": {
            "method": "GET",
            "auth": {
              "type": "noauth"
            },
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/exports/{{export_set_id}}/files/{{file_filename}}",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "exports",
                "{{export_set_id}}",
                "files",
                "{{file_filename}}"
              ]
            }
          },
          "response": [],
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "exec": [
                  "// A SKIP INSTEAD OF A RED ROW. Some entities do not exist yet on a fresh stand:",
                  "// they are created by the paid folder, and the free folder runs first.",
                  "// A request with an empty variable in the address would answer 404 or 400,",
                  "// i.e. it would look like a product failure rather than an unfilled chain.",
                  "const need = [{\"name\":\"export_set_id\",\"from\":\"POST /v1/masters/{id}/export\"},{\"name\":\"file_filename\",\"from\":\"fill in manually in the environment\"}];",
                  "const missing = need.filter(function (v) { return String(pm.variables.get(v.name) || '').trim() === ''; });",
                  "pm.variables.set('spai_skipped', missing.length > 0 ? '1' : '');",
                  "if (missing.length > 0) {",
                  "    console.log('SKIP ' + pm.info.requestName + ': not filled: ' + missing.map(function (v) { return v.name + ' <- ' + v.from; }).join('; '));",
                  "    if (typeof pm.execution !== 'undefined' && pm.execution.skipRequest) { pm.execution.skipRequest(); }",
                  "}"
                ],
                "type": "text/javascript"
              }
            },
            {
              "listen": "test",
              "script": {
                "exec": [
                  "if (pm.variables.get('spai_skipped') === '1') {",
                  "    // The request was not sent: nothing to assert.",
                  "} else {",
                  "    pm.test('successful response (< 400)', function () {",
                  "        pm.expect(pm.response.code).to.be.below(400);",
                  "    });",
                  "}"
                ],
                "type": "text/javascript"
              }
            }
          ]
        },
        {
          "name": "GET /v1/exports/{exportSetId}/overlay-history",
          "description": "List the caption builds of an export set\n\nEvery build of the set, newest first: which `overlay_config_id` was asked for (`null` = art without a caption), when it was requested and published, and how the attempt ended. To return the set to any earlier caption, call POST /v1/masters/{id}/export with that `overlay_config_id` (or `none` for an entry with `null`): the rebuild is free and the files come back byte for byte. Requires the `read` scope. This call never spends tokens.\n\nPrice: 0 tokens (the endpoint declares it with the X-Tokens-Charged header).\n\nPATH VARIABLES: {{export_set_id}} <- POST /v1/masters/{id}/export",
          "request": {
            "method": "GET",
            "auth": {
              "type": "noauth"
            },
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/exports/{{export_set_id}}/overlay-history",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "exports",
                "{{export_set_id}}",
                "overlay-history"
              ],
              "query": [
                {
                  "key": "limit",
                  "value": "",
                  "disabled": true,
                  "description": ""
                },
                {
                  "key": "offset",
                  "value": "",
                  "disabled": true,
                  "description": ""
                }
              ]
            }
          },
          "response": [],
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "exec": [
                  "// A SKIP INSTEAD OF A RED ROW. Some entities do not exist yet on a fresh stand:",
                  "// they are created by the paid folder, and the free folder runs first.",
                  "// A request with an empty variable in the address would answer 404 or 400,",
                  "// i.e. it would look like a product failure rather than an unfilled chain.",
                  "const need = [{\"name\":\"export_set_id\",\"from\":\"POST /v1/masters/{id}/export\"}];",
                  "const missing = need.filter(function (v) { return String(pm.variables.get(v.name) || '').trim() === ''; });",
                  "pm.variables.set('spai_skipped', missing.length > 0 ? '1' : '');",
                  "if (missing.length > 0) {",
                  "    console.log('SKIP ' + pm.info.requestName + ': not filled: ' + missing.map(function (v) { return v.name + ' <- ' + v.from; }).join('; '));",
                  "    if (typeof pm.execution !== 'undefined' && pm.execution.skipRequest) { pm.execution.skipRequest(); }",
                  "}"
                ],
                "type": "text/javascript"
              }
            },
            {
              "listen": "test",
              "script": {
                "exec": [
                  "if (pm.variables.get('spai_skipped') === '1') {",
                  "    // The request was not sent: nothing to assert.",
                  "} else {",
                  "    pm.test('successful response (< 400)', function () {",
                  "        pm.expect(pm.response.code).to.be.below(400);",
                  "    });",
                  "}"
                ],
                "type": "text/javascript"
              }
            }
          ]
        },
        {
          "name": "GET /v1/formats",
          "description": "List export formats\n\nWhat an export set will contain: format codes, pixel sizes and the pricing rule (extra sizes inside one aspect ratio are free, a different aspect ratio is billed as part of the set). This is also the ONLY public source of values for the required `art.composition` field: take them from the `composition` field of these formats — the server derives them from frame geometry and they must not be invented by hand. Requires the `read` scope. This call never spends tokens.\n\nPrice: 0 tokens (the endpoint declares it with the X-Tokens-Charged header).",
          "request": {
            "method": "GET",
            "auth": {
              "type": "noauth"
            },
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/formats",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "formats"
              ]
            }
          },
          "response": [],
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "if (pm.variables.get('spai_skipped') === '1') {",
                  "    // The request was not sent: nothing to assert.",
                  "} else {",
                  "    pm.test('successful response (< 400)', function () {",
                  "        pm.expect(pm.response.code).to.be.below(400);",
                  "    });",
                  "}"
                ],
                "type": "text/javascript"
              }
            }
          ]
        },
        {
          "name": "GET /v1/games",
          "description": "Search the game catalogue\n\nFinds a game to generate art for. Search by title with `q`, narrow by `provider`, `theme` or `upcoming`. Pages are cursor-based: pass `limit` (1..100), then feed `next_cursor` from the response back as `cursor`. If the game is not here yet, create it with POST /v1/games — unreleased titles are the point of this product. Requires the `read` scope. This call never spends tokens.\n\nPrice: 0 tokens (the endpoint declares it with the X-Tokens-Charged header).",
          "request": {
            "method": "GET",
            "auth": {
              "type": "noauth"
            },
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/games",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "games"
              ],
              "query": [
                {
                  "key": "q",
                  "value": "",
                  "disabled": true,
                  "description": ""
                },
                {
                  "key": "provider",
                  "value": "",
                  "disabled": true,
                  "description": ""
                },
                {
                  "key": "theme",
                  "value": "",
                  "disabled": true,
                  "description": ""
                },
                {
                  "key": "upcoming",
                  "value": "",
                  "disabled": true,
                  "description": ""
                },
                {
                  "key": "limit",
                  "value": "",
                  "disabled": true,
                  "description": ""
                },
                {
                  "key": "cursor",
                  "value": "",
                  "disabled": true,
                  "description": ""
                }
              ]
            }
          },
          "response": [],
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "if (pm.variables.get('spai_skipped') === '1') {",
                  "    // The request was not sent: nothing to assert.",
                  "} else {",
                  "    pm.test('successful response (< 400)', function () {",
                  "        pm.expect(pm.response.code).to.be.below(400);",
                  "    });",
                  "}",
                  "// Identifiers are chained through variables: the next request substitutes",
                  "// them itself, nobody copies them by hand.",
                  "if (pm.response.code < 300) {",
                  "    let body = null;",
                  "    try { body = pm.response.json(); } catch (e) { body = null; }",
                  "    const dig = function (node, segments) {",
                  "        for (const segment of segments) {",
                  "            if (node === null || node === undefined) return undefined;",
                  "            node = Array.isArray(node) ? node[Number(segment)] : node[segment];",
                  "        }",
                  "        return node;",
                  "    };",
                  "    // List shapes differ between endpoints (data, items, packs), so the FIRST",
                  "    // array of objects in the body is taken rather than a known field name:",
                  "    // knowing the field here would be one more copy of the contract.",
                  "    //",
                  "    // AN ELEMENT STILL IN PROGRESS OR FAILED IS NOT TAKEN. A master with status pending",
                  "    // is an ordered but not yet drawn picture: the next request (composition,",
                  "    // export estimate) would answer 404/400 for it, and that would look like a",
                  "    // product failure. The status list is closed; an unknown status counts as",
                  "    // ready, because the cost of that mistake is a red row, not a lost chain.",
                  "    const NOT_READY = ['pending', 'queued', 'running', 'processing', 'failed', 'error', 'cancelled', 'canceled'];",
                  "    const pickReady = function (arr) {",
                  "        const ready = arr.filter(function (e) { return !e || typeof e.status !== 'string' || NOT_READY.indexOf(e.status) === -1; });",
                  "        return ready.length > 0 ? ready[0] : undefined;",
                  "    };",
                  "    const firstList = function (node) {",
                  "        if (Array.isArray(node)) return node.length > 0 && typeof node[0] === 'object' ? pickReady(node) : undefined;",
                  "        if (!node || typeof node !== 'object') return undefined;",
                  "        for (const value of Object.values(node)) {",
                  "            if (!Array.isArray(value) || value.length === 0 || typeof value[0] !== 'object') continue;",
                  "            return pickReady(value);",
                  "        }",
                  "        return undefined;",
                  "    };",
                  "    const stores = [\n    { var: \"game_id\", list: \"id\" },\n    ];",
                  "    for (const store of stores) {",
                  "        const source = store.list ? firstList(store.in ? dig(body, store.in) : body) : body;",
                  "        const value = store.list ? (source ? source[store.list] : undefined) : dig(source, store.path);",
                  "        // An empty value does NOT overwrite: a list may be empty while the variable",
                  "        // was filled earlier and is still valid.",
                  "        if (value !== undefined && value !== null && String(value) !== '') pm.collectionVariables.set(store.var, String(value));",
                  "    }",
                  "}"
                ],
                "type": "text/javascript"
              }
            }
          ]
        },
        {
          "name": "GET /v1/games/{id}",
          "description": "Get one game\n\nEverything the generator knows about a title: `art_brief` (theme, palette, motifs), `has_reference` and `brief_confidence`. Read `has_reference` before you set `use_game_reference` on POST /v1/masters: with no reference on file the flag changes nothing. Requires the `read` scope. This call never spends tokens.\n\nPrice: 0 tokens (the endpoint declares it with the X-Tokens-Charged header).\n\nPATH VARIABLES: {{game_id}} <- GET /v1/games (first element of the list) or POST /v1/games (the created entity)",
          "request": {
            "method": "GET",
            "auth": {
              "type": "noauth"
            },
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/games/{{game_id}}",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "games",
                "{{game_id}}"
              ]
            }
          },
          "response": [],
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "exec": [
                  "// A SKIP INSTEAD OF A RED ROW. Some entities do not exist yet on a fresh stand:",
                  "// they are created by the paid folder, and the free folder runs first.",
                  "// A request with an empty variable in the address would answer 404 or 400,",
                  "// i.e. it would look like a product failure rather than an unfilled chain.",
                  "const need = [{\"name\":\"game_id\",\"from\":\"GET /v1/games (first element of the list) or POST /v1/games (the created entity)\"}];",
                  "const missing = need.filter(function (v) { return String(pm.variables.get(v.name) || '').trim() === ''; });",
                  "pm.variables.set('spai_skipped', missing.length > 0 ? '1' : '');",
                  "if (missing.length > 0) {",
                  "    console.log('SKIP ' + pm.info.requestName + ': not filled: ' + missing.map(function (v) { return v.name + ' <- ' + v.from; }).join('; '));",
                  "    if (typeof pm.execution !== 'undefined' && pm.execution.skipRequest) { pm.execution.skipRequest(); }",
                  "}"
                ],
                "type": "text/javascript"
              }
            },
            {
              "listen": "test",
              "script": {
                "exec": [
                  "if (pm.variables.get('spai_skipped') === '1') {",
                  "    // The request was not sent: nothing to assert.",
                  "} else {",
                  "    pm.test('successful response (< 400)', function () {",
                  "        pm.expect(pm.response.code).to.be.below(400);",
                  "    });",
                  "}"
                ],
                "type": "text/javascript"
              }
            }
          ]
        },
        {
          "name": "GET /v1/masters",
          "description": "List your masters\n\nYour generation history, newest first. This is how you recover after losing a job id, and how you find a master again by the `external_id` you sent when you created it. Cursor-based: pass `limit`, then `next_cursor` as `cursor`. Filter by `game_id`, `external_id`, `exported` or `created_after`. `from_batch=true` keeps only masters made by a batch, `from_batch=false` only single works; every record names its batch in `batch_id` (the original batch, also for a retried item), or `null`. Requires the `read` scope. This call never spends tokens.\n\nPrice: 0 tokens (the endpoint declares it with the X-Tokens-Charged header).",
          "request": {
            "method": "GET",
            "auth": {
              "type": "noauth"
            },
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/masters",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "masters"
              ],
              "query": [
                {
                  "key": "game_id",
                  "value": "",
                  "disabled": true,
                  "description": ""
                },
                {
                  "key": "external_id",
                  "value": "",
                  "disabled": true,
                  "description": ""
                },
                {
                  "key": "exported",
                  "value": "",
                  "disabled": true,
                  "description": ""
                },
                {
                  "key": "created_after",
                  "value": "",
                  "disabled": true,
                  "description": ""
                },
                {
                  "key": "limit",
                  "value": "",
                  "disabled": true,
                  "description": ""
                },
                {
                  "key": "cursor",
                  "value": "",
                  "disabled": true,
                  "description": ""
                },
                {
                  "key": "from_batch",
                  "value": "",
                  "disabled": true,
                  "description": ""
                }
              ]
            }
          },
          "response": [],
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "if (pm.variables.get('spai_skipped') === '1') {",
                  "    // The request was not sent: nothing to assert.",
                  "} else {",
                  "    pm.test('successful response (< 400)', function () {",
                  "        pm.expect(pm.response.code).to.be.below(400);",
                  "    });",
                  "}",
                  "// Identifiers are chained through variables: the next request substitutes",
                  "// them itself, nobody copies them by hand.",
                  "if (pm.response.code < 300) {",
                  "    let body = null;",
                  "    try { body = pm.response.json(); } catch (e) { body = null; }",
                  "    const dig = function (node, segments) {",
                  "        for (const segment of segments) {",
                  "            if (node === null || node === undefined) return undefined;",
                  "            node = Array.isArray(node) ? node[Number(segment)] : node[segment];",
                  "        }",
                  "        return node;",
                  "    };",
                  "    // List shapes differ between endpoints (data, items, packs), so the FIRST",
                  "    // array of objects in the body is taken rather than a known field name:",
                  "    // knowing the field here would be one more copy of the contract.",
                  "    //",
                  "    // AN ELEMENT STILL IN PROGRESS OR FAILED IS NOT TAKEN. A master with status pending",
                  "    // is an ordered but not yet drawn picture: the next request (composition,",
                  "    // export estimate) would answer 404/400 for it, and that would look like a",
                  "    // product failure. The status list is closed; an unknown status counts as",
                  "    // ready, because the cost of that mistake is a red row, not a lost chain.",
                  "    const NOT_READY = ['pending', 'queued', 'running', 'processing', 'failed', 'error', 'cancelled', 'canceled'];",
                  "    const pickReady = function (arr) {",
                  "        const ready = arr.filter(function (e) { return !e || typeof e.status !== 'string' || NOT_READY.indexOf(e.status) === -1; });",
                  "        return ready.length > 0 ? ready[0] : undefined;",
                  "    };",
                  "    const firstList = function (node) {",
                  "        if (Array.isArray(node)) return node.length > 0 && typeof node[0] === 'object' ? pickReady(node) : undefined;",
                  "        if (!node || typeof node !== 'object') return undefined;",
                  "        for (const value of Object.values(node)) {",
                  "            if (!Array.isArray(value) || value.length === 0 || typeof value[0] !== 'object') continue;",
                  "            return pickReady(value);",
                  "        }",
                  "        return undefined;",
                  "    };",
                  "    const stores = [\n    { var: \"master_id\", list: \"id\" },\n    { var: \"master_id\", list: \"id\" },\n    { var: \"master_id\", list: \"id\" },\n    { var: \"master_id\", list: \"id\" },\n    ];",
                  "    for (const store of stores) {",
                  "        const source = store.list ? firstList(store.in ? dig(body, store.in) : body) : body;",
                  "        const value = store.list ? (source ? source[store.list] : undefined) : dig(source, store.path);",
                  "        // An empty value does NOT overwrite: a list may be empty while the variable",
                  "        // was filled earlier and is still valid.",
                  "        if (value !== undefined && value !== null && String(value) !== '') pm.collectionVariables.set(store.var, String(value));",
                  "    }",
                  "}"
                ],
                "type": "text/javascript"
              }
            }
          ]
        },
        {
          "name": "GET /v1/masters/jobs/{id}",
          "description": "Poll a generation job\n\nThe polling half of POST /v1/masters. Call the `polling_url` you were given until `status` is `succeeded` (masters are in `masters[]`, each with its id for export), `failed` or `cancelled`. Poll no more often than every few seconds; a webhook subscription (POST /v1/webhook-endpoints) removes the need to poll at all. Requires the `read` scope. This call never spends tokens.\n\nPrice: 0 tokens (the endpoint declares it with the X-Tokens-Charged header).\n\nPATH VARIABLES: {{master_job_id}} <- POST /v1/masters (job id)",
          "request": {
            "method": "GET",
            "auth": {
              "type": "noauth"
            },
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/masters/jobs/{{master_job_id}}",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "masters",
                "jobs",
                "{{master_job_id}}"
              ]
            }
          },
          "response": [],
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "exec": [
                  "// A SKIP INSTEAD OF A RED ROW. Some entities do not exist yet on a fresh stand:",
                  "// they are created by the paid folder, and the free folder runs first.",
                  "// A request with an empty variable in the address would answer 404 or 400,",
                  "// i.e. it would look like a product failure rather than an unfilled chain.",
                  "const need = [{\"name\":\"master_job_id\",\"from\":\"POST /v1/masters (job id)\"}];",
                  "const missing = need.filter(function (v) { return String(pm.variables.get(v.name) || '').trim() === ''; });",
                  "pm.variables.set('spai_skipped', missing.length > 0 ? '1' : '');",
                  "if (missing.length > 0) {",
                  "    console.log('SKIP ' + pm.info.requestName + ': not filled: ' + missing.map(function (v) { return v.name + ' <- ' + v.from; }).join('; '));",
                  "    if (typeof pm.execution !== 'undefined' && pm.execution.skipRequest) { pm.execution.skipRequest(); }",
                  "}"
                ],
                "type": "text/javascript"
              }
            },
            {
              "listen": "test",
              "script": {
                "exec": [
                  "if (pm.variables.get('spai_skipped') === '1') {",
                  "    // The request was not sent: nothing to assert.",
                  "} else {",
                  "    pm.test('successful response (< 400)', function () {",
                  "        pm.expect(pm.response.code).to.be.below(400);",
                  "    });",
                  "}"
                ],
                "type": "text/javascript"
              }
            }
          ]
        },
        {
          "name": "GET /v1/masters/{id}",
          "description": "Get one master\n\nOne record of your generation history by master id, in exactly the shape of a row of GET /v1/masters: game, art parameters, set state and, for a ready master, `render_url` and `download_url`. A master of another organisation, a set file and an id that does not exist all answer the same 404. Requires the `read` scope. This call never spends tokens.\n\nPrice: 0 tokens (the endpoint declares it with the X-Tokens-Charged header).\n\nPATH VARIABLES: {{master_id}} <- POST /v1/masters (first ready master of the job)",
          "request": {
            "method": "GET",
            "auth": {
              "type": "noauth"
            },
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/masters/{{master_id}}",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "masters",
                "{{master_id}}"
              ]
            }
          },
          "response": [],
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "exec": [
                  "// A SKIP INSTEAD OF A RED ROW. Some entities do not exist yet on a fresh stand:",
                  "// they are created by the paid folder, and the free folder runs first.",
                  "// A request with an empty variable in the address would answer 404 or 400,",
                  "// i.e. it would look like a product failure rather than an unfilled chain.",
                  "const need = [{\"name\":\"master_id\",\"from\":\"POST /v1/masters (first ready master of the job)\"}];",
                  "const missing = need.filter(function (v) { return String(pm.variables.get(v.name) || '').trim() === ''; });",
                  "pm.variables.set('spai_skipped', missing.length > 0 ? '1' : '');",
                  "if (missing.length > 0) {",
                  "    console.log('SKIP ' + pm.info.requestName + ': not filled: ' + missing.map(function (v) { return v.name + ' <- ' + v.from; }).join('; '));",
                  "    if (typeof pm.execution !== 'undefined' && pm.execution.skipRequest) { pm.execution.skipRequest(); }",
                  "}"
                ],
                "type": "text/javascript"
              }
            },
            {
              "listen": "test",
              "script": {
                "exec": [
                  "if (pm.variables.get('spai_skipped') === '1') {",
                  "    // The request was not sent: nothing to assert.",
                  "} else {",
                  "    pm.test('successful response (< 400)', function () {",
                  "        pm.expect(pm.response.code).to.be.below(400);",
                  "    });",
                  "}"
                ],
                "type": "text/javascript"
              }
            }
          ]
        },
        {
          "name": "GET /v1/masters/{id}/download",
          "description": "Download the master image from this API\n\nThe raw art of a ready master as a binary stream with `Content-Disposition: attachment`, served by this API instead of the storage host. The file is named after the game and the studio (`<game>_<studio>_master.png`). Downloading is always free. `download_url` stays in the master body for clients that prefer to fetch from storage directly. Requires the `read` scope. This call never spends tokens.\n\nPrice: 0 tokens (the endpoint declares it with the X-Tokens-Charged header).\n\nPATH VARIABLES: {{master_id}} <- POST /v1/masters (first ready master of the job)",
          "request": {
            "method": "GET",
            "auth": {
              "type": "noauth"
            },
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/masters/{{master_id}}/download",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "masters",
                "{{master_id}}",
                "download"
              ]
            }
          },
          "response": [],
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "exec": [
                  "// A SKIP INSTEAD OF A RED ROW. Some entities do not exist yet on a fresh stand:",
                  "// they are created by the paid folder, and the free folder runs first.",
                  "// A request with an empty variable in the address would answer 404 or 400,",
                  "// i.e. it would look like a product failure rather than an unfilled chain.",
                  "const need = [{\"name\":\"master_id\",\"from\":\"POST /v1/masters (first ready master of the job)\"}];",
                  "const missing = need.filter(function (v) { return String(pm.variables.get(v.name) || '').trim() === ''; });",
                  "pm.variables.set('spai_skipped', missing.length > 0 ? '1' : '');",
                  "if (missing.length > 0) {",
                  "    console.log('SKIP ' + pm.info.requestName + ': not filled: ' + missing.map(function (v) { return v.name + ' <- ' + v.from; }).join('; '));",
                  "    if (typeof pm.execution !== 'undefined' && pm.execution.skipRequest) { pm.execution.skipRequest(); }",
                  "}"
                ],
                "type": "text/javascript"
              }
            },
            {
              "listen": "test",
              "script": {
                "exec": [
                  "if (pm.variables.get('spai_skipped') === '1') {",
                  "    // The request was not sent: nothing to assert.",
                  "} else {",
                  "    pm.test('successful response (< 400)', function () {",
                  "        pm.expect(pm.response.code).to.be.below(400);",
                  "    });",
                  "}"
                ],
                "type": "text/javascript"
              }
            }
          ]
        },
        {
          "name": "GET /v1/masters/{id}/export/estimate",
          "description": "Price an export before running it\n\nWhat GET-then-POST /v1/masters/{id}/export would charge for THIS master right now. Computed by the same code that the export itself uses, so the quote and the charge are one number. Zero means the set is already built and rebuilding it is free. Pricing holds nothing and creates nothing. Pass the same `overlay_config_id` (or `none`) and `formats` you are going to send to the export; `overlay_config_id` in the response is the caption the set is CURRENTLY built with, not the one you asked about. Requires the `read` scope. This call never spends tokens.\n\nPrice: 0 tokens (the endpoint declares it with the X-Tokens-Charged header).\n\nPATH VARIABLES: {{master_id}} <- POST /v1/masters (first ready master of the job)",
          "request": {
            "method": "GET",
            "auth": {
              "type": "noauth"
            },
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/masters/{{master_id}}/export/estimate",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "masters",
                "{{master_id}}",
                "export",
                "estimate"
              ],
              "query": [
                {
                  "key": "overlay_config_id",
                  "value": "",
                  "disabled": true,
                  "description": "Caption of the set. An `overlay_config_id` from POST /v1/renders applies that caption; \"none\" builds art without a caption. Omitted: a new set gets the default caption (game title and studio, as in the web app), an already built set keeps its caption unchanged. Changing the caption of a built set is a free rebuild; every build is listed by GET /v1/exports/{exportSetId}/overlay-history."
                },
                {
                  "key": "formats",
                  "value": "",
                  "disabled": true,
                  "description": ""
                }
              ]
            }
          },
          "response": [],
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "exec": [
                  "// A SKIP INSTEAD OF A RED ROW. Some entities do not exist yet on a fresh stand:",
                  "// they are created by the paid folder, and the free folder runs first.",
                  "// A request with an empty variable in the address would answer 404 or 400,",
                  "// i.e. it would look like a product failure rather than an unfilled chain.",
                  "const need = [{\"name\":\"master_id\",\"from\":\"POST /v1/masters (first ready master of the job)\"}];",
                  "const missing = need.filter(function (v) { return String(pm.variables.get(v.name) || '').trim() === ''; });",
                  "pm.variables.set('spai_skipped', missing.length > 0 ? '1' : '');",
                  "if (missing.length > 0) {",
                  "    console.log('SKIP ' + pm.info.requestName + ': not filled: ' + missing.map(function (v) { return v.name + ' <- ' + v.from; }).join('; '));",
                  "    if (typeof pm.execution !== 'undefined' && pm.execution.skipRequest) { pm.execution.skipRequest(); }",
                  "}"
                ],
                "type": "text/javascript"
              }
            },
            {
              "listen": "test",
              "script": {
                "exec": [
                  "if (pm.variables.get('spai_skipped') === '1') {",
                  "    // The request was not sent: nothing to assert.",
                  "} else {",
                  "    pm.test('successful response (< 400)', function () {",
                  "        pm.expect(pm.response.code).to.be.below(400);",
                  "    });",
                  "}"
                ],
                "type": "text/javascript"
              }
            }
          ]
        },
        {
          "name": "GET /v1/me",
          "description": "Check the API key\n\nThe first call of any integration. Tells you which organization the key belongs to, the plan, the token balance, the scopes the key was issued with, and — most importantly — its `environment`: `test` keys never touch a paid provider, `live` keys spend real money. There is no other programmatic way to tell a `spai_test_...` key from a `spai_live_...` one. A key with a daily token cap also gets `daily_token_cap`, `daily_tokens_used` and `daily_cap_resets_at`: the cap counts only charges, per UTC day, and a refund gives the tokens back to the day of the charge. Requires the `read` scope. This call never spends tokens.\n\nPrice: 0 tokens (the endpoint declares it with the X-Tokens-Charged header).",
          "request": {
            "method": "GET",
            "auth": {
              "type": "noauth"
            },
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/me",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "me"
              ]
            }
          },
          "response": [],
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "if (pm.variables.get('spai_skipped') === '1') {",
                  "    // The request was not sent: nothing to assert.",
                  "} else {",
                  "    pm.test('successful response (< 400)', function () {",
                  "        pm.expect(pm.response.code).to.be.below(400);",
                  "    });",
                  "}"
                ],
                "type": "text/javascript"
              }
            }
          ]
        },
        {
          "name": "GET /v1/providers",
          "description": "List game providers\n\nStudios that have at least one active public game. Use these spellings for the `provider` filter on GET /v1/games and for `provider_name` when you add a game or pass a game by name. Requires the `read` scope. This call never spends tokens.\n\nPrice: 0 tokens (the endpoint declares it with the X-Tokens-Charged header).",
          "request": {
            "method": "GET",
            "auth": {
              "type": "noauth"
            },
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/providers",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "providers"
              ]
            }
          },
          "response": [],
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "if (pm.variables.get('spai_skipped') === '1') {",
                  "    // The request was not sent: nothing to assert.",
                  "} else {",
                  "    pm.test('successful response (< 400)', function () {",
                  "        pm.expect(pm.response.code).to.be.below(400);",
                  "    });",
                  "}"
                ],
                "type": "text/javascript"
              }
            }
          ]
        },
        {
          "name": "GET /v1/styles",
          "description": "List art styles\n\nThe allowed values for `art.style` on POST /v1/masters and POST /v1/batches. Returned in canonical display order. Requires the `read` scope. This call never spends tokens.\n\nPrice: 0 tokens (the endpoint declares it with the X-Tokens-Charged header).",
          "request": {
            "method": "GET",
            "auth": {
              "type": "noauth"
            },
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/styles",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "styles"
              ]
            }
          },
          "response": [],
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "if (pm.variables.get('spai_skipped') === '1') {",
                  "    // The request was not sent: nothing to assert.",
                  "} else {",
                  "    pm.test('successful response (< 400)', function () {",
                  "        pm.expect(pm.response.code).to.be.below(400);",
                  "    });",
                  "}"
                ],
                "type": "text/javascript"
              }
            }
          ]
        },
        {
          "name": "GET /v1/webhook-deliveries",
          "description": "List webhook deliveries\n\nThe delivery log: which event went where, the response status and the attempt count. Filter by `endpoint_id`. Your receiver's response body is truncated to 2 KB. Use this to find deliveries your endpoint missed, then replay them with POST /v1/webhook-deliveries/{id}/retry. Requires the `read` scope. This call never spends tokens.\n\nPrice: 0 tokens (the endpoint declares it with the X-Tokens-Charged header).",
          "request": {
            "method": "GET",
            "auth": {
              "type": "noauth"
            },
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/webhook-deliveries",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "webhook-deliveries"
              ],
              "query": [
                {
                  "key": "endpoint_id",
                  "value": "",
                  "disabled": true,
                  "description": ""
                }
              ]
            }
          },
          "response": [],
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "if (pm.variables.get('spai_skipped') === '1') {",
                  "    // The request was not sent: nothing to assert.",
                  "} else {",
                  "    pm.test('successful response (< 400)', function () {",
                  "        pm.expect(pm.response.code).to.be.below(400);",
                  "    });",
                  "}",
                  "// Identifiers are chained through variables: the next request substitutes",
                  "// them itself, nobody copies them by hand.",
                  "if (pm.response.code < 300) {",
                  "    let body = null;",
                  "    try { body = pm.response.json(); } catch (e) { body = null; }",
                  "    const dig = function (node, segments) {",
                  "        for (const segment of segments) {",
                  "            if (node === null || node === undefined) return undefined;",
                  "            node = Array.isArray(node) ? node[Number(segment)] : node[segment];",
                  "        }",
                  "        return node;",
                  "    };",
                  "    // List shapes differ between endpoints (data, items, packs), so the FIRST",
                  "    // array of objects in the body is taken rather than a known field name:",
                  "    // knowing the field here would be one more copy of the contract.",
                  "    //",
                  "    // AN ELEMENT STILL IN PROGRESS OR FAILED IS NOT TAKEN. A master with status pending",
                  "    // is an ordered but not yet drawn picture: the next request (composition,",
                  "    // export estimate) would answer 404/400 for it, and that would look like a",
                  "    // product failure. The status list is closed; an unknown status counts as",
                  "    // ready, because the cost of that mistake is a red row, not a lost chain.",
                  "    const NOT_READY = ['pending', 'queued', 'running', 'processing', 'failed', 'error', 'cancelled', 'canceled'];",
                  "    const pickReady = function (arr) {",
                  "        const ready = arr.filter(function (e) { return !e || typeof e.status !== 'string' || NOT_READY.indexOf(e.status) === -1; });",
                  "        return ready.length > 0 ? ready[0] : undefined;",
                  "    };",
                  "    const firstList = function (node) {",
                  "        if (Array.isArray(node)) return node.length > 0 && typeof node[0] === 'object' ? pickReady(node) : undefined;",
                  "        if (!node || typeof node !== 'object') return undefined;",
                  "        for (const value of Object.values(node)) {",
                  "            if (!Array.isArray(value) || value.length === 0 || typeof value[0] !== 'object') continue;",
                  "            return pickReady(value);",
                  "        }",
                  "        return undefined;",
                  "    };",
                  "    const stores = [\n    { var: \"webhook_delivery_id\", list: \"id\" },\n    ];",
                  "    for (const store of stores) {",
                  "        const source = store.list ? firstList(store.in ? dig(body, store.in) : body) : body;",
                  "        const value = store.list ? (source ? source[store.list] : undefined) : dig(source, store.path);",
                  "        // An empty value does NOT overwrite: a list may be empty while the variable",
                  "        // was filled earlier and is still valid.",
                  "        if (value !== undefined && value !== null && String(value) !== '') pm.collectionVariables.set(store.var, String(value));",
                  "    }",
                  "}"
                ],
                "type": "text/javascript"
              }
            }
          ]
        },
        {
          "name": "GET /v1/webhook-endpoints",
          "description": "List webhook subscriptions\n\nYour registered endpoints and the events each one is subscribed to. Secrets are never returned here — not even hashed; the secret is shown once, when the subscription is created. Requires the `read` scope. This call never spends tokens.\n\nPrice: 0 tokens (the endpoint declares it with the X-Tokens-Charged header).",
          "request": {
            "method": "GET",
            "auth": {
              "type": "noauth"
            },
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/webhook-endpoints",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "webhook-endpoints"
              ]
            }
          },
          "response": [],
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "if (pm.variables.get('spai_skipped') === '1') {",
                  "    // The request was not sent: nothing to assert.",
                  "} else {",
                  "    pm.test('successful response (< 400)', function () {",
                  "        pm.expect(pm.response.code).to.be.below(400);",
                  "    });",
                  "}",
                  "// Identifiers are chained through variables: the next request substitutes",
                  "// them itself, nobody copies them by hand.",
                  "if (pm.response.code < 300) {",
                  "    let body = null;",
                  "    try { body = pm.response.json(); } catch (e) { body = null; }",
                  "    const dig = function (node, segments) {",
                  "        for (const segment of segments) {",
                  "            if (node === null || node === undefined) return undefined;",
                  "            node = Array.isArray(node) ? node[Number(segment)] : node[segment];",
                  "        }",
                  "        return node;",
                  "    };",
                  "    // List shapes differ between endpoints (data, items, packs), so the FIRST",
                  "    // array of objects in the body is taken rather than a known field name:",
                  "    // knowing the field here would be one more copy of the contract.",
                  "    //",
                  "    // AN ELEMENT STILL IN PROGRESS OR FAILED IS NOT TAKEN. A master with status pending",
                  "    // is an ordered but not yet drawn picture: the next request (composition,",
                  "    // export estimate) would answer 404/400 for it, and that would look like a",
                  "    // product failure. The status list is closed; an unknown status counts as",
                  "    // ready, because the cost of that mistake is a red row, not a lost chain.",
                  "    const NOT_READY = ['pending', 'queued', 'running', 'processing', 'failed', 'error', 'cancelled', 'canceled'];",
                  "    const pickReady = function (arr) {",
                  "        const ready = arr.filter(function (e) { return !e || typeof e.status !== 'string' || NOT_READY.indexOf(e.status) === -1; });",
                  "        return ready.length > 0 ? ready[0] : undefined;",
                  "    };",
                  "    const firstList = function (node) {",
                  "        if (Array.isArray(node)) return node.length > 0 && typeof node[0] === 'object' ? pickReady(node) : undefined;",
                  "        if (!node || typeof node !== 'object') return undefined;",
                  "        for (const value of Object.values(node)) {",
                  "            if (!Array.isArray(value) || value.length === 0 || typeof value[0] !== 'object') continue;",
                  "            return pickReady(value);",
                  "        }",
                  "        return undefined;",
                  "    };",
                  "    const stores = [\n    { var: \"webhook_endpoint_id\", list: \"id\" },\n    { var: \"webhook_endpoint_id\", list: \"id\" },\n    ];",
                  "    for (const store of stores) {",
                  "        const source = store.list ? firstList(store.in ? dig(body, store.in) : body) : body;",
                  "        const value = store.list ? (source ? source[store.list] : undefined) : dig(source, store.path);",
                  "        // An empty value does NOT overwrite: a list may be empty while the variable",
                  "        // was filled earlier and is still valid.",
                  "        if (value !== undefined && value !== null && String(value) !== '') pm.collectionVariables.set(store.var, String(value));",
                  "    }",
                  "}"
                ],
                "type": "text/javascript"
              }
            }
          ]
        },
        {
          "name": "POST /v1/batches/csv-preview",
          "description": "Dry-run a CSV against the catalogue\n\nMatches every row of your CSV against the game catalogue and reports what was found and what was not — BEFORE any money moves. Run this first: rows that do not match are the ones you will otherwise pay to generate wrongly, or lose from the batch. Unmatched rows are found here for free. Requires the `batch` scope. This call never spends tokens.\n\nPrice: 0 tokens (the endpoint declares it with the X-Tokens-Charged header).",
          "request": {
            "method": "POST",
            "auth": {
              "type": "noauth"
            },
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/batches/csv-preview",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "batches",
                "csv-preview"
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"csv\": \"game_name,provider,external_id\\nGates of Fortune,Pragmatic Play,row-1\\nBook of Sunrise,Pragmatic Play,row-2\\n\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [],
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "if (pm.variables.get('spai_skipped') === '1') {",
                  "    // The request was not sent: nothing to assert.",
                  "} else {",
                  "    pm.test('successful response (< 400)', function () {",
                  "        pm.expect(pm.response.code).to.be.below(400);",
                  "    });",
                  "}"
                ],
                "type": "text/javascript"
              }
            }
          ]
        },
        {
          "name": "POST /v1/batches/{id}/archives",
          "description": "Get a ZIP of the batch\n\nFree. `kind: sets` packs every built file set of the batch (the current set of each item's current master); `kind: masters` packs the raw art of every ready master, file names prefixed with the item index so two items of the same game never overwrite each other. The archive belongs to the current content: after another build the next call makes a new archive, an old link is never handed out. 202 while it is being built (call again to poll), 200 with `download_url` when ready. Over 2 GB the archive is not made: `status: omitted` with `omitted_reason: too_large`; nothing to pack gives `nothing_assembled`. The batch page (GET with `items_*`) lists the current archives in `archives`. Requires the `batch` scope. This call never spends tokens.\n\nPrice: 0 tokens (the endpoint declares it with the X-Tokens-Charged header).\n\nPATH VARIABLES: {{batch_id}} <- GET /v1/batches (first element of the list) or POST /v1/batches (the created entity)",
          "request": {
            "method": "POST",
            "auth": {
              "type": "noauth"
            },
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/batches/{{batch_id}}/archives",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "batches",
                "{{batch_id}}",
                "archives"
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"kind\": \"masters\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [],
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "exec": [
                  "// A SKIP INSTEAD OF A RED ROW. Some entities do not exist yet on a fresh stand:",
                  "// they are created by the paid folder, and the free folder runs first.",
                  "// A request with an empty variable in the address would answer 404 or 400,",
                  "// i.e. it would look like a product failure rather than an unfilled chain.",
                  "const need = [{\"name\":\"batch_id\",\"from\":\"GET /v1/batches (first element of the list) or POST /v1/batches (the created entity)\"}];",
                  "const missing = need.filter(function (v) { return String(pm.variables.get(v.name) || '').trim() === ''; });",
                  "pm.variables.set('spai_skipped', missing.length > 0 ? '1' : '');",
                  "if (missing.length > 0) {",
                  "    console.log('SKIP ' + pm.info.requestName + ': not filled: ' + missing.map(function (v) { return v.name + ' <- ' + v.from; }).join('; '));",
                  "    if (typeof pm.execution !== 'undefined' && pm.execution.skipRequest) { pm.execution.skipRequest(); }",
                  "}"
                ],
                "type": "text/javascript"
              }
            },
            {
              "listen": "test",
              "script": {
                "exec": [
                  "if (pm.variables.get('spai_skipped') === '1') {",
                  "    // The request was not sent: nothing to assert.",
                  "} else {",
                  "    pm.test('successful response (< 400)', function () {",
                  "        pm.expect(pm.response.code).to.be.below(400);",
                  "    });",
                  "}",
                  "// Identifiers are chained through variables: the next request substitutes",
                  "// them itself, nobody copies them by hand.",
                  "if (pm.response.code < 300) {",
                  "    let body = null;",
                  "    try { body = pm.response.json(); } catch (e) { body = null; }",
                  "    const dig = function (node, segments) {",
                  "        for (const segment of segments) {",
                  "            if (node === null || node === undefined) return undefined;",
                  "            node = Array.isArray(node) ? node[Number(segment)] : node[segment];",
                  "        }",
                  "        return node;",
                  "    };",
                  "    // List shapes differ between endpoints (data, items, packs), so the FIRST",
                  "    // array of objects in the body is taken rather than a known field name:",
                  "    // knowing the field here would be one more copy of the contract.",
                  "    //",
                  "    // AN ELEMENT STILL IN PROGRESS OR FAILED IS NOT TAKEN. A master with status pending",
                  "    // is an ordered but not yet drawn picture: the next request (composition,",
                  "    // export estimate) would answer 404/400 for it, and that would look like a",
                  "    // product failure. The status list is closed; an unknown status counts as",
                  "    // ready, because the cost of that mistake is a red row, not a lost chain.",
                  "    const NOT_READY = ['pending', 'queued', 'running', 'processing', 'failed', 'error', 'cancelled', 'canceled'];",
                  "    const pickReady = function (arr) {",
                  "        const ready = arr.filter(function (e) { return !e || typeof e.status !== 'string' || NOT_READY.indexOf(e.status) === -1; });",
                  "        return ready.length > 0 ? ready[0] : undefined;",
                  "    };",
                  "    const firstList = function (node) {",
                  "        if (Array.isArray(node)) return node.length > 0 && typeof node[0] === 'object' ? pickReady(node) : undefined;",
                  "        if (!node || typeof node !== 'object') return undefined;",
                  "        for (const value of Object.values(node)) {",
                  "            if (!Array.isArray(value) || value.length === 0 || typeof value[0] !== 'object') continue;",
                  "            return pickReady(value);",
                  "        }",
                  "        return undefined;",
                  "    };",
                  "    const stores = [\n    { var: \"archive_kind\", path: [\"kind\"] },\n    ];",
                  "    for (const store of stores) {",
                  "        const source = store.list ? firstList(store.in ? dig(body, store.in) : body) : body;",
                  "        const value = store.list ? (source ? source[store.list] : undefined) : dig(source, store.path);",
                  "        // An empty value does NOT overwrite: a list may be empty while the variable",
                  "        // was filled earlier and is still valid.",
                  "        if (value !== undefined && value !== null && String(value) !== '') pm.collectionVariables.set(store.var, String(value));",
                  "    }",
                  "}"
                ],
                "type": "text/javascript"
              }
            }
          ]
        },
        {
          "name": "POST /v1/batches/{id}/export/estimate",
          "description": "Price building file sets for a batch\n\nThe price of POST /v1/batches/{id}/export with the same body, before any money moves. Each chosen master is priced exactly as POST /v1/masters/{id}/export would price it: an already built set in the same configuration costs 0, a new set costs its usual price. `selection` is a rule (`all_ready` minus `except_indexes`, or explicit `indexes`), resolved over the whole batch, not over a page. Items that cannot be built are listed in `invalid` with a reason. Requires the `batch` scope. This call never spends tokens.\n\nPrice: 0 tokens (the endpoint declares it with the X-Tokens-Charged header).\n\nPATH VARIABLES: {{batch_id}} <- GET /v1/batches (first element of the list) or POST /v1/batches (the created entity)",
          "request": {
            "method": "POST",
            "auth": {
              "type": "noauth"
            },
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/batches/{{batch_id}}/export/estimate",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "batches",
                "{{batch_id}}",
                "export",
                "estimate"
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"selection\": {\n    \"select\": \"all_ready\",\n    \"except_indexes\": [\n      3\n    ]\n  },\n  \"file_format\": \"webp\",\n  \"overlay\": {\n    \"title\": {\n      \"visible\": true\n    }\n  },\n  \"max_tokens\": 40\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [],
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "exec": [
                  "// A SKIP INSTEAD OF A RED ROW. Some entities do not exist yet on a fresh stand:",
                  "// they are created by the paid folder, and the free folder runs first.",
                  "// A request with an empty variable in the address would answer 404 or 400,",
                  "// i.e. it would look like a product failure rather than an unfilled chain.",
                  "const need = [{\"name\":\"batch_id\",\"from\":\"GET /v1/batches (first element of the list) or POST /v1/batches (the created entity)\"}];",
                  "const missing = need.filter(function (v) { return String(pm.variables.get(v.name) || '').trim() === ''; });",
                  "pm.variables.set('spai_skipped', missing.length > 0 ? '1' : '');",
                  "if (missing.length > 0) {",
                  "    console.log('SKIP ' + pm.info.requestName + ': not filled: ' + missing.map(function (v) { return v.name + ' <- ' + v.from; }).join('; '));",
                  "    if (typeof pm.execution !== 'undefined' && pm.execution.skipRequest) { pm.execution.skipRequest(); }",
                  "}"
                ],
                "type": "text/javascript"
              }
            },
            {
              "listen": "test",
              "script": {
                "exec": [
                  "if (pm.variables.get('spai_skipped') === '1') {",
                  "    // The request was not sent: nothing to assert.",
                  "} else {",
                  "    pm.test('successful response (< 400)', function () {",
                  "        pm.expect(pm.response.code).to.be.below(400);",
                  "    });",
                  "}"
                ],
                "type": "text/javascript"
              }
            }
          ]
        },
        {
          "name": "POST /v1/batches/{id}/retry/estimate",
          "description": "Price a retry of batch items\n\nPrices `POST /v1/batches/{id}/retry` with the same body, without starting anything. `items` gives the price of each item and whether it is the free retry; `refused` lists the items that cannot be retried and why. Requires the `batch` scope. This call never spends tokens.\n\nPrice: 0 tokens (the endpoint declares it with the X-Tokens-Charged header).\n\nPATH VARIABLES: {{batch_id}} <- GET /v1/batches (first element of the list) or POST /v1/batches (the created entity)",
          "request": {
            "method": "POST",
            "auth": {
              "type": "noauth"
            },
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/batches/{{batch_id}}/retry/estimate",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "batches",
                "{{batch_id}}",
                "retry",
                "estimate"
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"kind\": \"retry\",\n  \"selection\": {\n    \"select\": \"failed\"\n  },\n  \"max_tokens\": 5\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [],
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "exec": [
                  "// A SKIP INSTEAD OF A RED ROW. Some entities do not exist yet on a fresh stand:",
                  "// they are created by the paid folder, and the free folder runs first.",
                  "// A request with an empty variable in the address would answer 404 or 400,",
                  "// i.e. it would look like a product failure rather than an unfilled chain.",
                  "const need = [{\"name\":\"batch_id\",\"from\":\"GET /v1/batches (first element of the list) or POST /v1/batches (the created entity)\"}];",
                  "const missing = need.filter(function (v) { return String(pm.variables.get(v.name) || '').trim() === ''; });",
                  "pm.variables.set('spai_skipped', missing.length > 0 ? '1' : '');",
                  "if (missing.length > 0) {",
                  "    console.log('SKIP ' + pm.info.requestName + ': not filled: ' + missing.map(function (v) { return v.name + ' <- ' + v.from; }).join('; '));",
                  "    if (typeof pm.execution !== 'undefined' && pm.execution.skipRequest) { pm.execution.skipRequest(); }",
                  "}"
                ],
                "type": "text/javascript"
              }
            },
            {
              "listen": "test",
              "script": {
                "exec": [
                  "if (pm.variables.get('spai_skipped') === '1') {",
                  "    // The request was not sent: nothing to assert.",
                  "} else {",
                  "    pm.test('successful response (< 400)', function () {",
                  "        pm.expect(pm.response.code).to.be.below(400);",
                  "    });",
                  "}"
                ],
                "type": "text/javascript"
              }
            }
          ]
        },
        {
          "name": "POST /v1/exports/{exportSetId}/formats/{format}/retry",
          "description": "Retry one missing format of a set\n\nOne free retry for a format that did not assemble in a partial set (`missing[].retry` is `available` in GET /v1/exports/{exportSetId}). The retry builds into the SAME set: no new hold, no charge (`X-Tokens-Charged: 0`). Exactly one retry per format for the whole life of the set: after it is used the answer is 409 `format_retry_used`. A format that is delivered, not ordered, cancelled by you, refused by the model, or a failed set answers 409 `format_retry_not_applicable`; while another build of this set runs the answer is 409 `export_set_busy`. Repeating the call while the retry runs returns the same job. If the retry fails too, the tokens for the lost formats come back by themselves: read `tokens_refunded` in GET /v1/exports/{exportSetId}. Requires the `export` scope. This call never spends tokens.\n\nPrice: 0 tokens (the endpoint declares it with the X-Tokens-Charged header).\n\nPATH VARIABLES: {{export_set_id}} <- POST /v1/masters/{id}/export; {{format}} <- fill in manually",
          "request": {
            "method": "POST",
            "auth": {
              "type": "noauth"
            },
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/exports/{{export_set_id}}/formats/{{format}}/retry",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "exports",
                "{{export_set_id}}",
                "formats",
                "{{format}}",
                "retry"
              ]
            }
          },
          "response": [],
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "exec": [
                  "// A SKIP INSTEAD OF A RED ROW. Some entities do not exist yet on a fresh stand:",
                  "// they are created by the paid folder, and the free folder runs first.",
                  "// A request with an empty variable in the address would answer 404 or 400,",
                  "// i.e. it would look like a product failure rather than an unfilled chain.",
                  "const need = [{\"name\":\"export_set_id\",\"from\":\"POST /v1/masters/{id}/export\"},{\"name\":\"format\",\"from\":\"fill in manually in the environment\"}];",
                  "const missing = need.filter(function (v) { return String(pm.variables.get(v.name) || '').trim() === ''; });",
                  "pm.variables.set('spai_skipped', missing.length > 0 ? '1' : '');",
                  "if (missing.length > 0) {",
                  "    console.log('SKIP ' + pm.info.requestName + ': not filled: ' + missing.map(function (v) { return v.name + ' <- ' + v.from; }).join('; '));",
                  "    if (typeof pm.execution !== 'undefined' && pm.execution.skipRequest) { pm.execution.skipRequest(); }",
                  "}"
                ],
                "type": "text/javascript"
              }
            },
            {
              "listen": "test",
              "script": {
                "exec": [
                  "if (pm.variables.get('spai_skipped') === '1') {",
                  "    // The request was not sent: nothing to assert.",
                  "} else {",
                  "    pm.test('successful response (< 400)', function () {",
                  "        pm.expect(pm.response.code).to.be.below(400);",
                  "    });",
                  "}"
                ],
                "type": "text/javascript"
              }
            }
          ]
        },
        {
          "name": "POST /v1/games",
          "description": "Add a game to the catalogue\n\nRegisters a title that is not in the catalogue yet — the main case for this product, since tiles are wanted before a slot ships. Returns 200, not 201, on purpose: sending the same name and provider again returns the same record instead of creating a duplicate, so this call is safe to retry. Use `GET /v1/providers` for the canonical studio spelling. A different spelling of a studio already in the catalogue (punctuation, spacing, a trailing Gaming or Studios) is matched to that studio; the response carries the canonical spelling. Requires the `read` scope. This call never spends tokens.\n\nPrice: 0 tokens (the endpoint declares it with the X-Tokens-Charged header).",
          "request": {
            "method": "POST",
            "auth": {
              "type": "noauth"
            },
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/games",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "games"
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"Gates of Fortune\",\n  \"provider_name\": \"Pragmatic Play\",\n  \"theme_hint\": \"ancient temple, gold, stormy sky\",\n  \"release_date\": \"2026-09-15\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [],
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "if (pm.variables.get('spai_skipped') === '1') {",
                  "    // The request was not sent: nothing to assert.",
                  "} else {",
                  "    pm.test('successful response (< 400)', function () {",
                  "        pm.expect(pm.response.code).to.be.below(400);",
                  "    });",
                  "}",
                  "// Identifiers are chained through variables: the next request substitutes",
                  "// them itself, nobody copies them by hand.",
                  "if (pm.response.code < 300) {",
                  "    let body = null;",
                  "    try { body = pm.response.json(); } catch (e) { body = null; }",
                  "    const dig = function (node, segments) {",
                  "        for (const segment of segments) {",
                  "            if (node === null || node === undefined) return undefined;",
                  "            node = Array.isArray(node) ? node[Number(segment)] : node[segment];",
                  "        }",
                  "        return node;",
                  "    };",
                  "    // List shapes differ between endpoints (data, items, packs), so the FIRST",
                  "    // array of objects in the body is taken rather than a known field name:",
                  "    // knowing the field here would be one more copy of the contract.",
                  "    //",
                  "    // AN ELEMENT STILL IN PROGRESS OR FAILED IS NOT TAKEN. A master with status pending",
                  "    // is an ordered but not yet drawn picture: the next request (composition,",
                  "    // export estimate) would answer 404/400 for it, and that would look like a",
                  "    // product failure. The status list is closed; an unknown status counts as",
                  "    // ready, because the cost of that mistake is a red row, not a lost chain.",
                  "    const NOT_READY = ['pending', 'queued', 'running', 'processing', 'failed', 'error', 'cancelled', 'canceled'];",
                  "    const pickReady = function (arr) {",
                  "        const ready = arr.filter(function (e) { return !e || typeof e.status !== 'string' || NOT_READY.indexOf(e.status) === -1; });",
                  "        return ready.length > 0 ? ready[0] : undefined;",
                  "    };",
                  "    const firstList = function (node) {",
                  "        if (Array.isArray(node)) return node.length > 0 && typeof node[0] === 'object' ? pickReady(node) : undefined;",
                  "        if (!node || typeof node !== 'object') return undefined;",
                  "        for (const value of Object.values(node)) {",
                  "            if (!Array.isArray(value) || value.length === 0 || typeof value[0] !== 'object') continue;",
                  "            return pickReady(value);",
                  "        }",
                  "        return undefined;",
                  "    };",
                  "    const stores = [\n    { var: \"game_id\", path: [\"id\"] },\n    ];",
                  "    for (const store of stores) {",
                  "        const source = store.list ? firstList(store.in ? dig(body, store.in) : body) : body;",
                  "        const value = store.list ? (source ? source[store.list] : undefined) : dig(source, store.path);",
                  "        // An empty value does NOT overwrite: a list may be empty while the variable",
                  "        // was filled earlier and is still valid.",
                  "        if (value !== undefined && value !== null && String(value) !== '') pm.collectionVariables.set(store.var, String(value));",
                  "    }",
                  "}"
                ],
                "type": "text/javascript"
              }
            }
          ]
        },
        {
          "name": "POST /v1/masters/estimate",
          "description": "Price a generation before running it\n\nReturns what POST /v1/masters would charge for exactly this body, and the number is binding: what you are quoted here is what the generation costs, as long as the reference state it was priced against (returned as `anchor` and `reference`) still holds. THE BODY IS NOT A COPY OF THE POST /v1/masters BODY: this operation is strict, so the request schema below is the complete list of what it accepts. It takes the inputs that move the price (`game`, `art`, `format_group`, `variant_seed`, the reference switches) plus `operation`, which POST /v1/masters does not take; the fields POST /v1/masters has on top of that (`variants`, `webhook_url`, `external_id`) do not change the price and are rejected here with 400 `validation_failed`, `unrecognized_keys`. Drop them to price, put them back to generate. Set `operation` to `export` to price an export instead. Requires the `read` scope. This call never spends tokens.\n\nPrice: 0 tokens (the endpoint declares it with the X-Tokens-Charged header).",
          "request": {
            "method": "POST",
            "auth": {
              "type": "noauth"
            },
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/masters/estimate",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "masters",
                "estimate"
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"game\": {\n    \"id\": \"{{game_id}}\"\n  },\n  \"art\": {\n    \"style\": \"dark-premium\",\n    \"background\": \"rays-vignette\",\n    \"hero\": {\n      \"mode\": \"auto\"\n    },\n    \"composition\": \"hero_upper_60_lower_third_clear\"\n  },\n  \"format_group\": \"tile\",\n  \"operation\": \"generate\",\n  \"variant_seed\": null\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [],
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "exec": [
                  "// A SKIP INSTEAD OF A RED ROW. Some entities do not exist yet on a fresh stand:",
                  "// they are created by the paid folder, and the free folder runs first.",
                  "// A request with an empty variable in the address would answer 404 or 400,",
                  "// i.e. it would look like a product failure rather than an unfilled chain.",
                  "const need = [{\"name\":\"game_id\",\"from\":\"GET /v1/games (first element of the list) or POST /v1/games (the created entity)\"}];",
                  "const missing = need.filter(function (v) { return String(pm.variables.get(v.name) || '').trim() === ''; });",
                  "pm.variables.set('spai_skipped', missing.length > 0 ? '1' : '');",
                  "if (missing.length > 0) {",
                  "    console.log('SKIP ' + pm.info.requestName + ': not filled: ' + missing.map(function (v) { return v.name + ' <- ' + v.from; }).join('; '));",
                  "    if (typeof pm.execution !== 'undefined' && pm.execution.skipRequest) { pm.execution.skipRequest(); }",
                  "}"
                ],
                "type": "text/javascript"
              }
            },
            {
              "listen": "test",
              "script": {
                "exec": [
                  "if (pm.variables.get('spai_skipped') === '1') {",
                  "    // The request was not sent: nothing to assert.",
                  "} else {",
                  "    pm.test('successful response (< 400)', function () {",
                  "        pm.expect(pm.response.code).to.be.below(400);",
                  "    });",
                  "}"
                ],
                "type": "text/javascript"
              }
            }
          ]
        },
        {
          "name": "POST /v1/renders",
          "description": "Render a captioned preview of a master\n\nDraws the caption described by `layers` over a ready master and returns the frames plus an `overlay_config_id`. Pass that id to POST /v1/masters/{id}/export (or to its estimate) to build the set with exactly this caption: what you see here is what the files get. `layers: {}` is the default caption (game title and studio). Free: 0 tokens. Only a variant in status `ready` can be rendered (400 `master_not_ready` otherwise). A name the fonts cannot draw answers 422 `font_coverage`; a frame of an already built set in a container the set has not paid for answers 422 `container_not_purchased`, with the paid containers in `details.paid_file_formats` and the price of buying the requested one in `details.tokens`. Requires the `render` scope. This call never spends tokens.\n\nPrice: 0 tokens (the endpoint declares it with the X-Tokens-Charged header).",
          "request": {
            "method": "POST",
            "auth": {
              "type": "noauth"
            },
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/renders",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "renders"
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"master\": \"{{master_id}}\",\n  \"layers\": {\n    \"title\": {\n      \"text\": \"Gates of Fortune\",\n      \"visible\": true\n    },\n    \"provider\": {\n      \"text\": \"Pragmatic Play\",\n      \"visible\": true\n    }\n  },\n  \"outputs\": [\n    {\n      \"name\": \"tile-1x1\",\n      \"width\": 640,\n      \"height\": 640,\n      \"format\": \"png\"\n    }\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [],
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "exec": [
                  "// A SKIP INSTEAD OF A RED ROW. Some entities do not exist yet on a fresh stand:",
                  "// they are created by the paid folder, and the free folder runs first.",
                  "// A request with an empty variable in the address would answer 404 or 400,",
                  "// i.e. it would look like a product failure rather than an unfilled chain.",
                  "const need = [{\"name\":\"master_id\",\"from\":\"POST /v1/masters (first ready master of the job)\"}];",
                  "const missing = need.filter(function (v) { return String(pm.variables.get(v.name) || '').trim() === ''; });",
                  "pm.variables.set('spai_skipped', missing.length > 0 ? '1' : '');",
                  "if (missing.length > 0) {",
                  "    console.log('SKIP ' + pm.info.requestName + ': not filled: ' + missing.map(function (v) { return v.name + ' <- ' + v.from; }).join('; '));",
                  "    if (typeof pm.execution !== 'undefined' && pm.execution.skipRequest) { pm.execution.skipRequest(); }",
                  "}"
                ],
                "type": "text/javascript"
              }
            },
            {
              "listen": "test",
              "script": {
                "exec": [
                  "if (pm.variables.get('spai_skipped') === '1') {",
                  "    // The request was not sent: nothing to assert.",
                  "} else {",
                  "    pm.test('successful response (< 400)', function () {",
                  "        pm.expect(pm.response.code).to.be.below(400);",
                  "    });",
                  "}"
                ],
                "type": "text/javascript"
              }
            }
          ]
        },
        {
          "name": "POST /v1/webhook-deliveries/{id}/retry",
          "description": "Retry a webhook delivery\n\nQueues one more attempt for a delivery that failed, outside the normal retry schedule — for when your receiver was down and you have already fixed it. Returns 202: the attempt is queued, not made inline. Requires the `read` scope. This call never spends tokens.\n\nPrice: 0 tokens (the endpoint declares it with the X-Tokens-Charged header).\n\nPATH VARIABLES: {{webhook_delivery_id}} <- GET /v1/webhook-deliveries (first element of the list)",
          "request": {
            "method": "POST",
            "auth": {
              "type": "noauth"
            },
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/webhook-deliveries/{{webhook_delivery_id}}/retry",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "webhook-deliveries",
                "{{webhook_delivery_id}}",
                "retry"
              ]
            }
          },
          "response": [],
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "exec": [
                  "// A SKIP INSTEAD OF A RED ROW. Some entities do not exist yet on a fresh stand:",
                  "// they are created by the paid folder, and the free folder runs first.",
                  "// A request with an empty variable in the address would answer 404 or 400,",
                  "// i.e. it would look like a product failure rather than an unfilled chain.",
                  "const need = [{\"name\":\"webhook_delivery_id\",\"from\":\"GET /v1/webhook-deliveries (first element of the list)\"}];",
                  "const missing = need.filter(function (v) { return String(pm.variables.get(v.name) || '').trim() === ''; });",
                  "pm.variables.set('spai_skipped', missing.length > 0 ? '1' : '');",
                  "if (missing.length > 0) {",
                  "    console.log('SKIP ' + pm.info.requestName + ': not filled: ' + missing.map(function (v) { return v.name + ' <- ' + v.from; }).join('; '));",
                  "    if (typeof pm.execution !== 'undefined' && pm.execution.skipRequest) { pm.execution.skipRequest(); }",
                  "}"
                ],
                "type": "text/javascript"
              }
            },
            {
              "listen": "test",
              "script": {
                "exec": [
                  "if (pm.variables.get('spai_skipped') === '1') {",
                  "    // The request was not sent: nothing to assert.",
                  "} else {",
                  "    pm.test('successful response (< 400)', function () {",
                  "        pm.expect(pm.response.code).to.be.below(400);",
                  "    });",
                  "}"
                ],
                "type": "text/javascript"
              }
            }
          ]
        },
        {
          "name": "POST /v1/webhook-endpoints",
          "description": "Subscribe to webhooks\n\nRegisters a URL that receives job events, so you do not have to poll. THE SIGNING SECRET IS RETURNED ONLY ONCE, in this response — store it immediately; it is never shown again and cannot be recovered. Use it to verify the signature on every delivery. The URL is checked against internal addresses before it is accepted. Requires the `read` scope. This call never spends tokens.\n\nPrice: 0 tokens (the endpoint declares it with the X-Tokens-Charged header).",
          "request": {
            "method": "POST",
            "auth": {
              "type": "noauth"
            },
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/webhook-endpoints",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "webhook-endpoints"
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"url\": \"https://example.com/spai-hook\",\n  \"events\": [\n    \"master.generation.completed\"\n  ],\n  \"description\": \"probe from the Postman collection\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [],
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "if (pm.variables.get('spai_skipped') === '1') {",
                  "    // The request was not sent: nothing to assert.",
                  "} else {",
                  "    pm.test('successful response (< 400)', function () {",
                  "        pm.expect(pm.response.code).to.be.below(400);",
                  "    });",
                  "}",
                  "// Identifiers are chained through variables: the next request substitutes",
                  "// them itself, nobody copies them by hand.",
                  "if (pm.response.code < 300) {",
                  "    let body = null;",
                  "    try { body = pm.response.json(); } catch (e) { body = null; }",
                  "    const dig = function (node, segments) {",
                  "        for (const segment of segments) {",
                  "            if (node === null || node === undefined) return undefined;",
                  "            node = Array.isArray(node) ? node[Number(segment)] : node[segment];",
                  "        }",
                  "        return node;",
                  "    };",
                  "    // List shapes differ between endpoints (data, items, packs), so the FIRST",
                  "    // array of objects in the body is taken rather than a known field name:",
                  "    // knowing the field here would be one more copy of the contract.",
                  "    //",
                  "    // AN ELEMENT STILL IN PROGRESS OR FAILED IS NOT TAKEN. A master with status pending",
                  "    // is an ordered but not yet drawn picture: the next request (composition,",
                  "    // export estimate) would answer 404/400 for it, and that would look like a",
                  "    // product failure. The status list is closed; an unknown status counts as",
                  "    // ready, because the cost of that mistake is a red row, not a lost chain.",
                  "    const NOT_READY = ['pending', 'queued', 'running', 'processing', 'failed', 'error', 'cancelled', 'canceled'];",
                  "    const pickReady = function (arr) {",
                  "        const ready = arr.filter(function (e) { return !e || typeof e.status !== 'string' || NOT_READY.indexOf(e.status) === -1; });",
                  "        return ready.length > 0 ? ready[0] : undefined;",
                  "    };",
                  "    const firstList = function (node) {",
                  "        if (Array.isArray(node)) return node.length > 0 && typeof node[0] === 'object' ? pickReady(node) : undefined;",
                  "        if (!node || typeof node !== 'object') return undefined;",
                  "        for (const value of Object.values(node)) {",
                  "            if (!Array.isArray(value) || value.length === 0 || typeof value[0] !== 'object') continue;",
                  "            return pickReady(value);",
                  "        }",
                  "        return undefined;",
                  "    };",
                  "    const stores = [\n    { var: \"webhook_endpoint_id\", path: [\"id\"] },\n    { var: \"webhook_endpoint_id\", path: [\"id\"] },\n    ];",
                  "    for (const store of stores) {",
                  "        const source = store.list ? firstList(store.in ? dig(body, store.in) : body) : body;",
                  "        const value = store.list ? (source ? source[store.list] : undefined) : dig(source, store.path);",
                  "        // An empty value does NOT overwrite: a list may be empty while the variable",
                  "        // was filled earlier and is still valid.",
                  "        if (value !== undefined && value !== null && String(value) !== '') pm.collectionVariables.set(store.var, String(value));",
                  "    }",
                  "}"
                ],
                "type": "text/javascript"
              }
            }
          ]
        },
        {
          "name": "POST /v1/webhook-endpoints/{id}/test",
          "description": "Send a test delivery\n\nSends one delivery to this endpoint synchronously, bypassing the queue, and returns the result in the response — status code, timing and the truncated response body. Use it to verify your receiver and your signature check before real events arrive. Use it before real events arrive. Requires the `read` scope. This call never spends tokens.\n\nPrice: 0 tokens (the endpoint declares it with the X-Tokens-Charged header).\n\nPATH VARIABLES: {{webhook_endpoint_id}} <- GET /v1/webhook-endpoints (first element of the list) or POST /v1/webhook-endpoints (the created entity)",
          "request": {
            "method": "POST",
            "auth": {
              "type": "noauth"
            },
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/webhook-endpoints/{{webhook_endpoint_id}}/test",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "webhook-endpoints",
                "{{webhook_endpoint_id}}",
                "test"
              ]
            }
          },
          "response": [],
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "exec": [
                  "// A SKIP INSTEAD OF A RED ROW. Some entities do not exist yet on a fresh stand:",
                  "// they are created by the paid folder, and the free folder runs first.",
                  "// A request with an empty variable in the address would answer 404 or 400,",
                  "// i.e. it would look like a product failure rather than an unfilled chain.",
                  "const need = [{\"name\":\"webhook_endpoint_id\",\"from\":\"GET /v1/webhook-endpoints (first element of the list) or POST /v1/webhook-endpoints (the created entity)\"}];",
                  "const missing = need.filter(function (v) { return String(pm.variables.get(v.name) || '').trim() === ''; });",
                  "pm.variables.set('spai_skipped', missing.length > 0 ? '1' : '');",
                  "if (missing.length > 0) {",
                  "    console.log('SKIP ' + pm.info.requestName + ': not filled: ' + missing.map(function (v) { return v.name + ' <- ' + v.from; }).join('; '));",
                  "    if (typeof pm.execution !== 'undefined' && pm.execution.skipRequest) { pm.execution.skipRequest(); }",
                  "}"
                ],
                "type": "text/javascript"
              }
            },
            {
              "listen": "test",
              "script": {
                "exec": [
                  "if (pm.variables.get('spai_skipped') === '1') {",
                  "    // The request was not sent: nothing to assert.",
                  "} else {",
                  "    pm.test('successful response (< 400)', function () {",
                  "        pm.expect(pm.response.code).to.be.below(400);",
                  "    });",
                  "}"
                ],
                "type": "text/javascript"
              }
            }
          ]
        }
      ]
    },
    {
      "name": "2. Spends tokens (deliberately only): 5",
      "description": "Everything that has NOT declared itself free. The default is fail-closed: unknown counts as paid, because a refused run is loud and a spent token is silent.\n\nLOCK: with a spai_live_... key the run stops until the environment variable i_know_this_spends_money equals 'SPEND'. The key is looked up in ALL Postman substitution scopes (local, data, environment, collection, global) and in the assembled request header as well.\n\nTHE KEY GOES INTO THE api_key VARIABLE ONLY. The collection deliberately has no 'Authorization' tab and every request is 'No Auth': the auth helper signs AFTER pre-request scripts and replaces the header entirely, so a key typed there is invisible to the lock. Measured with real Newman: 27 paid requests went out with a live key while the lock stayed silent.\n\nRE-RUNNING CHARGES A SECOND TIME: a new Idempotency-Key on every run.",
      "event": [
        {
          "listen": "prerequest",
          "script": {
            "exec": [
              "// A LOCK, NOT A WARNING. This folder SPENDS REAL TOKENS.",
              "// The run STOPS here: the request is not sent, and the whole Collection Runner",
              "// stops. Nobody reads the description in the very second they press Run,",
              "// so text alone cannot close this.",
              "// THE KEY IS LOOKED UP WHEREVER POSTMAN WOULD SUBSTITUTE IT FROM, NOT IN TWO SCOPES OUT OF FIVE.",
              "// {{api_key}} resolves along the chain local > data > environment > collection > global.",
              "// A lock that read only environment and collectionVariables stayed silent on a live key",
              "// placed in Globals with no environment selected: 34 paid requests went out",
              "// with Authorization: Bearer spai_live_... (acceptance run, 30.08).",
              "const spaiKeyCandidates = [",
              "    pm.variables.get('api_key'),",
              "    (typeof pm.iterationData !== 'undefined' && pm.iterationData && pm.iterationData.get) ? pm.iterationData.get('api_key') : undefined,",
              "    pm.environment.get('api_key'),",
              "    pm.collectionVariables.get('api_key'),",
              "    (typeof pm.globals !== 'undefined' && pm.globals && pm.globals.get) ? pm.globals.get('api_key') : undefined,",
              "    pm.variables.replaceIn('{{api_key}}'),",
              "    // The last candidate is the ASSEMBLED request header. It catches a key typed",
              "    // literally into the header instead of the variable.",
              "    // WHAT IT DID NOT CATCH, AND WHY THERE IS NO PROMISE OF 'no sixth scope' HERE ANY MORE:",
              "    // while the collection had an auth helper, its signature ran AFTER this script and",
              "    // replaced the header entirely: the script read the template, the wire carried the",
              "    // helper token. Measured with real Newman: 22-27 paid requests went out with",
              "    // Bearer spai_live_... while the lock stayed silent. That class is closed not here",
              "    // but by having no auth helper on the collection or any request (auth: noauth).",
              "    (pm.request && pm.request.headers && pm.request.headers.get)",
              "        ? String(pm.variables.replaceIn(String(pm.request.headers.get('Authorization') || ''))).replace(/^Bearer\\s+/i, '')",
              "        : '',",
              "].map(function (v) { return (v === undefined || v === null) ? '' : String(v); })",
              "  // Nested substitution: api_key = {{my_real_key}} is a second pass through the same resolver.",
              "  .map(function (v) { return v.indexOf('{{') === -1 ? v : String(pm.variables.replaceIn(v)); })",
              "  // Surrounding whitespace is not part of a key, and 'spai_live_...' with a leading",
              "  // space would slip past a prefix comparison.",
              "  .map(function (v) { return v.trim(); });",
              "// An unresolved placeholder ('{{api_key}}') is not a key.",
              "const spaiKeys = spaiKeyCandidates.filter(function (v) { return v !== '' && v.indexOf('{{') !== 0; });",
              "const key = spaiKeys.length > 0 ? spaiKeys[0] : '';",
              "// Fail-closed three times over: a key found in ANY scope (not only the one that won",
              "// the chain) counts as live; case is ignored; whitespace is trimmed.",
              "// A spurious refusal is loud, a spurious spend is silent.",
              "const keyIsLive = spaiKeys.some(function (v) { return v.toLowerCase().indexOf('spai_live_') === 0; });",
              "const confirm = pm.environment.get('i_know_this_spends_money') || '';",
              "if (keyIsLive) {",
              "    if (confirm !== 'SPEND') {",
              "        // THE ORDER HERE IS A MEASUREMENT, NOT A PREFERENCE. Newman 6.2.2, a folder of",
              "        // three requests, a wire tap counting what was sent:",
              "        //   a single throw                    -> ALL THREE went out;",
              "        //   setNextRequest(null) + throw      -> ONE went out (the current one);",
              "        //   skipRequest()                     -> ZERO went out.",
              "        // So throwing in a pre-request script does NOT cancel the request: it only",
              "        // paints the script red. A lock built on a throw let EXACTLY ONE paid",
              "        // request through with a live key.",
              "        //",
              "        // 1) Strip the key from the request. This works even where skipRequest is",
              "        //    missing: if the request does go out, it goes out WITHOUT the key and",
              "        //    gets 401 instead of spending a token. Verified on the wire:",
              "        //    the header read 'Bearer LOCKED-NO-KEY'.",
              "        if (pm.request && pm.request.headers && pm.request.headers.upsert) {",
              "            pm.request.headers.upsert({ key: 'Authorization', value: 'Bearer LOCKED-NO-KEY' });",
              "        }",
              "        // 2) Stop the WHOLE run, not only this request: otherwise the next",
              "        //    dangerous request would go out as if nothing happened.",
              "        if (typeof pm.execution !== 'undefined' && pm.execution.setNextRequest) { pm.execution.setNextRequest(null); }",
              "        else if (typeof postman !== 'undefined' && postman.setNextRequest) { postman.setNextRequest(null); }",
              "        // 3) Say it OUT LOUD. skipRequest aborts the script, so the throw below is",
              "        //    never reached and the Runner report shows no red row: the run looks",
              "        //    like a clean run of zero requests. So the reason is printed to the",
              "        //    console BEFORE the request is skipped.",
              "        if (typeof console !== 'undefined' && console.error) { console.error('LOCK: the key is live (spai_live_...) and the i_know_this_spends_money variable is not \\'SPEND\\'. Run stopped, request not sent, key stripped from the request.'); }",
              "        // 4) Do not send THIS request. The only thing this call can do.",
              "        //    Missing in Postman older than 10.15 and Newman older than 6: there",
              "        //    steps 1 and 2 apply, and one request goes out without a key. Worse, but not costlier.",
              "        if (typeof pm.execution !== 'undefined' && pm.execution.skipRequest) { pm.execution.skipRequest(); }",
              "        throw new Error('REFUSED: the key is live (spai_live_...) and the i_know_this_spends_money variable is not \\'SPEND\\'. Run stopped, request not sent, key stripped from the request. To really run it, set i_know_this_spends_money=SPEND deliberately.');",
              "    }",
              "}"
            ],
            "type": "text/javascript"
          }
        }
      ],
      "item": [
        {
          "name": "POST /v1/batches ◈ SPENDS",
          "description": "Start a batch generation\n\nGenerates art for many games in one paid call — from `items[]` or from a `csv` payload. Dry-run the CSV with POST /v1/batches/csv-preview first: unmatched rows are found there for free. Items named by name run only on an exact catalogue match; an approximate match returns 409 inexact_match_unconfirmed with the suggested game_id, before any charge. Tokens are held for the whole batch up front and released for any item that does not run. Track progress with GET /v1/batches/{id}; stop it with POST /v1/batches/{id}/cancel, which returns the hold on items that have not started. Requires the `batch` scope. This call can spend tokens: the `X-Tokens-Charged` response header states what it actually cost.\n\nPrice: the endpoint has NOT declared itself free, so it is treated as spending.",
          "request": {
            "method": "POST",
            "auth": {
              "type": "noauth"
            },
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/batches",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "batches"
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"operation\": \"generate\",\n  \"art\": {\n    \"style\": \"dark-premium\",\n    \"background\": \"rays-vignette\",\n    \"hero\": {\n      \"mode\": \"auto\"\n    },\n    \"composition\": \"hero_upper_60_lower_third_clear\"\n  },\n  \"format_group\": \"tile\",\n  \"variants\": 1,\n  \"auto_pick\": \"first_qa_passed\",\n  \"items\": [\n    {\n      \"external_id\": \"row-1\",\n      \"game_id\": \"{{game_id}}\"\n    },\n    {\n      \"external_id\": \"row-2\",\n      \"game\": {\n        \"name\": \"Book of Sunrise\",\n        \"provider_name\": \"Pragmatic Play\"\n      }\n    }\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [],
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "exec": [
                  "// A SKIP INSTEAD OF A RED ROW. Some entities do not exist yet on a fresh stand:",
                  "// they are created by the paid folder, and the free folder runs first.",
                  "// A request with an empty variable in the address would answer 404 or 400,",
                  "// i.e. it would look like a product failure rather than an unfilled chain.",
                  "const need = [{\"name\":\"game_id\",\"from\":\"GET /v1/games (first element of the list) or POST /v1/games (the created entity)\"}];",
                  "const missing = need.filter(function (v) { return String(pm.variables.get(v.name) || '').trim() === ''; });",
                  "pm.variables.set('spai_skipped', missing.length > 0 ? '1' : '');",
                  "if (missing.length > 0) {",
                  "    console.log('SKIP ' + pm.info.requestName + ': not filled: ' + missing.map(function (v) { return v.name + ' <- ' + v.from; }).join('; '));",
                  "    if (typeof pm.execution !== 'undefined' && pm.execution.skipRequest) { pm.execution.skipRequest(); }",
                  "}"
                ],
                "type": "text/javascript"
              }
            },
            {
              "listen": "test",
              "script": {
                "exec": [
                  "// Identifiers are chained through variables: the next request substitutes",
                  "// them itself, nobody copies them by hand.",
                  "if (pm.response.code < 300) {",
                  "    let body = null;",
                  "    try { body = pm.response.json(); } catch (e) { body = null; }",
                  "    const dig = function (node, segments) {",
                  "        for (const segment of segments) {",
                  "            if (node === null || node === undefined) return undefined;",
                  "            node = Array.isArray(node) ? node[Number(segment)] : node[segment];",
                  "        }",
                  "        return node;",
                  "    };",
                  "    // List shapes differ between endpoints (data, items, packs), so the FIRST",
                  "    // array of objects in the body is taken rather than a known field name:",
                  "    // knowing the field here would be one more copy of the contract.",
                  "    //",
                  "    // AN ELEMENT STILL IN PROGRESS OR FAILED IS NOT TAKEN. A master with status pending",
                  "    // is an ordered but not yet drawn picture: the next request (composition,",
                  "    // export estimate) would answer 404/400 for it, and that would look like a",
                  "    // product failure. The status list is closed; an unknown status counts as",
                  "    // ready, because the cost of that mistake is a red row, not a lost chain.",
                  "    const NOT_READY = ['pending', 'queued', 'running', 'processing', 'failed', 'error', 'cancelled', 'canceled'];",
                  "    const pickReady = function (arr) {",
                  "        const ready = arr.filter(function (e) { return !e || typeof e.status !== 'string' || NOT_READY.indexOf(e.status) === -1; });",
                  "        return ready.length > 0 ? ready[0] : undefined;",
                  "    };",
                  "    const firstList = function (node) {",
                  "        if (Array.isArray(node)) return node.length > 0 && typeof node[0] === 'object' ? pickReady(node) : undefined;",
                  "        if (!node || typeof node !== 'object') return undefined;",
                  "        for (const value of Object.values(node)) {",
                  "            if (!Array.isArray(value) || value.length === 0 || typeof value[0] !== 'object') continue;",
                  "            return pickReady(value);",
                  "        }",
                  "        return undefined;",
                  "    };",
                  "    const stores = [\n    { var: \"batch_id\", path: [\"id\"] },\n    { var: \"batch_id\", path: [\"id\"] },\n    { var: \"batch_id\", path: [\"id\"] },\n    { var: \"batch_id\", path: [\"id\"] },\n    { var: \"batch_id\", path: [\"id\"] },\n    { var: \"batch_id\", path: [\"id\"] },\n    { var: \"batch_id\", path: [\"id\"] },\n    { var: \"batch_id\", path: [\"id\"] },\n    ];",
                  "    for (const store of stores) {",
                  "        const source = store.list ? firstList(store.in ? dig(body, store.in) : body) : body;",
                  "        const value = store.list ? (source ? source[store.list] : undefined) : dig(source, store.path);",
                  "        // An empty value does NOT overwrite: a list may be empty while the variable",
                  "        // was filled earlier and is still valid.",
                  "        if (value !== undefined && value !== null && String(value) !== '') pm.collectionVariables.set(store.var, String(value));",
                  "    }",
                  "}"
                ],
                "type": "text/javascript"
              }
            }
          ]
        },
        {
          "name": "POST /v1/batches/{id}/export ◈ SPENDS",
          "description": "Build file sets for a batch\n\nBuilds the file set of every chosen ready master of the batch, one set per master, each priced and held exactly as POST /v1/masters/{id}/export (sets already built in the same configuration cost 0). The choice is frozen when you call: `all_ready` takes every ready item at that moment minus `except_indexes`; explicit `indexes` with any item that has no ready master is refused whole with 422 `batch_export_items_invalid`. The balance must cover the price of the whole order (402 otherwise, nothing held). The order runs in the background; follow it in `export_order` of GET /v1/batches/{id} with any `items_*` parameter, and each item's set in `items[].set`. Tokens are held per set as the order reaches it, so `export_order.charged` grows while it runs (X-Tokens-Charged of this call is 0). If the balance runs out midway the order stops as `stopped_insufficient`; sets already built stay built, a new call prices the rest. One running order per batch: a second call meanwhile is 409 `batch_export_in_progress`. `max_tokens` caps the whole order. These charges are not part of the batch `tokens_*` fields, which describe the generation run. SEND AN `Idempotency-Key` to make a retry safe. Scope: `batch`, not `export` — the sets of a batch are bought with the organisation's tokens like the batch generation itself. Requires the `batch` scope. This call starts paid work: the response itself holds nothing (`X-Tokens-Charged: 0`), tokens are held while the work runs, and 402 comes back if the balance cannot cover it.\n\nPrice: the response holds 0 tokens; the order holds tokens while it runs (see export_order.charged).\n\nPATH VARIABLES: {{batch_id}} <- GET /v1/batches (first element of the list) or POST /v1/batches (the created entity)",
          "request": {
            "method": "POST",
            "auth": {
              "type": "noauth"
            },
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/batches/{{batch_id}}/export",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "batches",
                "{{batch_id}}",
                "export"
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"selection\": {\n    \"select\": \"all_ready\",\n    \"except_indexes\": [\n      3\n    ]\n  },\n  \"file_format\": \"webp\",\n  \"overlay\": {\n    \"title\": {\n      \"visible\": true\n    }\n  },\n  \"max_tokens\": 40\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [],
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "exec": [
                  "// A SKIP INSTEAD OF A RED ROW. Some entities do not exist yet on a fresh stand:",
                  "// they are created by the paid folder, and the free folder runs first.",
                  "// A request with an empty variable in the address would answer 404 or 400,",
                  "// i.e. it would look like a product failure rather than an unfilled chain.",
                  "const need = [{\"name\":\"batch_id\",\"from\":\"GET /v1/batches (first element of the list) or POST /v1/batches (the created entity)\"}];",
                  "const missing = need.filter(function (v) { return String(pm.variables.get(v.name) || '').trim() === ''; });",
                  "pm.variables.set('spai_skipped', missing.length > 0 ? '1' : '');",
                  "if (missing.length > 0) {",
                  "    console.log('SKIP ' + pm.info.requestName + ': not filled: ' + missing.map(function (v) { return v.name + ' <- ' + v.from; }).join('; '));",
                  "    if (typeof pm.execution !== 'undefined' && pm.execution.skipRequest) { pm.execution.skipRequest(); }",
                  "}"
                ],
                "type": "text/javascript"
              }
            }
          ]
        },
        {
          "name": "POST /v1/batches/{id}/retry ◈ SPENDS",
          "description": "Retry failed batch items or regenerate ready ones\n\nStarts a new batch for the chosen items of this batch; the new batch names this one in `retry_of_batch_id`, runs like any batch and sends its own `batch.completed`. `kind: retry` re-runs failed items. An item that failed with a generation error and never produced a master gets one free retry (`X-Tokens-Charged: 0`); the free retry counts as used once a call to the image generator was made for it — cancelling the retry before it starts keeps it, and so does a call the generator refused without doing any work (HTTP 429 quota, 401, 403). Any further retry, and a retry of an item whose master was delivered, costs 1 token. `kind: regenerate` draws a new master for a ready item for 1 token; the previous master stays downloadable and in your tiles. Only the master is generated: build the formats of the new master with the batch format build. `background` replaces the background of the original batch; everything else is inherited. Items still running give 409 `batch_retry_in_progress`; items that cannot be retried give 422 `batch_retry_not_applicable` with the reason per item; nothing is held in either case. `max_tokens` caps the price (409 `price_above_limit`). Send an `Idempotency-Key`: a repeat with the same key and body returns the same retry batch and charges nothing. Requires the `batch` scope. This call can spend tokens: the `X-Tokens-Charged` response header states what it actually cost.\n\nPrice: the endpoint has NOT declared itself free, so it is treated as spending.\n\nPATH VARIABLES: {{batch_id}} <- GET /v1/batches (first element of the list) or POST /v1/batches (the created entity)",
          "request": {
            "method": "POST",
            "auth": {
              "type": "noauth"
            },
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/batches/{{batch_id}}/retry",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "batches",
                "{{batch_id}}",
                "retry"
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"kind\": \"retry\",\n  \"selection\": {\n    \"select\": \"failed\"\n  },\n  \"max_tokens\": 5\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [],
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "exec": [
                  "// A SKIP INSTEAD OF A RED ROW. Some entities do not exist yet on a fresh stand:",
                  "// they are created by the paid folder, and the free folder runs first.",
                  "// A request with an empty variable in the address would answer 404 or 400,",
                  "// i.e. it would look like a product failure rather than an unfilled chain.",
                  "const need = [{\"name\":\"batch_id\",\"from\":\"GET /v1/batches (first element of the list) or POST /v1/batches (the created entity)\"}];",
                  "const missing = need.filter(function (v) { return String(pm.variables.get(v.name) || '').trim() === ''; });",
                  "pm.variables.set('spai_skipped', missing.length > 0 ? '1' : '');",
                  "if (missing.length > 0) {",
                  "    console.log('SKIP ' + pm.info.requestName + ': not filled: ' + missing.map(function (v) { return v.name + ' <- ' + v.from; }).join('; '));",
                  "    if (typeof pm.execution !== 'undefined' && pm.execution.skipRequest) { pm.execution.skipRequest(); }",
                  "}"
                ],
                "type": "text/javascript"
              }
            }
          ]
        },
        {
          "name": "POST /v1/masters ◈ SPENDS",
          "description": "Generate a master image\n\nThe paid call at the centre of this API. Queues a generation job and returns 202 with a job envelope; poll `polling_url` (GET /v1/masters/jobs/{id}) until the status is `succeeded`, or subscribe to a webhook instead. `art.style` and `art.background` come from GET /v1/styles and GET /v1/backgrounds; `art.composition` comes from the `composition` field of GET /v1/formats and must not be invented by hand. Price it first with POST /v1/masters/estimate — the estimate is binding. SEND AN `Idempotency-Key`: without it a retried request creates a second job and charges a second time. The amount actually charged is returned in the `X-Tokens-Charged` response header; with a test key `X-Tokens-Would-Charge` shows what a live run would cost. Requires the `generate` scope. This call can spend tokens: the `X-Tokens-Charged` response header states what it actually cost.\n\nPrice: the endpoint has NOT declared itself free, so it is treated as spending.",
          "request": {
            "method": "POST",
            "auth": {
              "type": "noauth"
            },
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/masters",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "masters"
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"game\": {\n    \"id\": \"{{game_id}}\"\n  },\n  \"art\": {\n    \"style\": \"dark-premium\",\n    \"background\": \"rays-vignette\",\n    \"hero\": {\n      \"mode\": \"auto\"\n    },\n    \"composition\": \"hero_upper_60_lower_third_clear\"\n  },\n  \"format_group\": \"tile\",\n  \"variants\": 2,\n  \"variant_seed\": null,\n  \"external_id\": \"postman-master-1\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [],
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "exec": [
                  "// A SKIP INSTEAD OF A RED ROW. Some entities do not exist yet on a fresh stand:",
                  "// they are created by the paid folder, and the free folder runs first.",
                  "// A request with an empty variable in the address would answer 404 or 400,",
                  "// i.e. it would look like a product failure rather than an unfilled chain.",
                  "const need = [{\"name\":\"game_id\",\"from\":\"GET /v1/games (first element of the list) or POST /v1/games (the created entity)\"}];",
                  "const missing = need.filter(function (v) { return String(pm.variables.get(v.name) || '').trim() === ''; });",
                  "pm.variables.set('spai_skipped', missing.length > 0 ? '1' : '');",
                  "if (missing.length > 0) {",
                  "    console.log('SKIP ' + pm.info.requestName + ': not filled: ' + missing.map(function (v) { return v.name + ' <- ' + v.from; }).join('; '));",
                  "    if (typeof pm.execution !== 'undefined' && pm.execution.skipRequest) { pm.execution.skipRequest(); }",
                  "}"
                ],
                "type": "text/javascript"
              }
            },
            {
              "listen": "test",
              "script": {
                "exec": [
                  "// Identifiers are chained through variables: the next request substitutes",
                  "// them itself, nobody copies them by hand.",
                  "if (pm.response.code < 300) {",
                  "    let body = null;",
                  "    try { body = pm.response.json(); } catch (e) { body = null; }",
                  "    const dig = function (node, segments) {",
                  "        for (const segment of segments) {",
                  "            if (node === null || node === undefined) return undefined;",
                  "            node = Array.isArray(node) ? node[Number(segment)] : node[segment];",
                  "        }",
                  "        return node;",
                  "    };",
                  "    // List shapes differ between endpoints (data, items, packs), so the FIRST",
                  "    // array of objects in the body is taken rather than a known field name:",
                  "    // knowing the field here would be one more copy of the contract.",
                  "    //",
                  "    // AN ELEMENT STILL IN PROGRESS OR FAILED IS NOT TAKEN. A master with status pending",
                  "    // is an ordered but not yet drawn picture: the next request (composition,",
                  "    // export estimate) would answer 404/400 for it, and that would look like a",
                  "    // product failure. The status list is closed; an unknown status counts as",
                  "    // ready, because the cost of that mistake is a red row, not a lost chain.",
                  "    const NOT_READY = ['pending', 'queued', 'running', 'processing', 'failed', 'error', 'cancelled', 'canceled'];",
                  "    const pickReady = function (arr) {",
                  "        const ready = arr.filter(function (e) { return !e || typeof e.status !== 'string' || NOT_READY.indexOf(e.status) === -1; });",
                  "        return ready.length > 0 ? ready[0] : undefined;",
                  "    };",
                  "    const firstList = function (node) {",
                  "        if (Array.isArray(node)) return node.length > 0 && typeof node[0] === 'object' ? pickReady(node) : undefined;",
                  "        if (!node || typeof node !== 'object') return undefined;",
                  "        for (const value of Object.values(node)) {",
                  "            if (!Array.isArray(value) || value.length === 0 || typeof value[0] !== 'object') continue;",
                  "            return pickReady(value);",
                  "        }",
                  "        return undefined;",
                  "    };",
                  "    const stores = [\n    { var: \"master_job_id\", path: [\"id\"] },\n    { var: \"master_id\", list: \"id\", in: [\"masters\"] },\n    { var: \"master_id\", path: [\"id\"] },\n    { var: \"master_id\", path: [\"id\"] },\n    { var: \"master_id\", path: [\"id\"] },\n    { var: \"master_id\", path: [\"id\"] },\n    ];",
                  "    for (const store of stores) {",
                  "        const source = store.list ? firstList(store.in ? dig(body, store.in) : body) : body;",
                  "        const value = store.list ? (source ? source[store.list] : undefined) : dig(source, store.path);",
                  "        // An empty value does NOT overwrite: a list may be empty while the variable",
                  "        // was filled earlier and is still valid.",
                  "        if (value !== undefined && value !== null && String(value) !== '') pm.collectionVariables.set(store.var, String(value));",
                  "    }",
                  "}"
                ],
                "type": "text/javascript"
              }
            }
          ]
        },
        {
          "name": "POST /v1/masters/{id}/export ◈ SPENDS",
          "description": "Export a master to every format\n\nBuilds the deliverable set of image files from a finished master. Two success codes and the difference matters for your wallet: 202 means a new set is being built and tokens were held, 200 means this master was already exported and the repeat is FREE — check `X-Tokens-Charged`, not the status code. On 202, poll `polling_url` (GET /v1/exports/jobs/{jobId}) until `export_set_id` appears, then fetch the files with GET /v1/exports/{exportSetId}. Price it first with GET /v1/masters/{id}/export/estimate. Caption: without `overlay_config_id` a new set gets the default caption (game title and studio) and an already built set is left exactly as it is, so repeating this call never redraws your files. Pass an `overlay_config_id` from POST /v1/renders for your own caption, or `\"none\"` for art without a caption; changing the caption of a built set is a free rebuild. File container of a built set: a set owns the containers it has paid for (`paid_file_formats` of the estimate and of GET /v1/exports/{id}). Switching to a paid container is free; naming a container the set has not paid for in `file_format` costs 1 token (`X-Tokens-Charged: 1`), is charged only if the rebuild in that container is published and is returned otherwise; the estimate gives every price in `container_prices`. A call without `file_format` never buys a container. While a build is running, naming an unpaid container holds 1 token at once for the rebuild that follows. The price of a container of an existing set follows the environment of the set, not of the key: a test key that names an unpaid container of a set built with a live key gets 409 `sandbox_key_on_live_set` and nothing is held. SEND AN `Idempotency-Key`: a repeat with the same key and the same body returns the original response with `X-Tokens-Charged: 0` and changes nothing, even if the set has since failed or been cancelled and its tokens returned; the same key with a different body (including another master) is 409 `idempotency_key_reuse`. Without the key a repeat after a failed or cancelled build charges again. `max_tokens` caps the price: if the live price of this call is higher, the answer is 409 `price_above_limit` and nothing is held. Requires the `export` scope. This call can spend tokens: the `X-Tokens-Charged` response header states what it actually cost.\n\nPrice: the endpoint has NOT declared itself free, so it is treated as spending.\n\nPATH VARIABLES: {{master_id}} <- POST /v1/masters (first ready master of the job)",
          "request": {
            "method": "POST",
            "auth": {
              "type": "noauth"
            },
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/masters/{{master_id}}/export",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "masters",
                "{{master_id}}",
                "export"
              ]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"hero_position\": \"center\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [],
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "exec": [
                  "// A SKIP INSTEAD OF A RED ROW. Some entities do not exist yet on a fresh stand:",
                  "// they are created by the paid folder, and the free folder runs first.",
                  "// A request with an empty variable in the address would answer 404 or 400,",
                  "// i.e. it would look like a product failure rather than an unfilled chain.",
                  "const need = [{\"name\":\"master_id\",\"from\":\"POST /v1/masters (first ready master of the job)\"}];",
                  "const missing = need.filter(function (v) { return String(pm.variables.get(v.name) || '').trim() === ''; });",
                  "pm.variables.set('spai_skipped', missing.length > 0 ? '1' : '');",
                  "if (missing.length > 0) {",
                  "    console.log('SKIP ' + pm.info.requestName + ': not filled: ' + missing.map(function (v) { return v.name + ' <- ' + v.from; }).join('; '));",
                  "    if (typeof pm.execution !== 'undefined' && pm.execution.skipRequest) { pm.execution.skipRequest(); }",
                  "}"
                ],
                "type": "text/javascript"
              }
            },
            {
              "listen": "test",
              "script": {
                "exec": [
                  "// Identifiers are chained through variables: the next request substitutes",
                  "// them itself, nobody copies them by hand.",
                  "if (pm.response.code < 300) {",
                  "    let body = null;",
                  "    try { body = pm.response.json(); } catch (e) { body = null; }",
                  "    const dig = function (node, segments) {",
                  "        for (const segment of segments) {",
                  "            if (node === null || node === undefined) return undefined;",
                  "            node = Array.isArray(node) ? node[Number(segment)] : node[segment];",
                  "        }",
                  "        return node;",
                  "    };",
                  "    // List shapes differ between endpoints (data, items, packs), so the FIRST",
                  "    // array of objects in the body is taken rather than a known field name:",
                  "    // knowing the field here would be one more copy of the contract.",
                  "    //",
                  "    // AN ELEMENT STILL IN PROGRESS OR FAILED IS NOT TAKEN. A master with status pending",
                  "    // is an ordered but not yet drawn picture: the next request (composition,",
                  "    // export estimate) would answer 404/400 for it, and that would look like a",
                  "    // product failure. The status list is closed; an unknown status counts as",
                  "    // ready, because the cost of that mistake is a red row, not a lost chain.",
                  "    const NOT_READY = ['pending', 'queued', 'running', 'processing', 'failed', 'error', 'cancelled', 'canceled'];",
                  "    const pickReady = function (arr) {",
                  "        const ready = arr.filter(function (e) { return !e || typeof e.status !== 'string' || NOT_READY.indexOf(e.status) === -1; });",
                  "        return ready.length > 0 ? ready[0] : undefined;",
                  "    };",
                  "    const firstList = function (node) {",
                  "        if (Array.isArray(node)) return node.length > 0 && typeof node[0] === 'object' ? pickReady(node) : undefined;",
                  "        if (!node || typeof node !== 'object') return undefined;",
                  "        for (const value of Object.values(node)) {",
                  "            if (!Array.isArray(value) || value.length === 0 || typeof value[0] !== 'object') continue;",
                  "            return pickReady(value);",
                  "        }",
                  "        return undefined;",
                  "    };",
                  "    const stores = [\n    { var: \"export_set_id\", path: [\"export_set_id\"] },\n    { var: \"export_job_id\", path: [\"id\"] },\n    ];",
                  "    for (const store of stores) {",
                  "        const source = store.list ? firstList(store.in ? dig(body, store.in) : body) : body;",
                  "        const value = store.list ? (source ? source[store.list] : undefined) : dig(source, store.path);",
                  "        // An empty value does NOT overwrite: a list may be empty while the variable",
                  "        // was filled earlier and is still valid.",
                  "        if (value !== undefined && value !== null && String(value) !== '') pm.collectionVariables.set(store.var, String(value));",
                  "    }",
                  "}"
                ],
                "type": "text/javascript"
              }
            }
          ]
        }
      ]
    },
    {
      "name": "3. Destructive (irreversible): 3",
      "description": "Deleting webhook subscriptions, cancelling jobs and batches. They spend no tokens, and that is exactly why they are dangerous: by the rule 'free means safe' they would land in the first folder, and 'Run collection' with a test key would delete a subscription and cancel a queued job.\n\nTHE LOCK FIRES WITH ANY KEY, including a test key: a cancelled sandbox job does not resume any more than a live one. The run stops until the environment variable i_know_this_is_irreversible equals 'DESTROY'.",
      "event": [
        {
          "listen": "prerequest",
          "script": {
            "exec": [
              "// A LOCK, NOT A WARNING. This folder DOES SOMETHING IRREVERSIBLE.",
              "// The run STOPS here: the request is not sent, and the whole Collection Runner",
              "// stops. Nobody reads the description in the very second they press Run,",
              "// so text alone cannot close this.",
              "// THE KEY IS LOOKED UP WHEREVER POSTMAN WOULD SUBSTITUTE IT FROM, NOT IN TWO SCOPES OUT OF FIVE.",
              "// {{api_key}} resolves along the chain local > data > environment > collection > global.",
              "// A lock that read only environment and collectionVariables stayed silent on a live key",
              "// placed in Globals with no environment selected: 34 paid requests went out",
              "// with Authorization: Bearer spai_live_... (acceptance run, 30.08).",
              "const spaiKeyCandidates = [",
              "    pm.variables.get('api_key'),",
              "    (typeof pm.iterationData !== 'undefined' && pm.iterationData && pm.iterationData.get) ? pm.iterationData.get('api_key') : undefined,",
              "    pm.environment.get('api_key'),",
              "    pm.collectionVariables.get('api_key'),",
              "    (typeof pm.globals !== 'undefined' && pm.globals && pm.globals.get) ? pm.globals.get('api_key') : undefined,",
              "    pm.variables.replaceIn('{{api_key}}'),",
              "    // The last candidate is the ASSEMBLED request header. It catches a key typed",
              "    // literally into the header instead of the variable.",
              "    // WHAT IT DID NOT CATCH, AND WHY THERE IS NO PROMISE OF 'no sixth scope' HERE ANY MORE:",
              "    // while the collection had an auth helper, its signature ran AFTER this script and",
              "    // replaced the header entirely: the script read the template, the wire carried the",
              "    // helper token. Measured with real Newman: 22-27 paid requests went out with",
              "    // Bearer spai_live_... while the lock stayed silent. That class is closed not here",
              "    // but by having no auth helper on the collection or any request (auth: noauth).",
              "    (pm.request && pm.request.headers && pm.request.headers.get)",
              "        ? String(pm.variables.replaceIn(String(pm.request.headers.get('Authorization') || ''))).replace(/^Bearer\\s+/i, '')",
              "        : '',",
              "].map(function (v) { return (v === undefined || v === null) ? '' : String(v); })",
              "  // Nested substitution: api_key = {{my_real_key}} is a second pass through the same resolver.",
              "  .map(function (v) { return v.indexOf('{{') === -1 ? v : String(pm.variables.replaceIn(v)); })",
              "  // Surrounding whitespace is not part of a key, and 'spai_live_...' with a leading",
              "  // space would slip past a prefix comparison.",
              "  .map(function (v) { return v.trim(); });",
              "// An unresolved placeholder ('{{api_key}}') is not a key.",
              "const spaiKeys = spaiKeyCandidates.filter(function (v) { return v !== '' && v.indexOf('{{') !== 0; });",
              "const key = spaiKeys.length > 0 ? spaiKeys[0] : '';",
              "// Fail-closed three times over: a key found in ANY scope (not only the one that won",
              "// the chain) counts as live; case is ignored; whitespace is trimmed.",
              "// A spurious refusal is loud, a spurious spend is silent.",
              "const keyIsLive = spaiKeys.some(function (v) { return v.toLowerCase().indexOf('spai_live_') === 0; });",
              "const confirm = pm.environment.get('i_know_this_is_irreversible') || '';",
              "if (true /* with ANY key: a test key deletes a subscription and cancels a job just as irreversibly */) {",
              "    if (confirm !== 'DESTROY') {",
              "        // THE ORDER HERE IS A MEASUREMENT, NOT A PREFERENCE. Newman 6.2.2, a folder of",
              "        // three requests, a wire tap counting what was sent:",
              "        //   a single throw                    -> ALL THREE went out;",
              "        //   setNextRequest(null) + throw      -> ONE went out (the current one);",
              "        //   skipRequest()                     -> ZERO went out.",
              "        // So throwing in a pre-request script does NOT cancel the request: it only",
              "        // paints the script red. A lock built on a throw let EXACTLY ONE paid",
              "        // request through with a live key.",
              "        //",
              "        // 1) Strip the key from the request. This works even where skipRequest is",
              "        //    missing: if the request does go out, it goes out WITHOUT the key and",
              "        //    gets 401 instead of spending a token. Verified on the wire:",
              "        //    the header read 'Bearer LOCKED-NO-KEY'.",
              "        if (pm.request && pm.request.headers && pm.request.headers.upsert) {",
              "            pm.request.headers.upsert({ key: 'Authorization', value: 'Bearer LOCKED-NO-KEY' });",
              "        }",
              "        // 2) Stop the WHOLE run, not only this request: otherwise the next",
              "        //    dangerous request would go out as if nothing happened.",
              "        if (typeof pm.execution !== 'undefined' && pm.execution.setNextRequest) { pm.execution.setNextRequest(null); }",
              "        else if (typeof postman !== 'undefined' && postman.setNextRequest) { postman.setNextRequest(null); }",
              "        // 3) Say it OUT LOUD. skipRequest aborts the script, so the throw below is",
              "        //    never reached and the Runner report shows no red row: the run looks",
              "        //    like a clean run of zero requests. So the reason is printed to the",
              "        //    console BEFORE the request is skipped.",
              "        if (typeof console !== 'undefined' && console.error) { console.error('LOCK: this folder is irreversible and the i_know_this_is_irreversible variable is not \\'DESTROY\\'. Run stopped, request not sent, key stripped from the request.'); }",
              "        // 4) Do not send THIS request. The only thing this call can do.",
              "        //    Missing in Postman older than 10.15 and Newman older than 6: there",
              "        //    steps 1 and 2 apply, and one request goes out without a key. Worse, but not costlier.",
              "        if (typeof pm.execution !== 'undefined' && pm.execution.skipRequest) { pm.execution.skipRequest(); }",
              "        throw new Error('REFUSED: this folder is irreversible and the i_know_this_is_irreversible variable is not \\'DESTROY\\'. Run stopped, request not sent, key stripped from the request. To really run it, set i_know_this_is_irreversible=DESTROY deliberately.');",
              "    }",
              "}"
            ],
            "type": "text/javascript"
          }
        }
      ],
      "item": [
        {
          "name": "DELETE /v1/webhook-endpoints/{id} ⚠ IRREVERSIBLE",
          "description": "Delete a webhook subscription\n\nIRREVERSIBLE: deliveries to this URL stop, and because the signing secret is never shown a second time, the subscription cannot be restored — recreating it issues a NEW secret that you must deploy. Requires the `read` scope. This call never spends tokens.\n\nPrice: 0 tokens (the endpoint declares it with the X-Tokens-Charged header).\n\nIRREVERSIBLE: this request cannot be undone. The folder lock fires with any key, including a test one.\n\nPATH VARIABLES: {{webhook_endpoint_id}} <- GET /v1/webhook-endpoints (first element of the list) or POST /v1/webhook-endpoints (the created entity)",
          "request": {
            "method": "DELETE",
            "auth": {
              "type": "noauth"
            },
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/webhook-endpoints/{{webhook_endpoint_id}}",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "webhook-endpoints",
                "{{webhook_endpoint_id}}"
              ]
            }
          },
          "response": [],
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "exec": [
                  "// A SKIP INSTEAD OF A RED ROW. Some entities do not exist yet on a fresh stand:",
                  "// they are created by the paid folder, and the free folder runs first.",
                  "// A request with an empty variable in the address would answer 404 or 400,",
                  "// i.e. it would look like a product failure rather than an unfilled chain.",
                  "const need = [{\"name\":\"webhook_endpoint_id\",\"from\":\"GET /v1/webhook-endpoints (first element of the list) or POST /v1/webhook-endpoints (the created entity)\"}];",
                  "const missing = need.filter(function (v) { return String(pm.variables.get(v.name) || '').trim() === ''; });",
                  "pm.variables.set('spai_skipped', missing.length > 0 ? '1' : '');",
                  "if (missing.length > 0) {",
                  "    console.log('SKIP ' + pm.info.requestName + ': not filled: ' + missing.map(function (v) { return v.name + ' <- ' + v.from; }).join('; '));",
                  "    if (typeof pm.execution !== 'undefined' && pm.execution.skipRequest) { pm.execution.skipRequest(); }",
                  "}"
                ],
                "type": "text/javascript"
              }
            }
          ]
        },
        {
          "name": "POST /v1/batches/{id}/cancel ⚠ IRREVERSIBLE",
          "description": "Cancel a batch\n\nIRREVERSIBLE: items that have not started are dropped and their hold is returned; items already generated stay generated and stay charged. A cancelled batch cannot be resumed — start a new one. Whoever can spend must be able to stop spending, so the scope here is the same one that starts a batch. Requires the `batch` scope. This call never spends tokens.\n\nPrice: 0 tokens (the endpoint declares it with the X-Tokens-Charged header).\n\nIRREVERSIBLE: this request cannot be undone. The folder lock fires with any key, including a test one.\n\nPATH VARIABLES: {{batch_id}} <- GET /v1/batches (first element of the list) or POST /v1/batches (the created entity)",
          "request": {
            "method": "POST",
            "auth": {
              "type": "noauth"
            },
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/batches/{{batch_id}}/cancel",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "batches",
                "{{batch_id}}",
                "cancel"
              ]
            }
          },
          "response": [],
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "exec": [
                  "// A SKIP INSTEAD OF A RED ROW. Some entities do not exist yet on a fresh stand:",
                  "// they are created by the paid folder, and the free folder runs first.",
                  "// A request with an empty variable in the address would answer 404 or 400,",
                  "// i.e. it would look like a product failure rather than an unfilled chain.",
                  "const need = [{\"name\":\"batch_id\",\"from\":\"GET /v1/batches (first element of the list) or POST /v1/batches (the created entity)\"}];",
                  "const missing = need.filter(function (v) { return String(pm.variables.get(v.name) || '').trim() === ''; });",
                  "pm.variables.set('spai_skipped', missing.length > 0 ? '1' : '');",
                  "if (missing.length > 0) {",
                  "    console.log('SKIP ' + pm.info.requestName + ': not filled: ' + missing.map(function (v) { return v.name + ' <- ' + v.from; }).join('; '));",
                  "    if (typeof pm.execution !== 'undefined' && pm.execution.skipRequest) { pm.execution.skipRequest(); }",
                  "}"
                ],
                "type": "text/javascript"
              }
            }
          ]
        },
        {
          "name": "POST /v1/masters/jobs/{id}/cancel ⚠ IRREVERSIBLE",
          "description": "Cancel a generation job\n\nStops a job that is stuck or no longer wanted and releases the tokens still held for it. IRREVERSIBLE: a cancelled job cannot be resumed, and work already sent to the provider is not refunded. A job that has already finished answers with its current state rather than an error, so cancelling twice is not a failure. Whoever can spend must be able to stop spending, so the scope here is the same one that starts a job. Requires the `generate` scope. This call never spends tokens.\n\nPrice: 0 tokens (the endpoint declares it with the X-Tokens-Charged header).\n\nIRREVERSIBLE: this request cannot be undone. The folder lock fires with any key, including a test one.\n\nPATH VARIABLES: {{master_job_id}} <- POST /v1/masters (job id)",
          "request": {
            "method": "POST",
            "auth": {
              "type": "noauth"
            },
            "header": [
              {
                "key": "Authorization",
                "value": "Bearer {{api_key}}"
              },
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/masters/jobs/{{master_job_id}}/cancel",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "masters",
                "jobs",
                "{{master_job_id}}",
                "cancel"
              ]
            }
          },
          "response": [],
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "exec": [
                  "// A SKIP INSTEAD OF A RED ROW. Some entities do not exist yet on a fresh stand:",
                  "// they are created by the paid folder, and the free folder runs first.",
                  "// A request with an empty variable in the address would answer 404 or 400,",
                  "// i.e. it would look like a product failure rather than an unfilled chain.",
                  "const need = [{\"name\":\"master_job_id\",\"from\":\"POST /v1/masters (job id)\"}];",
                  "const missing = need.filter(function (v) { return String(pm.variables.get(v.name) || '').trim() === ''; });",
                  "pm.variables.set('spai_skipped', missing.length > 0 ? '1' : '');",
                  "if (missing.length > 0) {",
                  "    console.log('SKIP ' + pm.info.requestName + ': not filled: ' + missing.map(function (v) { return v.name + ' <- ' + v.from; }).join('; '));",
                  "    if (typeof pm.execution !== 'undefined' && pm.execution.skipRequest) { pm.execution.skipRequest(); }",
                  "}"
                ],
                "type": "text/javascript"
              }
            }
          ]
        }
      ]
    }
  ]
}
