{
  "name": "SpicyAPI Cloud — Character Photo Set",
  "nodes": [
    {
      "id": "a91f36e7-f6fd-4761-999d-11ab16310a97",
      "name": "Start",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        0
      ],
      "parameters": {}
    },
    {
      "id": "dde68616-2953-4453-838b-a7252024ced9",
      "name": "Settings",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        220,
        0
      ],
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "a8a30ef1-963d-4609-bd59-a07cee6f678c",
              "name": "enabled",
              "value": false,
              "type": "boolean"
            },
            {
              "id": "d443be33-f88f-4c47-9730-258ca3a465e5",
              "name": "referenceImageUrl",
              "value": "",
              "type": "string"
            },
            {
              "id": "4b98e364-91f6-48da-9a13-2eddc4b42dbc",
              "name": "photoCount",
              "value": 4,
              "type": "number"
            },
            {
              "id": "5b448251-6806-4f40-8182-0e56fcd4b0f4",
              "name": "maxCostUsd",
              "value": 0.1,
              "type": "number"
            },
            {
              "id": "e0d3cd1c-f7fe-456f-a78b-383f072cd4ad",
              "name": "batchId",
              "value": "",
              "type": "string"
            },
            {
              "id": "072f59fc-ec49-4a69-829a-ec2fc8bc6452",
              "name": "resumeTaskIdsJson",
              "value": "[]",
              "type": "string"
            }
          ]
        },
        "options": {}
      }
    },
    {
      "id": "0ebbd927-d985-49b2-8bcb-0151b9dd72b8",
      "name": "Build Scenes",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        440,
        0
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "function costLimit(value) {\n  const limit = Number(value);\n  if (!Number.isFinite(limit) || limit <= 0 || limit > 0.10) throw new Error('maxCostUsd must be above 0 and at most 0.10 per image.');\n  return limit;\n}\nfunction publicImage(value) {\n  const url = String(value || '').trim();\n  const match = url.match(/^https:\\/\\/([^/?#]+)(?:[/?#]|$)/i);\n  const host = match ? match[1].toLowerCase() : '';\n  const hostname = host.split(':')[0];\n  if (!host || host.includes('@') || host.startsWith('[') || !/^[a-z0-9.-]+(?::443)?$/.test(host) || /^(localhost|127\\.|0\\.|10\\.|192\\.168\\.|169\\.254\\.|172\\.(1[6-9]|2[0-9]|3[01])\\.)/i.test(hostname)) throw new Error('Set a public HTTPS referenceImageUrl without URL credentials.');\n  return url;\n}\n\nconst c = $input.first().json;\nif (c.enabled !== true) throw new Error('Configure Settings and select the Bearer credential before setting enabled=true.');\nconst maxCostUsd = costLimit(c.maxCostUsd);\nlet resume;\ntry { resume = JSON.parse(String(c.resumeTaskIdsJson || '[]')); } catch { throw new Error('resumeTaskIdsJson must be a JSON array of accepted task IDs.'); }\nif (!Array.isArray(resume) || resume.length > 12 || resume.some(id => typeof id !== 'string' || !/^[A-Za-z0-9_-]{1,128}$/.test(id)) || new Set(resume).size !== resume.length) throw new Error('Use at most 12 different accepted task IDs in resumeTaskIdsJson.');\nif (resume.length) return resume.map((taskId, i) => ({json: {sceneNumber: i + 1, sceneName: 'Recovered image ' + (i + 1), taskId, acceptedTaskId: taskId, maxCostUsd, batchMaxCostUsd: 0, recoveryOnly: true}, pairedItem: {item: 0}}));\nconst referenceImageUrl = publicImage(c.referenceImageUrl);\nconst count = Number(c.photoCount);\nif (![4, 8, 12].includes(count)) throw new Error('photoCount must be 4, 8 or 12.');\nconst batchId = String(c.batchId || '').trim();\nconst started = Date.parse(batchId);\nif (!/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{3})?Z$/.test(batchId) || !Number.isFinite(started) || started > Date.now() + 120000 || Date.now() - started >= 23 * 3600000) throw new Error('Set batchId to the current UTC timestamp, for example YYYY-MM-DDTHH:mm:ssZ. Reuse it with unchanged settings within 23 hours; for older attempts, recover by task ID.');\nconst scenes = [[\"Rooftop at dusk\", \"On a rooftop at dusk, crimson satin evening dress, warm string lights and a glowing skyline.\"], [\"Soft window light\", \"In a quiet bedroom by the window, fully covered silk robe, soft morning light, relaxed seated pose.\"], [\"Studio evening portrait\", \"In a graphite photography studio, black evening dress, full-length portrait, soft rim light.\"], [\"Neon street\", \"On a neon-lit city street, dark leather jacket over a simple top, rain reflections.\"], [\"Rainy city\", \"Under a clear umbrella on a rainy street, belted trench coat, blue hour city lights.\"], [\"Cafe portrait\", \"At a small cafe table, cashmere sweater, warm coffee-shop light, candid expression.\"], [\"Athletic editorial\", \"In a modern fitness studio, fitted sportswear, confident standing pose, daylight.\"], [\"Art gallery\", \"In a bright art gallery, white blazer and tailored trousers, natural walking pose.\"], [\"Resort terrace\", \"On a resort terrace, a modest two-piece swimsuit with a light wrap, warm sunset.\"], [\"Moonlit garden\", \"In a moonlit garden, deep-green velvet evening dress, cinematic soft light.\"], [\"Hotel corridor\", \"In a warmly lit hotel corridor, tailored blazer dress, fashion editorial composition.\"], [\"Piano lounge\", \"Beside a piano in a quiet lounge, burgundy cocktail dress, amber lamps and soft shadows.\"]];\nreturn scenes.slice(0, count).map(([sceneName, scene], i) => {\n  const model = 'alibaba/qwen-image-2.1/edit';\n  const input = {prompt: 'Use the same adult fictional character as the reference. Keep the face, hairstyle and overall build recognisable; change the setting and outfit. ' + scene + ' Fashion photography, complete clothing, natural anatomy.', image_urls: [referenceImageUrl], resolution: '1k', aspect_ratio: '3:4', output_format: 'png', background: 'opaque', enable_prompt_expansion: false};\n  return {json: {sceneNumber: i + 1, sceneName, model, input, maxCostUsd, batchMaxCostUsd: Number((count * maxCostUsd).toFixed(4)), idempotencyKey: 'spicy-cloud-photo-' + started + '-' + (i + 1), firstAttemptAt: new Date(started).toISOString(), taskId: '', acceptedTaskId: ''}, pairedItem: {item: 0}};\n});\n"
      }
    },
    {
      "id": "205a6666-d766-41a3-b0f7-6d9765e64c88",
      "name": "Loop Over Items",
      "type": "n8n-nodes-base.splitInBatches",
      "typeVersion": 3,
      "position": [
        1100,
        0
      ],
      "parameters": {
        "batchSize": 1,
        "options": {}
      }
    },
    {
      "id": "e982a6e5-b7e9-41ee-9994-1905c0312312",
      "name": "Batch Results",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        1320,
        -240
      ],
      "parameters": {}
    },
    {
      "id": "02a02d2d-cb5f-4f82-98ce-5b116ea451cd",
      "name": "Item Context",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1320,
        100
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const c = $input.first().json;\nreturn [{json: {...c, accepted: !!c.acceptedTaskId, quoteRefreshes: 0, pollCount: 0, deadlineAt: Date.now() + 300000}, pairedItem: {item: 0}}];"
      }
    },
    {
      "id": "c598ff68-be3c-4993-ae94-bcf1fb2296ff",
      "name": "Skip Submission?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1540,
        100
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "04b58c08-a557-4a74-8380-7382fb782378",
              "leftValue": "={{ $json.skipSubmit === true }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "46dc949c-52a8-4d8e-8453-b37e39a22ed5",
      "name": "Existing Task?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1760,
        180
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "be7554a1-572b-4f30-a545-8fca81e95003",
              "leftValue": "={{ !!$json.acceptedTaskId }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "a06bb4a4-0278-46a8-a700-0406b0e1cd67",
      "name": "Quote Context",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2420,
        360
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "return [{json: {...$input.first().json}, pairedItem: {item: 0}}];"
      }
    },
    {
      "id": "5046f783-4196-4432-8605-272a95d6394f",
      "name": "Quote Request",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2640,
        360
      ],
      "parameters": {
        "method": "POST",
        "url": "https://api.spicyapi.ai/api/v1/jobs/quote",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth",
        "options": {
          "timeout": 30000,
          "response": {
            "response": {
              "responseFormat": "json",
              "fullResponse": true,
              "neverError": true
            }
          },
          "redirect": {
            "redirect": {
              "followRedirects": false
            }
          }
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({model: $json.model, input: $json.input}) }}"
      },
      "credentials": {
        "httpBearerAuth": {
          "id": "",
          "name": "SpicyAPI Bearer"
        }
      },
      "onError": "continueRegularOutput"
    },
    {
      "id": "ef6606c9-060d-444d-98fd-5368f7d501b7",
      "name": "Check Quote",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2860,
        360
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const c = $('Quote Context').item.json;\nconst response = $input.first().json;\nconst b = response.body || {};\nconst q = b.data || {};\nconst estimated = Number(q.estimatedCost);\nconst maximum = Number(q.maxCharge);\nconst amount = value => typeof value === 'string' && /^\\d+(?:\\.\\d+)?$/.test(value);\nconst valid = response.statusCode >= 200 && response.statusCode < 300 && b.code === 200 && typeof q.quoteId === 'string' && !!q.quoteId && amount(q.estimatedCost) && amount(q.maxCharge) && Number.isFinite(estimated) && Number.isFinite(maximum) && estimated >= 0 && maximum >= estimated;\nconst replayAge = Date.now() - Date.parse(c.firstAttemptAt);\nconst withinWindow = Number.isFinite(replayAge) && replayAge >= -120000 && replayAge < 23 * 3600000;\nconst withinLimit = valid && withinWindow && maximum <= c.maxCostUsd && estimated <= c.maxCostUsd;\nreturn [{json: {...c, quote: valid ? q : null, quoteAllowed: withinLimit, refreshPrice: false, status: withinLimit ? 'processing' : 'needs_review', error: withinLimit ? '' : (valid && !withinWindow ? 'The 23-hour replay window ended. Recover by the accepted task ID; no task was submitted in this attempt.' : valid ? 'Quote exceeds maxCostUsd. No task was submitted in this attempt.' : 'Could not verify the quote. No task was submitted in this attempt.'), quoteRequestId: b.request_id || ''}, pairedItem: {item: 0}}];"
      }
    },
    {
      "id": "00252b75-4c34-4d64-b828-5ef084f22f51",
      "name": "Quote Approved?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        3080,
        360
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "3b0fa6eb-c54d-4348-a4de-b43b41882644",
              "leftValue": "={{ $json.quoteAllowed === true }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "bfa7fd51-85fe-4713-a9e4-228a4611f2d4",
      "name": "Submit Context",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3300,
        460
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "return [{json: {...$input.first().json}, pairedItem: {item: 0}}];"
      }
    },
    {
      "id": "e84ca531-7fa3-47c4-80ab-28eeb6a47979",
      "name": "Create Task",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        3520,
        460
      ],
      "parameters": {
        "method": "POST",
        "url": "https://api.spicyapi.ai/api/v1/jobs/createTask",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth",
        "options": {
          "timeout": 30000,
          "response": {
            "response": {
              "responseFormat": "json",
              "fullResponse": true,
              "neverError": true
            }
          },
          "redirect": {
            "redirect": {
              "followRedirects": false
            }
          }
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({model: $json.model, input: $json.input, quoteId: $json.quote.quoteId, expectedCost: $json.quote.estimatedCost}) }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Idempotency-Key",
              "value": "={{ $json.idempotencyKey }}"
            }
          ]
        }
      },
      "credentials": {
        "httpBearerAuth": {
          "id": "",
          "name": "SpicyAPI Bearer"
        }
      },
      "onError": "continueRegularOutput"
    },
    {
      "id": "0acfc05a-d6e8-49b8-b1b5-2f07e05fa399",
      "name": "Check Submission",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3740,
        460
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const c = $('Submit Context').item.json;\nconst response = $input.first().json;\nconst b = response.body || {};\nconst task = b.data || {};\nconst id = typeof task.taskId === 'string' && /^[A-Za-z0-9_-]{1,128}$/.test(task.taskId) ? task.taskId : '';\nconst accepted = response.statusCode >= 200 && response.statusCode < 300 && b.code === 200 && !!id;\nconst refreshPrice = !accepted && b.code === 40901 && c.quoteRefreshes === 0;\nreturn [{json: {...c, taskId: accepted ? id : (c.taskId || ''), acceptedTaskId: accepted ? id : (c.acceptedTaskId || ''), requestId: b.request_id || c.requestId || '', accepted, refreshPrice, quoteRefreshes: c.quoteRefreshes + (refreshPrice ? 1 : 0), status: accepted || refreshPrice ? 'processing' : 'needs_review', error: accepted || refreshPrice ? '' : 'Submission did not return a confirmed task ID. Keep this idempotencyKey and unchanged request; inspect the saved execution and Console before any new attempt.'}, pairedItem: {item: 0}}];"
      }
    },
    {
      "id": "e8128e68-c5d7-437f-a458-b61f16ac3c38",
      "name": "Refresh Price Once?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        3960,
        460
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "e10b885f-86a6-491f-a8b8-c94da1732177",
              "leftValue": "={{ $json.refreshPrice === true }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "a11fcf0d-4524-4844-8834-40896ceb7fe9",
      "name": "Task Accepted?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        4180,
        560
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "9092f00d-fd08-4878-8840-59ed2f75e2f7",
              "leftValue": "={{ $json.accepted === true }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "60e8bbba-b8fa-4d98-8f52-342a61e96684",
      "name": "Poll Context",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        4840,
        100
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const c = $input.first().json;\nconst allowed = !!c.acceptedTaskId && c.pollCount < 60 && Date.now() < c.deadlineAt;\nreturn [{json: {...c, taskId: c.acceptedTaskId || c.taskId || '', pollAllowed: allowed, pollCount: c.pollCount + (allowed ? 1 : 0), status: allowed ? 'processing' : 'needs_review', error: allowed ? '' : 'The polling deadline was reached. The task may still finish; retrieve the same task ID instead of creating another image.'}, pairedItem: {item: 0}}];"
      }
    },
    {
      "id": "18a821a1-5f9e-4793-9143-b7e4bc581387",
      "name": "Within Polling Limit?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        5060,
        100
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "4f0a73a5-cafe-4ef3-a6c6-d22e2cb88aca",
              "leftValue": "={{ $json.pollAllowed === true }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "59958072-bbbe-42fe-8068-f15e48567f5a",
      "name": "Get Task",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        5280,
        200
      ],
      "parameters": {
        "method": "GET",
        "url": "https://api.spicyapi.ai/api/v1/jobs/recordInfo",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth",
        "options": {
          "timeout": 30000,
          "response": {
            "response": {
              "responseFormat": "json",
              "fullResponse": true,
              "neverError": true
            }
          },
          "redirect": {
            "redirect": {
              "followRedirects": false
            }
          }
        },
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "taskId",
              "value": "={{ $json.acceptedTaskId }}"
            }
          ]
        }
      },
      "credentials": {
        "httpBearerAuth": {
          "id": "",
          "name": "SpicyAPI Bearer"
        }
      },
      "onError": "continueRegularOutput"
    },
    {
      "id": "4db62057-e784-46e1-b128-3b4c5cf4953f",
      "name": "Check Task",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        5500,
        200
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const c = $('Poll Context').item.json;\nconst response = $input.first().json;\nconst b = response.body || {};\nconst t = b.data || {};\nconst valid = response.statusCode >= 200 && response.statusCode < 300 && b.code === 200 && t.taskId === c.acceptedTaskId;\nconst assets = Array.isArray(t.output?.assets) ? t.output.assets : [];\nconst pending = assets.some(a => a.pending === true && a.unavailable !== true);\nconst asset = assets.find(a => a.pending !== true && a.unavailable !== true && typeof a.url === 'string' && /^https:\\/\\//i.test(a.url) && String(a.mime || '').startsWith('image/'));\nconst ready = valid && t.state === 'succeeded' && !pending && !!asset;\nconst retryableLookup = !!response.error || [429,500,502,503,504].includes(Number(response.statusCode));\nconst waiting = (valid && (['queued','running'].includes(t.state) || (t.state === 'succeeded' && pending))) || (!valid && retryableLookup);\nconst keepWaiting = !ready && waiting && c.pollCount < 60 && Date.now() < c.deadlineAt;\nreturn [{json: {...c, taskId: c.acceptedTaskId, state: valid ? t.state : (c.state || ''), cost: valid ? String(t.cost ?? '') : (c.cost || ''), settled: valid ? t.settled : c.settled, resultUrl: ready ? asset.url : (c.resultUrl || ''), readyToDownload: ready, keepWaiting, status: ready || keepWaiting ? 'processing' : 'needs_review', error: ready || keepWaiting ? '' : (valid && ['failed','expired','canceled'].includes(t.state) ? String(t.errorMessage || t.errorCode || 'The accepted task ended without an image.') : 'No downloadable image was confirmed before the deadline. Retrieve this accepted task ID; do not generate a replacement automatically.'), lookupRequestId: b.request_id || ''}, pairedItem: {item: 0}}];"
      }
    },
    {
      "id": "c23e9ddf-f987-4eff-99f6-03c076be4010",
      "name": "Image Ready?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        5720,
        200
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "875e387e-39b0-4290-81a6-7d4ca2cee42d",
              "leftValue": "={{ $json.readyToDownload === true }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "5288c641-1ffe-4244-a04d-ab43ee3f7985",
      "name": "Keep Waiting?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        5940,
        360
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "46d76853-7d74-4d55-9733-5d11c5dc8d62",
              "leftValue": "={{ $json.keepWaiting === true }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "ea6b4c38-f301-47f4-b13a-804a31fc3a8f",
      "name": "Wait 5 Seconds",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [
        6160,
        460
      ],
      "parameters": {
        "resume": "timeInterval",
        "amount": 5,
        "unit": "seconds"
      },
      "webhookId": "87ab919f-24e9-4be3-8b04-708332f3a9f1"
    },
    {
      "id": "dbe7334b-983c-4af7-a136-ca4216c45c69",
      "name": "Download Context",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        5940,
        40
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "return [{json: {...$input.first().json}, pairedItem: {item: 0}}];"
      }
    },
    {
      "id": "450cc172-3525-43a5-95f5-5d3e57635b75",
      "name": "Download Image",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        6160,
        40
      ],
      "parameters": {
        "method": "GET",
        "url": "={{ $json.resultUrl }}",
        "authentication": "none",
        "options": {
          "timeout": 30000,
          "response": {
            "response": {
              "responseFormat": "file",
              "outputPropertyName": "data",
              "fullResponse": true
            }
          },
          "redirect": {
            "redirect": {
              "followRedirects": true,
              "maxRedirects": 3
            }
          }
        }
      },
      "onError": "continueRegularOutput"
    },
    {
      "id": "f561e430-f966-46f1-a54e-8f9f0742462c",
      "name": "Check Download",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        6380,
        40
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const c = $('Download Context').item.json;\nconst item = $input.first();\nconst response = item.json;\nconst mime = String(item.binary?.data?.mimeType || '').toLowerCase();\nconst ok = response.statusCode >= 200 && response.statusCode < 300 && !!item.binary?.data && mime.startsWith('image/');\nreturn [{json: {...c, status: ok ? 'done' : 'needs_review', error: ok ? '' : 'The task succeeded but its image could not be downloaded. Retrieve this same task ID to refresh the result URL; do not create a new image.'}, ...(ok ? {binary: item.binary} : {}), pairedItem: {item: 0}}];"
      }
    },
    {
      "id": "2cad25fe-2409-4060-ab35-ab88251ff4f0",
      "name": "Final Result",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        6600,
        280
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const item = $input.first();\nconst c = item.json;\nconst done = c.status === 'done' && !!item.binary?.data;\nreturn [{json: {id: c.id || '', sceneNumber: c.sceneNumber || 0, sceneName: c.sceneName || '', status: done ? 'done' : 'needs_review', taskId: c.acceptedTaskId || c.taskId || '', acceptedTaskId: c.acceptedTaskId || c.taskId || '', idempotencyKey: c.idempotencyKey || '', firstAttemptAt: c.firstAttemptAt || '', requestJson: c.requestJson || (c.model && c.input ? JSON.stringify({model: c.model, input: c.input}) : ''), resultUrl: c.resultUrl || '', cost: c.cost || '', error: done ? '' : String(c.error || 'Inspect the saved execution and Console before submitting any replacement.'), batchMaxCostUsd: c.batchMaxCostUsd || 0, requestId: c.requestId || '', quoteRequestId: c.quoteRequestId || '', lookupRequestId: c.lookupRequestId || ''}, ...(done ? {binary: item.binary} : {}), pairedItem: {item: 0}}];"
      }
    },
    {
      "id": "c47a0823-9539-4453-b104-aadfcb6b1332",
      "name": "Setup and Recovery",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        0,
        -820
      ],
      "parameters": {
        "content": "## Set up before running\nImport into n8n Cloud. No community node is required. In Quote Request, Create Task and Get Task, choose Generic Credential Type → Bearer Auth and select the same credential. Configure Settings, then set enabled=true. The workflow is inactive and starts manually.\n\nSet referenceImageUrl, photoCount (4/8/12), maxCostUsd (up to $0.10 per image), and batchId (a current UTC ISO timestamp such as YYYY-MM-DDTHH:mm:ssZ). Keep batchId and the original settings unchanged when replaying the same batch within 23 hours. Use a new timestamp only for a deliberately new paid batch. For accepted-task recovery, put up to 12 task IDs in resumeTaskIdsJson, for example [\"YOUR_ACCEPTED_TASK_ID\"]. This bypasses quote and creation. Never replay a batch after the 23-hour guard; retrieve its accepted task IDs instead.\n\n## Cost and recovery\nEvery image has its own fresh quote and maximum-charge check. A price change gets one re-quote under the same key; a higher quote stops before another submission. Poll every 5 seconds, for at most 60 lookups or 300 seconds per item (plus an in-flight HTTP request). Accepted tasks are never automatically recreated. needs_review retains taskId, replay key and request IDs. Stop waiting does not cancel a task. Download Image uses no API credential. Batch Results contains binary.data for successful images. resultUrl is temporary; save copies to your own storage. A Sheets write failure stops the run, leaving the accepted ID in the saved execution.\n\nLocal contract and simulated execution checks are documented on the download page; no real Cloud import or paid generation is claimed.",
        "height": 700,
        "width": 1000,
        "color": 5
      }
    }
  ],
  "connections": {
    "Start": {
      "main": [
        [
          {
            "node": "Settings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Settings": {
      "main": [
        [
          {
            "node": "Build Scenes",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Scenes": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Over Items": {
      "main": [
        [
          {
            "node": "Batch Results",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Item Context",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Item Context": {
      "main": [
        [
          {
            "node": "Skip Submission?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Skip Submission?": {
      "main": [
        [
          {
            "node": "Final Result",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Existing Task?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Existing Task?": {
      "main": [
        [
          {
            "node": "Poll Context",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Quote Context",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Quote Context": {
      "main": [
        [
          {
            "node": "Quote Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Quote Request": {
      "main": [
        [
          {
            "node": "Check Quote",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Quote": {
      "main": [
        [
          {
            "node": "Quote Approved?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Quote Approved?": {
      "main": [
        [
          {
            "node": "Submit Context",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Final Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Submit Context": {
      "main": [
        [
          {
            "node": "Create Task",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Task": {
      "main": [
        [
          {
            "node": "Check Submission",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Submission": {
      "main": [
        [
          {
            "node": "Refresh Price Once?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Refresh Price Once?": {
      "main": [
        [
          {
            "node": "Quote Context",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Task Accepted?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Task Accepted?": {
      "main": [
        [
          {
            "node": "Poll Context",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Final Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Poll Context": {
      "main": [
        [
          {
            "node": "Within Polling Limit?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Within Polling Limit?": {
      "main": [
        [
          {
            "node": "Get Task",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Final Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Task": {
      "main": [
        [
          {
            "node": "Check Task",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Task": {
      "main": [
        [
          {
            "node": "Image Ready?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Image Ready?": {
      "main": [
        [
          {
            "node": "Download Context",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Keep Waiting?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Keep Waiting?": {
      "main": [
        [
          {
            "node": "Wait 5 Seconds",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Final Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait 5 Seconds": {
      "main": [
        [
          {
            "node": "Poll Context",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download Context": {
      "main": [
        [
          {
            "node": "Download Image",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download Image": {
      "main": [
        [
          {
            "node": "Check Download",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Download": {
      "main": [
        [
          {
            "node": "Final Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Final Result": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1",
    "saveDataErrorExecution": "all",
    "saveDataSuccessExecution": "all",
    "saveExecutionProgress": true,
    "executionTimeout": 4500
  },
  "pinData": {},
  "tags": []
}
