YouTube transcript in Make.com
Make's YouTube app can list a channel's uploads, and that is where it stops: the Data API behind it refuses to download captions for videos you do not own. Since Integromat days the answer has been the generic HTTP module. Here is exactly how to configure it against https://api.transcriptapi.io/transcript, how to turn the response into text a model or a document can use, and what to do when a video has no captions.
HTTP → Make a request: settings
Add the HTTP app, action Make a request. Fill in these fields and leave the rest at their defaults.
| Field | Value |
|---|---|
| URL | https://api.transcriptapi.io/transcript |
| Method | GET |
| Headers | Name Authorization, Value Bearer ta_your_key |
| Query Parameters | video_id → the mapped id from the previous module, e.g. {{2.videoId}}; language → en; optional translate_to → de |
| Body type | none (it is a GET) |
| Parse response | Yes. Without it data is a string and nothing downstream can map the segments. |
| Return error if HTTP request fails | No for the Router pattern below; Yes if you prefer error handlers. |
Run the module once so Make learns the output structure. You get statusCode, headers and data, where data.transcript is an array of segments with start and duration in seconds:
{
"video_id": "dQw4w9WgXcQ",
"transcript": [
{ "start": 0.0, "duration": 3.52, "text": "We're no strangers to love" },
{ "start": 3.52, "duration": 2.88, "text": "You know the rules and so do I" }
]
}The key goes into the header and nowhere else. Make stores it with the scenario; if you share a blueprint, strip the header first or rotate the key in the dashboard afterwards. The endpoint takes video_id, language and translate_to; the full reference is in the API docs.
Get the 11-character id out of any URL
Triggers rarely give you a bare id. RSS gives a watch URL, a form gives whatever the person pasted, a Notion database gives a youtu.be link with tracking parameters. Add a Tools → Set variable module named videoId with this value, replacing 1.url with the field that holds the link:
{{replace(1.url; "/.*(?:v=|youtu\.be\/|shorts\/|embed\/|live\/)([A-Za-z0-9_-]{11}).*/"; "$1")}}Make's replace() accepts a regular expression between slashes and $1 for the capture group. The pattern matches watch?v=, youtu.be/, Shorts, embed and live URLs. If you would rather see the match than trust a formula, the Text parser → Match pattern module does the same with the pattern (?:v=|youtu\.be/|shorts/)([A-Za-z0-9_-]{11}).
Send a full URL as video_id and the API answers 422 without charging you. The same happens if the mapping text itself gets through:
HTTP 422
{
"detail": "video_id still contains an unsubstituted template placeholder ('{{2.videoId}}'). Your automation sent the variable rather than its value — check the expression that builds this request."
}That one means the field contained the literal characters of a mapping rather than its value — usually a value pasted as text instead of dragged in from the mapping panel. We check for {{, ${ and similar markers before touching YouTube, so the request stops at our door.
Join the segments into one text
Most people do not need the timestamps for a summary. One formula in any text field does it, with 3 being the number of the HTTP module:
{{join(map(3.data.transcript; "text"); " ")}}map() takes the text of every segment and join() glues them with a space. A one-hour talk is roughly 9,000 words, well within what a Make text field and an OpenAI module will accept.
When you do want timestamps — to build show notes, or so a model can cite [12:40] — use an Iterator on 3.data.transcript followed by a Text aggregator with a newline separator and this row template:
[{{formatDate(parseDate(4.start; "X"); "mm:ss")}}] {{4.text}}The iterator route costs one operation per segment, so a two-hour video can burn a thousand operations. Group segments first if that matters, or do the formatting in the model prompt instead.
Scenario: new channel video → summary in Google Docs
The full scenario, six modules, no Google OAuth needed for the trigger:
- RSS → Watch RSS feed items. YouTube publishes a feed per channel; paste the URL below with the channel's
UC…id. Poll every 15 minutes. (The YouTube app's "Watch videos in a channel" trigger works too, at the cost of a Google connection.) - Tools → Set variable
videoIdwith the formula above, from1.url. - HTTP → Make a request as configured in the first section.
- Router with the routes from the next section.
- OpenAI → Create a completion(or Anthropic's app). System: "Summarise the transcript in five bullets and list every number or name mentioned." User:
{{join(map(3.data.transcript; "text"); " ")}}. - Google Docs → Create a document titled
{{1.title}}with the completion as the body — or Notion, Slack, a spreadsheet row.
https://www.youtube.com/feeds/videos.xml?channel_id=UC_x5XG1OV2P6uZZ5FSM9Ttw
Auto-generated captions can trail an upload by minutes to hours. A brand-new video often returns 404 NoTranscriptFound on the first attempt, refunded. Either add a Sleep module of a few minutes before the HTTP call, or accept that a few videos will be picked up on the next scenario run. If you would rather we poll the channel, POST /channel/subscribe with a Make Custom webhook URL delivers {channel_id, video_id} to the scenario instead — see the cookbook.
Error handling: 402, 404 and the rest
Not every video has a transcript. Captions off, private, deleted or age-restricted videos return 404, the credit is refunded, and the body says so in plain words. The field that matters for a scenario is retryable:
HTTP 404
{
"detail": "This video has captions turned off, so there is no transcript to fetch. Your credits have been refunded.",
"error": "TranscriptsDisabled",
"hint": "Retrying will not help — the uploader has disabled captions for this video.",
"retryable": false,
"credits_refunded": true
}With Return error if HTTP request fails set to No, the HTTP module never throws; statusCode and data are always there to filter on. A Router sorts the outcomes:
Router after the HTTP module (Return error if HTTP request fails = No)
Route 1 filter: {{3.statusCode}} = 200
→ Set variable "text" → OpenAI → Google Docs
Route 2 filter: {{3.data.retryable}} = false
→ Google Sheets "Add a row": {{2.videoId}}, {{3.data.detail}}
(captions off / private / deleted — refunded, do not retry)
Route 3 filter: {{3.statusCode}} = 402
→ Slack / email: "TranscriptAPI balance is empty"
Route 4 fallback (502 / 503, refunded)
→ Sleep 30 s → HTTP module again, or leave it for the next runPrefer error handlers? Set the option to Yes, right-click the HTTP module, Add error handler, and use Break for 402 — the run is stored as an incomplete execution you can resume after topping up — and Ignore or Resume (with an empty text) for 404, since retrying cannot change a video's captions. 502 and 503are temporary, refunded, and safe to retry with Break's automatic retries.
What it costs
One credit per transcript. A cached repeat of the same video is free, a refunded failure is free, and the channel webhook is free. Translation is one extra credit per 40 segments, minimum two. A channel that uploads daily is roughly 30 credits a month.
Accounts start with 20 free credits and no card; 2,500 credits are a $10 one-time top-up that never expires; the Production plan is $49 a month for 25,000. Full table on the pricing page.
What we do not do: download video or audio, return comments or channel statistics, or stand in for the Data API on a channel you manage. If a video has no caption track, there is nothing to fetch and you are not charged.
Frequently asked questions
Does Make.com have a YouTube transcript module?
No. Make's YouTube app talks to the YouTube Data API, which lists caption tracks but only lets the video's owner download them. For any other public video you use the generic HTTP module and point it at TranscriptAPI, which returns the transcript as JSON segments.
Does this still work if I set it up back in the Integromat days?
Yes. The module was called HTTP → Make a request then too, with Query String instead of Query Parameters and 'Evaluate all states as errors' instead of 'Return error if HTTP request fails'. The mapping and the API call are identical.
How do I turn the segments into one block of text?
Put {{join(map(3.data.transcript; "text"); " ")}} in any text field, replacing 3 with your HTTP module's number. map() pulls the text out of every segment and join() glues them with a space. Use an Iterator plus a Text aggregator instead when you want timestamps in front of each line.
How many operations does a run use?
Make bills one operation per module execution, so the minimal scenario is three or four operations per video: trigger, HTTP, one formula module, one output module. On our side it is one credit per transcript, nothing for a cached repeat, nothing for a refunded failure. Accounts start with 20 free credits; 2,500 more are $10 and never expire.
Why does the HTTP module return 422 with 'unsubstituted template placeholder'?
The literal text of a mapping reached the API instead of its value, usually because the id was pasted into a field as text rather than mapped from the previous module, or because the previous module produced nothing. TranscriptAPI rejects placeholder strings before fetching, so the call is not charged.
Can the scenario translate the transcript?
Yes. Add translate_to to the query parameters with a language code or name (fr, French). It costs one extra credit per 40 segments, minimum two, and the response includes translated_to. Summarising in another language is usually cheaper done in the OpenAI prompt instead.
The same integration in n8n and Zapier; in code, see LangChain. If an AI assistant should fetch the transcript itself, the MCP server skips the scenario entirely. Get a free key with 20 credits at /login.