n8n integration

Auto captions in n8n

An importable n8n workflow that sends a video link to the AwesomeVideo caption API and gets back the MP4 with styled, animated captions burned in, plus SRT and VTT. Core nodes only, no FFmpeg, works on n8n Cloud. 5 credits (about $0.05) per minute of video.

What the workflow does

  1. Run it→
  2. Settings→
  3. Caption the video→
  4. Wait 5 s→
  5. Check the job→
  6. Done?→
  7. Captioned video

If the job is still running, the loop waits 5 seconds and checks again, up to 120 times (10 minutes). A failed job stops the run with the reason; failed jobs are refunded.

Set it up

  1. 1. Get an API key

    Create a key at awesomevideoeditor.com/account, or from a terminal with no sign-up. While the trial runs, a new self-serve key can claim a few free welcome credits (see /facts). A short balance answers 402 with a checkout link and nothing is charged.

    curl -X POST https://api.awesomevideoeditor.com/v1/keys/anonymous -H "Content-Type: application/json" -d '{"name":"n8n"}'
  2. 2. Import the workflow

    Download the workflow JSON below. In n8n, open a new workflow, then use the ... menu > Import from File (or paste the JSON onto the canvas). It works on n8n Cloud and self-hosted: only core nodes (HTTP Request, Wait, If, Edit Fields), no community node and no FFmpeg.

  3. 3. Add the credential

    Open Caption the video, pick Credential > Create new (Header Auth). Name: Authorization. Value: Bearer followed by your key. Select the same credential in Check the job.

    Name:  Authorization
    Value: Bearer av_live_...
  4. 4. Set the video and style

    In Settings put a public link to the video (direct file, Google Drive, Dropbox or OneDrive share link; not YouTube). Pick a preset from /styles, a platform (tiktok, reels, shorts, youtube or none) to keep captions clear of the app buttons, and translateTo if you want the captions in another language. maxCredits refuses any job that would cost more.

  5. 5. Run it

    Click Execute workflow. The first node starts the job and returns at once; the loop checks GET /v1/jobs/{id} every 5 seconds. When status is succeeded, Captioned video holds the MP4 link, the SRT and VTT text, the language and the credits charged. A failed job is refunded and the workflow stops with the reason.

  6. 6. Wire it into your flow

    Replace Run it with any trigger (Google Drive new file, a form, a webhook, a schedule) and map its link into Settings > videoUrl. After Captioned video, add an HTTP Request node to download videoUrl as a file, then upload it to Drive, S3, YouTube or Slack. Result links are signed and expire after 24 hours, so download them in the same run.

The two API calls

What the HTTP Request nodes send, and the real responses for the 8-second CC0 sample (trimmed; run on November 4, 2026). Use the same calls from Make, Zapier or any HTTP client.

POST https://api.awesomevideoeditor.com/v1/captions
Authorization: Bearer av_live_...
Content-Type: application/json

{
  "sourceUrl": "https://awesomevideoeditor.com/samples/cc0-interview-es.mp4",
  "preset": "builtin:karaoke",
  "platform": "tiktok",
  "maxCredits": 50
}
202
{
  "id": "382d429a-...",
  "kind": "caption",
  "status": "queued",
  "audioSeconds": 8,
  "costCents": 2,
  "steps": ["transcribe", "burn", "finalize"],
  "eta_seconds": 16,
  "pollAfterSeconds": 5
}
GET https://api.awesomevideoeditor.com/v1/jobs/382d429a-...
200
{
  "id": "382d429a-...",
  "status": "succeeded",
  "step": "done",
  "progress": 100,
  "costCents": 2,
  "result": {
    "language": "es",
    "creditsCharged": 2,
    "files": [
      { "name": "mp4", "contentType": "video/mp4", "url": "https://api.awesomevideoeditor.com/v1/jobs/382d429a-.../result?...&file=mp4" },
      { "name": "srt", "text": "1\n00:00:00,000 --> 00:00:02,380\nen la escuela es súper diferente al mundo\n..." },
      { "name": "vtt", "text": "WEBVTT\n\n00:00:00.000 --> 00:00:02.380\n..." },
      { "name": "json", "text": "{ \"language\": \"es\", \"cues\": [...] }" }
    ]
  }
}

Every job view also has step, progress (0-100), eta_seconds and pollAfterSeconds. Full schema: OpenAPI.

Workflow JSON

Copy it and paste it onto an n8n canvas, or download the file above. The key lives in your n8n credential, not in the JSON.

