{
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "nodes": [
    {
      "id": "a05ea6b6-03cd-4915-b716-2dd0b0b54e95",
      "name": "\ud83d\udccb Flow Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -672,
        -416
      ],
      "parameters": {
        "width": 740,
        "height": 580,
        "content": "## \ud83c\udfac OpenAI Sora \u2013 Video Generation \u2192 Upload to URL\n\n**How it works:**\n1. Webhook receives a POST with `jobType`: **ecommerce** or **remix**\n2. **Route by Job Type** switch sends it down the correct pipeline\n3. A code node crafts the **Sora prompt** and submits the job via `POST /v1/video/generations`\n4. Sora is **async** \u2014 a **Wait \u2192 Poll \u2192 IF** loop checks status every 20 seconds\n5. When `status = succeeded`, an HTTP GET node **downloads the MP4 as binary**\n6. The binary is passed to the **n8n Upload to URL node** which PUTs the MP4 to your CDN\n7. A code node builds the **JSON response** with the public CDN URL\n\n**Credential needed:**\n`OpenAI Header Auth` \u2014 HTTP Header Auth\nHeader name: `Authorization`  \u00b7  Value: `Bearer YOUR_OPENAI_KEY`\nSora access required: platform.openai.com (Tier 4/5 or enterprise)\n\n**Sora API endpoints used:**\n- Submit:  `POST https://api.openai.com/v1/video/generations`\n- Poll:    `GET  https://api.openai.com/v1/video/generations/{id}`\n- Download: video URL from completed response\n\n"
      },
      "typeVersion": 1
    },
    {
      "id": "d3d90ba8-0ba0-41d0-941b-379b5034f085",
      "name": "Sticky \u2013 Entry & Routing",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -32,
        192
      ],
      "parameters": {
        "color": 7,
        "width": 560,
        "height": 924,
        "content": "### 1\ufe0f\u20e3 Webhook Entry & Job Router\n**Webhook \u2013 Receive Video Job** accepts a POST at `/sora-video-job` with `jobType` and generation parameters in the request body.\n**Route by Job Type Switch** reads `$json.body.jobType` and routes:\n- Output 0 `ecommerce` \u2192 Cinematic E-commerce pipeline\n- Output 1 `remix` \u2192 Social Media Remix pipeline\n- Fallback \u2192 **Respond \u2013 Error** returns a 400 JSON error"
      },
      "typeVersion": 1
    },
    {
      "id": "e1b88a43-f58c-4dfb-acf0-6355204a40b3",
      "name": "Sticky \u2013 E-commerce Pipeline",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        592,
        176
      ],
      "parameters": {
        "color": 7,
        "width": 2480,
        "height": 500,
        "content": "### 2\ufe0f\u20e3 Cinematic E-commerce Walkthrough\n**Build E-commerce Prompt** constructs the Sora prompt: cinematic product reveal, 360\u00b0 rotation, studio lighting, close-ups on key features, premium brand aesthetic. Injects `productName`, `duration` (10\u201320 s), `style`, and stamps a `jobId`.\n**Sora \u2013 Submit E-commerce Job** POSTs to `/v1/video/generations` with the prompt, `input_image` URL, duration and resolution `1080p`. Returns a `generation_id` immediately \u2014 Sora renders async.\n**Store E-commerce Job ID** code node carries `generationId` and all metadata forward.\n**Wait 20s** pauses execution while Sora renders.\n**Sora \u2013 Poll E-commerce Status** GETs `/v1/video/generations/{generationId}` to read the current status.\n**Check E-commerce Done IF** evaluates `$json.status`: `succeeded` \u2192 true branch (continue); anything else \u2192 false branch (loop back to Wait 20s).\n**Fetch E-commerce Video** HTTP GET downloads the MP4 binary from `$json.video_url`.\n**Upload to URL \u2013 E-commerce** is the **n8n built-in Upload to URL node** \u2014 PUTs the MP4 binary to your CDN presigned URL.\n**Build E-commerce Response** returns `{ publicUrl, productName, duration, jobId, generatedAt }`.\n**Respond to Webhook \u2013 E-commerce** sends JSON to caller."
      },
      "typeVersion": 1
    },
    {
      "id": "3daf779d-1dd3-430c-b015-b361b60d3fbd",
      "name": "Sticky \u2013 Remix Pipeline",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        592,
        704
      ],
      "parameters": {
        "color": 7,
        "width": 2480,
        "height": 480,
        "content": "### 3\ufe0f\u20e3 Dynamic Social Media Remix\n**Build Remix Prompt** constructs the Sora prompt: transforms the source image into a `remixStyle` visual (e.g. cyberpunk, anime, golden hour), adds cinematic camera movement and `addEffect`. Sets `aspectRatio: 9:16` for Reels/TikTok, `1:1` for Feed. Duration fixed at 8 s. Stamps a `jobId`.\n**Sora \u2013 Submit Remix Job** POSTs to `/v1/video/generations` with `input_image`, prompt, duration 8 and platform-appropriate aspect ratio.\n**Store Remix Job ID** carries `generationId` and metadata forward.\n**Wait 20s** pauses while Sora renders.\n**Sora \u2013 Poll Remix Status** GETs the generation status endpoint.\n**Check Remix Done IF** evaluates status: `succeeded` \u2192 true; else \u2192 false loops back to Wait 20s.\n**Fetch Remix Video** downloads the MP4 binary from the completed response.\n**Upload to URL \u2013 Remix** is the **n8n built-in Upload to URL node** \u2014 PUTs the MP4 to your CDN.\n**Build Remix Response** returns `{ publicUrl, remixStyle, platform, aspectRatio, jobId, generatedAt }`.\n**Respond to Webhook \u2013 Remix** sends JSON to caller."
      },
      "typeVersion": 1
    },
    {
      "id": "19282007-01ef-4021-9702-4a6f521ea0da",
      "name": "Webhook \u2013 Receive Video Job",
      "type": "n8n-nodes-base.webhook",
      "position": [
        0,
        608
      ],
      "parameters": {
        "path": "sora-video-job",
        "options": {},
        "httpMethod": "POST",
        "responseMode": "responseNode"
      },
      "typeVersion": 2
    },
    {
      "id": "aa7080f8-4aae-4f67-ba08-562f3d7e4180",
      "name": "Route by Job Type",
      "type": "n8n-nodes-base.switch",
      "position": [
        320,
        608
      ],
      "parameters": {
        "rules": {
          "values": [
            {
              "outputKey": "E-commerce",
              "conditions": {
                "options": {
                  "caseSensitive": false,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.body.jobType.toLowerCase() }}",
                    "rightValue": "ecommerce"
                  }
                ]
              },
              "renameOutput": true
            },
            {
              "outputKey": "Remix",
              "conditions": {
                "options": {
                  "caseSensitive": false,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.body.jobType.toLowerCase() }}",
                    "rightValue": "remix"
                  }
                ]
              },
              "renameOutput": true
            }
          ]
        },
        "options": {
          "fallbackOutput": "extra"
        }
      },
      "typeVersion": 3
    },
    {
      "id": "a66d6c39-f952-4303-b966-a7e7627e4a97",
      "name": "Respond \u2013 Error",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        336,
        928
      ],
      "parameters": {
        "options": {},
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify($json) }}"
      },
      "typeVersion": 1
    },
    {
      "id": "d1aaf53c-fa42-4eb4-bb77-42d3612824da",
      "name": "Build E-commerce Prompt",
      "type": "n8n-nodes-base.code",
      "position": [
        624,
        448
      ],
      "parameters": {
        "jsCode": "const b       = $json.body;\nconst product = b.productName     || 'Product';\nconst dur     = parseInt(b.duration || 15);\nconst style   = b.style           || 'premium product reveal';\nconst imgUrl  = b.productImageUrl || '';\nconst jobId   = `ECOM-${Date.now()}`;\nconst prompt  = `Cinematic ${dur}-second product video for ${product}. ` +\n  `Style: ${style}. Start with a dramatic reveal from black, ` +\n  `smoothly rotate the product 360 degrees under professional studio lighting, ` +\n  `close-up on key features with shallow depth of field, ` +\n  `end with the product centred on a clean white background. ` +\n  `4K quality, no text, premium commercial aesthetic.`;\nreturn [{ json: { prompt, product, duration: dur, style, productImageUrl: imgUrl, jobId } }];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "70b20b5c-dd27-44e7-92ae-278c26bd2553",
      "name": "Sora \u2013 Submit E-commerce Job",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        880,
        448
      ],
      "parameters": {
        "url": "https://api.openai.com/v1/video/generations",
        "method": "POST",
        "options": {},
        "sendHeaders": true,
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "e305f297-4187-4df1-a6c7-85f8d60e01a5",
      "name": "Store E-commerce Job ID",
      "type": "n8n-nodes-base.code",
      "position": [
        1152,
        448
      ],
      "parameters": {
        "jsCode": "const prev = $('Build E-commerce Prompt').item.json;\nconst genId = $json.id || $json.generation_id;\nif (!genId) throw new Error('No generation_id returned from Sora');\nreturn [{ json: { ...prev, generationId: genId } }];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "a7a3bb94-30d6-4da2-932e-0f7c96f990cc",
      "name": "Wait 20s \u2013 E-commerce",
      "type": "n8n-nodes-base.wait",
      "position": [
        1392,
        448
      ],
      "parameters": {
        "amount": 20
      },
      "typeVersion": 1.1
    },
    {
      "id": "6e696d0f-5dba-4c9d-ba98-7a0bb6ed5eea",
      "name": "Sora \u2013 Poll E-commerce Status",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1632,
        448
      ],
      "parameters": {
        "url": "=https://api.openai.com/v1/video/generations/{{ $json.generationId }}",
        "options": {}
      },
      "typeVersion": 4.2
    },
    {
      "id": "e13f76b5-6b3b-4386-b6cc-13c7bebeb947",
      "name": "Check E-commerce Done",
      "type": "n8n-nodes-base.if",
      "position": [
        1872,
        448
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "caseSensitive": false,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.status }}",
              "rightValue": "succeeded"
            }
          ]
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "afab4d94-8cc3-479c-8ff8-1f3ff6263bf6",
      "name": "Fetch E-commerce Video",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        2112,
        368
      ],
      "parameters": {
        "url": "={{ $json.video_url || $json.output?.url || $json.generations?.[0]?.url }}",
        "options": {}
      },
      "typeVersion": 4.2
    },
    {
      "id": "1ba3c0f3-a981-4bec-b377-5b4af8a1a192",
      "name": "Build E-commerce Response",
      "type": "n8n-nodes-base.code",
      "position": [
        2608,
        368
      ],
      "parameters": {
        "jsCode": "const p = $('Build E-commerce Prompt').item.json;\nreturn [{ json: {\n  success: true, jobId: p.jobId, jobType: 'ecommerce',\n  productName: p.product, duration: p.duration, style: p.style,\n  publicUrl: `https://YOUR_CDN_DOMAIN/${p.jobId}.mp4`,\n  generatedAt: new Date().toISOString(),\n  note: 'Embed this MP4 URL directly in your Shopify/Meesho product listing.'\n}}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "ce0c666a-fde5-4e5a-b65f-b3932eb9e564",
      "name": "Respond to Webhook \u2013 E-commerce",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        2848,
        368
      ],
      "parameters": {
        "options": {},
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify($json) }}"
      },
      "typeVersion": 1
    },
    {
      "id": "f371d4b1-a647-42b6-a007-756f60c4ca30",
      "name": "Build Remix Prompt",
      "type": "n8n-nodes-base.code",
      "position": [
        624,
        960
      ],
      "parameters": {
        "jsCode": "const b        = $json.body;\nconst style    = b.remixStyle     || 'cyberpunk';\nconst effect   = b.addEffect      || 'golden hour lighting';\nconst platform = (b.platform || 'reels').toLowerCase();\nconst srcImg   = b.sourceImageUrl || '';\nconst jobId    = `REMIX-${Date.now()}`;\nconst aspect   = ['reels','tiktok','story'].includes(platform) ? '9:16' : '1:1';\nconst dur      = 8;\nconst prompt   = `Transform this image into a ${dur}-second cinematic ${style} style video. ` +\n  `Apply ${effect} with smooth cinematic camera movement and dramatic transitions. ` +\n  `Vibrant colour grading, professional motion blur on cuts, ` +\n  `optimised for ${platform} vertical short-form format. No text overlays.`;\nreturn [{ json: { prompt, style, effect, platform, aspect, duration: dur, sourceImageUrl: srcImg, jobId } }];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "2549243b-f77b-4f20-bc13-8cffa7c586d5",
      "name": "Sora \u2013 Submit Remix Job",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        848,
        960
      ],
      "parameters": {
        "url": "https://api.openai.com/v1/video/generations",
        "method": "POST",
        "options": {},
        "sendHeaders": true,
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "434029b0-9ca9-4081-9760-5929a899632e",
      "name": "Store Remix Job ID",
      "type": "n8n-nodes-base.code",
      "position": [
        1152,
        960
      ],
      "parameters": {
        "jsCode": "const prev = $('Build Remix Prompt').item.json;\nconst genId = $json.id || $json.generation_id;\nif (!genId) throw new Error('No generation_id returned from Sora');\nreturn [{ json: { ...prev, generationId: genId } }];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "6cea91b0-5583-4062-8e74-e0de974d57af",
      "name": "Wait 20s \u2013 Remix",
      "type": "n8n-nodes-base.wait",
      "position": [
        1376,
        960
      ],
      "parameters": {
        "amount": 20
      },
      "typeVersion": 1.1
    },
    {
      "id": "ccc8ec96-c821-46e7-a38e-58b2f4585e40",
      "name": "Sora \u2013 Poll Remix Status",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1648,
        960
      ],
      "parameters": {
        "url": "=https://api.openai.com/v1/video/generations/{{ $json.generationId }}",
        "options": {}
      },
      "typeVersion": 4.2
    },
    {
      "id": "7665ced7-b14a-4e64-a607-31b48ae3ab92",
      "name": "Check Remix Done",
      "type": "n8n-nodes-base.if",
      "position": [
        1888,
        960
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "caseSensitive": false,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.status }}",
              "rightValue": "succeeded"
            }
          ]
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "7beeb3d4-c4d7-474f-bfbc-5123c3d66cc7",
      "name": "Fetch Remix Video",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        2128,
        944
      ],
      "parameters": {
        "url": "={{ $json.video_url || $json.output?.url || $json.generations?.[0]?.url }}",
        "options": {}
      },
      "typeVersion": 4.2
    },
    {
      "id": "9bddd891-bcc3-4321-8f84-43c087f514fc",
      "name": "Build Remix Response",
      "type": "n8n-nodes-base.code",
      "position": [
        2576,
        944
      ],
      "parameters": {
        "jsCode": "const p = $('Build Remix Prompt').item.json;\nreturn [{ json: {\n  success: true, jobId: p.jobId, jobType: 'remix',\n  remixStyle: p.style, effect: p.effect,\n  platform: p.platform, aspectRatio: p.aspect,\n  publicUrl: `https://YOUR_CDN_DOMAIN/${p.jobId}.mp4`,\n  generatedAt: new Date().toISOString(),\n  note: 'Post this URL directly to TikTok/Reels via their upload API.'\n}}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "a8fcc0ba-1093-4fe8-8718-98947247d385",
      "name": "Respond to Webhook \u2013 Remix",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        2832,
        944
      ],
      "parameters": {
        "options": {},
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify($json) }}"
      },
      "typeVersion": 1
    },
    {
      "id": "23ee271e-8b4f-46a1-875a-81d18e259f21",
      "name": "Upload to URL",
      "type": "n8n-nodes-uploadtourl.uploadToUrl",
      "position": [
        2368,
        368
      ],
      "parameters": {},
      "credentials": {
        "uploadToUrlApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "41c3168b-2bb4-4e41-a8d2-bb7fce5f45b0",
      "name": "Upload to URL1",
      "type": "n8n-nodes-uploadtourl.uploadToUrl",
      "position": [
        2352,
        944
      ],
      "parameters": {},
      "credentials": {
        "uploadToUrlApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    }
  ],
  "connections": {
    "Upload to URL": {
      "main": [
        [
          {
            "node": "Build E-commerce Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upload to URL1": {
      "main": [
        [
          {
            "node": "Build Remix Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Remix Done": {
      "main": [
        [
          {
            "node": "Fetch Remix Video",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Wait 20s \u2013 Remix",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Remix Video": {
      "main": [
        [
          {
            "node": "Upload to URL1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route by Job Type": {
      "main": [
        [
          {
            "node": "Build E-commerce Prompt",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Build Remix Prompt",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Respond \u2013 Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Remix Prompt": {
      "main": [
        [
          {
            "node": "Sora \u2013 Submit Remix Job",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Store Remix Job ID": {
      "main": [
        [
          {
            "node": "Wait 20s \u2013 Remix",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait 20s \u2013 Remix": {
      "main": [
        [
          {
            "node": "Sora \u2013 Poll Remix Status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Remix Response": {
      "main": [
        [
          {
            "node": "Respond to Webhook \u2013 Remix",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check E-commerce Done": {
      "main": [
        [
          {
            "node": "Fetch E-commerce Video",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Wait 20s \u2013 E-commerce",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch E-commerce Video": {
      "main": [
        [
          {
            "node": "Upload to URL",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build E-commerce Prompt": {
      "main": [
        [
          {
            "node": "Sora \u2013 Submit E-commerce Job",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Store E-commerce Job ID": {
      "main": [
        [
          {
            "node": "Wait 20s \u2013 E-commerce",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait 20s \u2013 E-commerce": {
      "main": [
        [
          {
            "node": "Sora \u2013 Poll E-commerce Status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build E-commerce Response": {
      "main": [
        [
          {
            "node": "Respond to Webhook \u2013 E-commerce",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Sora \u2013 Submit Remix Job": {
      "main": [
        [
          {
            "node": "Store Remix Job ID",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Sora \u2013 Poll Remix Status": {
      "main": [
        [
          {
            "node": "Check Remix Done",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Webhook \u2013 Receive Video Job": {
      "main": [
        [
          {
            "node": "Route by Job Type",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Sora \u2013 Submit E-commerce Job": {
      "main": [
        [
          {
            "node": "Store E-commerce Job ID",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Sora \u2013 Poll E-commerce Status": {
      "main": [
        [
          {
            "node": "Check E-commerce Done",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}