Show the workflow (9 nodes)
{
  "name": "Burn captions into a video with AwesomeVideo",
  "nodes": [
    {
      "parameters": {},
      "id": "8f0d1a52-1b8e-4d3c-9a51-0c5b8f7a6e01",
      "name": "Run it",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        0
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "a1",
              "name": "videoUrl",
              "value": "https://awesomevideoeditor.com/samples/cc0-interview-es.mp4",
              "type": "string"
            },
            {
              "id": "a2",
              "name": "preset",
              "value": "builtin:karaoke",
              "type": "string"
            },
            {
              "id": "a3",
              "name": "platform",
              "value": "tiktok",
              "type": "string"
            },
            {
              "id": "a4",
              "name": "translateTo",
              "value": "",
              "type": "string"
            },
            {
              "id": "a5",
              "name": "maxCredits",
              "value": 50,
              "type": "number"
            }
          ]
        },
        "options": {}
      },
      "id": "8f0d1a52-1b8e-4d3c-9a51-0c5b8f7a6e02",
      "name": "Settings",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        220,
        0
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.awesomevideoeditor.com/v1/captions",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify(Object.assign({ sourceUrl: $json.videoUrl, preset: $json.preset, platform: $json.platform, maxCredits: $json.maxCredits }, $json.translateTo ? { translateTo: $json.translateTo } : {})) }}",
        "options": {}
      },
      "id": "8f0d1a52-1b8e-4d3c-9a51-0c5b8f7a6e03",
      "name": "Caption the video",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        440,
        0
      ]
    },
    {
      "parameters": {
        "amount": 5,
        "unit": "seconds"
      },
      "id": "8f0d1a52-1b8e-4d3c-9a51-0c5b8f7a6e04",
      "name": "Wait 5 s",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [
        660,
        0
      ],
      "webhookId": "8f0d1a52-1b8e-4d3c-9a51-0c5b8f7a6e0a"
    },
    {
      "parameters": {
        "url": "=https://api.awesomevideoeditor.com/v1/jobs/{{ $('Caption the video').first().json.id }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {}
      },
      "id": "8f0d1a52-1b8e-4d3c-9a51-0c5b8f7a6e05",
      "name": "Check the job",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        880,
        0
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "c1",
              "leftValue": "={{ $json.status }}",
              "rightValue": "succeeded",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "8f0d1a52-1b8e-4d3c-9a51-0c5b8f7a6e06",
      "name": "Done?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        1100,
        0
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "r1",
              "name": "videoUrl",
              "value": "={{ $json.result.files.find(f => f.name === 'mp4').url }}",
              "type": "string"
            },
            {
              "id": "r2",
              "name": "srt",
              "value": "={{ $json.result.files.find(f => f.name === 'srt').text }}",
              "type": "string"
            },
            {
              "id": "r3",
              "name": "vtt",
              "value": "={{ $json.result.files.find(f => f.name === 'vtt').text }}",
              "type": "string"
            },
            {
              "id": "r4",
              "name": "language",
              "value": "={{ $json.result.language }}",
              "type": "string"
            },
            {
              "id": "r5",
              "name": "creditsCharged",
              "value": "={{ $json.result.creditsCharged }}",
              "type": "number"
            },
            {
              "id": "r6",
              "name": "jobId",
              "value": "={{ $json.id }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "8f0d1a52-1b8e-4d3c-9a51-0c5b8f7a6e07",
      "name": "Captioned video",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        1320,
        -100
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "c2",
              "leftValue": "={{ $json.status }}",
              "rightValue": "failed",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            },
            {
              "id": "c3",
              "leftValue": "={{ $runIndex }}",
              "rightValue": 120,
              "operator": {
                "type": "number",
                "operation": "gte"
              }
            }
          ],
          "combinator": "or"
        },
        "options": {}
      },
      "id": "8f0d1a52-1b8e-4d3c-9a51-0c5b8f7a6e08",
      "name": "Failed or too long?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        1320,
        100
      ]
    },
    {
      "parameters": {
        "errorMessage": "={{ $json.status === 'failed' ? 'Job ' + $json.id + ' failed (refunded): ' + JSON.stringify($json.error) : 'Job ' + $json.id + ' still ' + $json.status + ' after ' + $runIndex + ' checks' }}"
      },
      "id": "8f0d1a52-1b8e-4d3c-9a51-0c5b8f7a6e09",
      "name": "Stop with the reason",
      "type": "n8n-nodes-base.stopAndError",
      "typeVersion": 1,
      "position": [
        1540,
        100
      ]
    }
  ],
  "connections": {
    "Run it": {
      "main": [
        [
          {
            "node": "Settings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Settings": {
      "main": [
        [
          {
            "node": "Caption the video",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Caption the video": {
      "main": [
        [
          {
            "node": "Wait 5 s",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait 5 s": {
      "main": [
        [
          {
            "node": "Check the job",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check the job": {
      "main": [
        [
          {
            "node": "Done?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Done?": {
      "main": [
        [
          {
            "node": "Captioned video",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Failed or too long?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Failed or too long?": {
      "main": [
        [
          {
            "node": "Stop with the reason",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Wait 5 s",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "pinData": {},
  "settings": {
    "executionOrder": "v1"
  },
  "meta": {
    "templateCredsSetupCompleted": false
  },
  "tags": []
}

Questions

Do I need a community node or FFmpeg in n8n?
No. The workflow uses only core nodes: HTTP Request, Wait, If, Edit Fields and Stop and Error. Transcription and the burn run on the AwesomeVideo server, so it runs on n8n Cloud unchanged.
How much does it cost per video?
5 credits per minute of audio (1 to transcribe, 4 to burn), 7 with translateTo. 1 credit is 1 US cent, so about $0.05 per minute, billed per second with each step rounded up to the next cent. The 8-second sample costs 2 credits. Failed jobs are refunded.
Which video links work?
Any public https link to the file: a direct MP4 or MOV URL, or a Google Drive, Dropbox or OneDrive share link. YouTube, TikTok and Instagram page links are refused with charged: false; download the file first and upload it with POST /v1/uploads/url.
Can I skip polling?
Yes. Short clips can send wait: true (or waitSeconds up to 50) and get the finished job in the same response. For long videos pass webhookUrl (public https) in the request and AwesomeVideo sends a signed POST to an n8n Webhook node when the job ends.
Does it work with Make or Zapier?
The same two calls work from any HTTP module: POST /v1/captions with the Authorization header, then GET /v1/jobs/{id} until status is succeeded. This page ships a ready workflow only for n8n.

More

n8n is a trademark of n8n GmbH; this is an independent integration. Sample clip: “Entrevista Maricarmen Canales - TECMTY” by NachoCHOCO, CC0, via Wikimedia Commons.