Skip to main content

n8n node

Release status: built, not yet published

The VieNeu community node is finished and tested, but the package has not been released publicly. It is marked private: true in its package.json and has never been pushed to npm, so there is nothing for you to install today from a public registry.

If you want to run it now, contact VieNeu and ask for the package — we can hand you a build. Watch the Cloud API changelog for the announcement when it reaches npm; the Install section below has both paths.

Everything else on this page — the operations, parameters, output fields and error behaviour — is accurate against the built package, so you can decide whether the node is what you want before asking for it.

VieNeu ships an n8n community node — one node, four resources, audio out as a binary field. It is a programmatic node, not a trigger: it sits in the middle of a workflow and turns Vietnamese text into a file the next node can send, store or upload.

There are two ways to call VieNeu from n8n:

  • The node — a voice picker with search, automatic routing between the short and long text endpoints, and API errors mapped onto n8n's machine-readable failure.cause. Needs the package (see above) and a self-hosted n8n.
  • Plain HTTP Request nodes — nothing to install, works on n8n Cloud, and available to everyone today. See Don't want to install the node? at the bottom. Two importable templates ship with the package.
Do not npm install n8n-nodes-vieneu

The name is unclaimed on npm, so whatever that command resolves to today is not this package. Do not install it and do not paste it into n8n's community-node dialog.

Install​

Self-hosted n8n only. n8n Cloud installs verified packages from npm, so the community-node path is unavailable there whatever happens — use the HTTP Request route instead.

When the package is published​

This is the shape the install will take. None of it works yet — the package is not on npm:

Settings → Community nodes → Install → n8n-nodes-vieneu

Nothing below will be needed then. Check the changelog before following the manual path.

Today​

Ask VieNeu for the package. What arrives is the source directory n8n-nodes-vieneu/, which you build yourself; the repository it lives in is private, so there is no git clone to give you.

Prerequisites:

RequirementWhere it comes from
Node 20.19 or newerengines.node: ">=20.19" in the package's package.json
pnpm — corepack enable is enoughthe package pins packageManager: "pnpm@10.24.0"; npm and yarn are not substitutes here (see below)
n8n bundling n8n-workflow 2.37.1 or newerthe version the package is compiled against, and the source imports NodeConnectionTypes, which older releases do not export

n8n does not publish a plain "minimum version" number for this; check what your instance actually bundles with npm ls n8n-workflow where n8n is installed. The package declares n8nNodesApiVersion: 1. On an n8n too old to export NodeConnectionTypes the node does not appear in the panel at all; on one too old for the Failure type, the failure.cause branching documented below does nothing.

Build it:

cd n8n-nodes-vieneu
pnpm install
pnpm build
pnpm verify

pnpm build is mandatory. dist/ is git-ignored, and dist/ is exactly what the n8n block in package.json points n8n at — a fresh copy has none.

pnpm verify is the check worth running. It require()s the built dist/ the way n8n's loader does and instantiates the exported class, which catches the three failures that otherwise show up only as a node that silently never appears in the panel: a package.json path that no longer matches the emitted layout, an icon tsc did not copy, and a class n8n cannot construct.

Use pnpm, not npm install. The package sits outside the repository's own workspace and carries an empty pnpm-workspace.yaml to make its directory a workspace root; without pnpm reading that file, an install run from here walks up to the repository root and installs nothing at all, exiting 0 with nothing to build from.

Then either copy the build in, or point n8n at the package directory:

# Option A — copy into n8n's private-node folder, then restart n8n.
mkdir -p ~/.n8n/custom
cp -r dist/nodes dist/credentials ~/.n8n/custom/

# Option B — leave it in place and point n8n at it.
export N8N_CUSTOM_EXTENSIONS="/abs/path/to/n8n-nodes-vieneu"
n8n start

Two mistakes that cost an afternoon:

  • ~/.n8n/custom is not ~/.n8n/nodes. The latter holds npm-installed community nodes; a private build placed there is ignored.
  • N8N_CUSTOM_EXTENSIONS takes a semicolon-separated list of absolute paths on every platform. A colon-separated list does not error — it loads nothing.

For Docker, mount the built package into the container and set that same variable to the in-container path.

The node should then appear as VieNeu in the node panel. Its full type is n8n-nodes-vieneu.vieneu.

This install path has not been exercised against a live n8n

The package compiles, its unit tests pass, and pnpm verify loads the built output in the shape n8n's loader expects — but nobody has yet loaded it into a running n8n instance, and the same is true of the two workflow templates, which are validated structurally rather than by importing them. Treat your first install as a smoke test, and tell us what breaks rather than assuming it is something you did.

Not an AI Agent tool

The node deliberately does not declare usableAsTool, so an AI Agent cannot call it. Speech generation spends the account's tokens on every call, and an agent invoking it speculatively is the wrong first experience. Agents have their own supported surface: the MCP server.

Credential​

One credential type, VieNeu API, with two fields and nothing else.

FieldName in workflow JSONRequiredDefault
API KeyapiKeyYes— (password-masked, placeholder vn_sk_…)
Base URLbaseUrlYeshttps://api.vieneu.io/api/v1

The /api/v1 prefix in the Base URL is real. The controller is mounted at v1 but the application sets a global api prefix, so the documented /v1/... paths are served at /api/v1/.... A base URL of https://api.vieneu.io/v1 404s on everything. Change this field only to point at staging or a self-hosted deployment; trailing slashes are trimmed, and an empty value falls back to the default.

Press Test after pasting a key. The test issues GET /voices against the configured base URL — it costs no tokens and is exempt from the rate limiter, but its guard still rejects a malformed or revoked key with 401, so a wrong key fails here instead of quietly returning the public catalogue.

Which keys are valid, and what a vn_test_ key can do, is the same everywhere: see Authentication. The one thing that matters in n8n is that a vn_test_ key's 100-word-per-request cap surfaces as a 400 on long text, not as a quota error — the node does not check the prefix or the word count locally.

The key never reaches a workflow variable​

This is the reason the key is a credential rather than a node parameter:

  • Node parameters are written into execution history, into the workflow JSON people paste into issues, and into log lines. A credential is encrypted at rest and referenced by id — it is not part of an exported workflow.
  • No code in the package ever reads apiKey. n8n builds Authorization: Bearer {{$credentials.apiKey}} itself, inside httpRequestWithAuthentication, from the credential's authenticate block. There is no variable holding the key that an expression or a log line could reach.
  • Every error message, description and attached response body is run through a redactor first. Bearer tokens and anything shaped like vn_sk_…, vn_test_… or sk_… become ***REDACTED*** before n8n stores them in execution data.

Resources and operations​

ResourceOperationCallsParameters
SpeechGeneratePOST /audio/speech or POST /tts + pollText, Engine, Voice, Put Output File in Field, Options
JobGetGET /tts/{jobId}Job ID, Download Audio, Put Output File in Field
VoiceGet ManyGET /voicesEngine, Search, Return All, Limit
EngineGet ManyGET /enginesnone

Engine: Get Many has no parameters of its own. It returns one item per engine, including the live billingMultiplier — read that rather than any table, including the snapshot in Engines.

What the node does not do​

Not everything the API offers is exposed, and the omissions are deliberate: an operation that spends real money or needs a file upload is a bad thing for a workflow to reach by accident, and a Loop Over Items reaches things many times.

MissingWhy
Voice cloning (POST /voices, /clone, /prepare, /upload)A flat 5,000-token charge before any multiplier (15,000 on v4) for one clip, a multipart upload, and a slot against the plan's clone limit. Clones you already own are selectable in the Voice list; you just cannot create one from a workflow.
DELETE /voices/:voiceIdDestructive, and irreversible from inside a workflow.
Dubbing and SRT (/dub, /srt)Both upload-shaped, both billed per call.
POST /dialogueKey-only and no upload, but 50 turns × 5,000 characters is uncapped in the request schema — by a wide margin the largest single-call exposure on the surface.
Streaming (POST /tts/stream)Its response is a custom length-prefixed frame format with heartbeat frames mixed in; n8n has no frame parser and would hand back one opaque buffer. It also caps at 4 concurrent streams per key, and every stream bills at the v4 multiplier whatever engine was sent. See Streaming if you need that surface.

Picking a voice​

The Voice field is a resource locator with two modes:

  • From List — searchable, backed by GET /voices. Filtering happens in the node, because the API has no search parameter: it folds diacritics (đ/Đ included) and requires every word to match against id, name, description, gender, region, engine and kind. Rows read Name — gender · region · engine (id), with · your clone inside the facet group for any voice the API returns as kind: "cloned" — your own clones and admin-published ones alike, so a public clone somebody else created is labelled that way too. The list pages 250 at a time.
  • By ID — for expressions. Copy the id exactly, diacritics included.

Setting Engine first scopes the list, and on Speech: Generate the list re-loads when Engine changes. Do that: a voice renders only on its own engine and is never substituted — a mismatch is a hard 400. The dropdown is loaded live from GET /engines, so since v3 was retired on 2026-09-24 it offers v4 alone; the only mismatch left is an old v3 slug pasted By ID.

A clone_… voice does not work on short text

The short-text route the node picks by default, POST /audio/speech, rejects every cloned voice outright — its voice check runs without a user id, so it cannot tell your clone from anyone's and refuses them all with a 400 reading "Cloned voice … can only be used on POST /v1/tts or POST /v1/tts/stream." This applies to admin-published clones too.

The long-text route is POST /tts, which does accept clones — your own and published ones. So to use a clone_… voice from the node, keep the item over the Synchronous Route Threshold, or set Options → Synchronous Route Threshold to 1 to force every item onto the async route.

Do not follow the node's 400 advice here. It says the usual cause is a voice belonging to the other engine, which is true of catalogue voices and has nothing to do with this.

Ids are not descriptive. Many are opaque slugs such as vieneu-2-000494 whose voice is named Minh Đức, and a few are an unrelated person's name outright. Never infer gender or identity from an id.

Speech: Generate​

ParameterTypeDefaultNotes
Textstring (4 rows)—Required. Billed per character with a 50-character minimum — see Billing.
Engine Name or IDoptionsaccount defaultEmpty = the account default decides, which also decides the rate.
Voiceresource locatorengine defaultScoped by Engine.
Put Output File in FieldstringdataRequired. The binary property the audio lands in.
Optionscollection{}Nine options, below.

Options​

OptionNameDefaultNotes
AI RefineaiRefinefalseRuns AI normalisation and moderation first. Costs a surcharge on top of the character count and adds latency. No /v1 route reports the surcharge — the node's help text cites 1.3× at the time of writing. It is also the only thing that can produce a 422.
Emotion Name or IDemotionengine defaultLoaded from GET /emotion-tags for the selected engine. Engine-specific: v4 has no styles, so the list stays empty there.
File NamefileNamederivedEmpty derives a name from the format the API actually produced.
Formatformatwavwav, mp3, opus, pcm, ulaw.
Download AudiodownloadAudiotrueLong-text route only — see below.
Job Timeout (Seconds)jobTimeout600Minimum 5. How long to poll a long-text job.
Poll Interval (Seconds)pollInterval2Minimum 0.5, and clamped to 500 ms in code regardless of what the workflow JSON says.
Speedspeed10.5–2.0.
Synchronous Route ThresholdsyncMaxChars500Minimum 1. Where the route split happens.

The node sends no sample rate and exposes no option for one.

You may not need Job Timeout at all

Job Timeout exists because the node polls. If your jobs are long enough that the timeout is a real risk, register a webhook instead: VieNeu POSTs a signed event when the job reaches a terminal state, and an n8n Webhook trigger node receives it. POST /tts — the route this option governs — is the only route that produces those events. See Webhooks.

Two routes, chosen by text length​

Short textLong text
Conditionlength ≤ syncMaxChars (default 500)longer
EndpointPOST /audio/speech (blocking)POST /tts, then poll GET /tts/{jobId}
Body fieldsinput, response_format, speed, and voice / engine / emotion / aiRefine when settext, speed, and voiceId / engine / emotion / aiRefine when set
Formatsall fiveWAV only — the route has no format parameter
Cloned voicesrejected with 400accepted (yours and admin-published)
Download Audioignored; bytes come back inlineapplies
Extra JSON outsampleRate, requestIdjobId, duration, audioUrl, audioUrlExpiresIn

format and mimeType are set on both routes and are listed with the shared fields below — do not treat their presence as a signal of which route an item took. Read route for that.

Note the field names differ between the routes (input/voice versus text/voiceId). The node handles that; it matters only if you copy a body between the node and an HTTP Request node.

Raising syncMaxChars makes the node hold an HTTP connection — and an n8n worker slot — open for the whole synthesis.

The finished audio on the long-text route is fetched from a presigned S3 URL without authentication. S3 rejects a presigned request that also carries an Authorization header, so that one call deliberately skips the credential.

Checks that run before anything is billed​

These fail locally, with no request sent and no money spent:

  • empty text;
  • text over 50,000 characters;
  • speed outside 0.5–2.0;
  • a non-WAV format combined with text over the threshold. The long-text route can only return WAV, so this is refused rather than silently downgraded.

Output​

Every item's JSON carries:

FieldMeaning
routesync or async — the only reliable way to tell which route ran
engine, voiceIdwhat was requested, or null for the default
textLengthraw character count
billedCharacterBasisNFC character count with a 50-character floor
billingNotestates that the basis is before the engine multiplier and the AI-refine surcharge
formatthe format actually produced; always wav on the long-text route
mimeTyperead off the response, not the request — see below

billedCharacterBasis is the basis of the charge, not a token total. The backend applies four factors in this order: the character basis (50-character floor), then the AI-refine surcharge if it is on, then the engine multiplier, then a global token multiplier — an administrator setting, 1 by default, that no /v1 route reports. That last factor is why no number the node prints is a quote. Read the engine multiplier live from Engine: Get Many, and treat any budget you compute as approximate.

Where the audio comes out​

The audio is attached to the binary property named by Put Output File in Field — data unless you change it. The same item's JSON also gains fileName and fileSize.

Rename the field when a node upstream already occupies data, or when a downstream node expects a specific name.

The mime type and file name are read off the response, never from the request:

  1. the X-Output-Format header, if it names a known format;
  2. otherwise the Content-Type;
  3. the file name comes from Content-Disposition when present, else speech.<ext> — wav, mp3, ogg for Opus, and raw for the headerless pcm and ulaw.

That indirection is deliberate: the short-text route can fall back to WAV against an older worker, and these headers are the only signal that it did. Labelling the binary from the request would hand a downstream node WAV bytes marked audio/mpeg.

On the long-text route the default name is speech-<jobId>.wav.

Feeding the next node. Any node that consumes a file asks for the binary property by name: give it data, or whatever you renamed the field to. The size and name are also on the item's JSON as {{ $json.fileSize }} and {{ $json.fileName }}, so a downstream IF can check them without touching the bytes. To keep a large WAV out of workflow memory when you only need the link, switch Download Audio off on the long-text route and pass {{ $json.audioUrl }} along instead — it stays valid for a limited time, and the JSON reports how long as audioUrlExpiresIn.

How failures surface​

Every API failure becomes a NodeApiError whose message reads:

VieNeu API error 403 while synthesizing speech — <detail> (request id <x-request-id>)

The prose is in description. The part to branch on is n8n's machine-readable failure.cause. What each status means on the API side is in Errors; this table is the mapping the node adds on top:

Statusfailure.causeWhat happened
400configuration-invalidUsually a voice belonging to the other engine — but also a clone_… voice on the short-text route, non-Vietnamese text, and a vn_test_ key over its 100-word cap.
401credential-invalidThe key was rejected. Paste a current one into the credential.
403, message mentions an engineconfiguration-invalidThe plan does not include that engine.
403, otherwisequota-exhaustedThe token grant is exhausted or expired. No wait hint is set — it does not clear on its own.
404configuration-invalidNothing under that id. A jobId from another key reads exactly like one that never existed; a withdrawn voice answers 404, not 400.
413configuration-invalidPayload too large.
422configuration-invalidModeration refused the text. Only reachable with AI Refine on. Rephrase; retrying unchanged fails again.
429see belowThree different limiters.
503temporarily-unavailableNo worker free for that engine. Transient.
other 5xxtemporarily-unavailableRetry shortly.
anything elsenode-defectUnexpected response.

VieNeu answers 403 when tokens run out — never 402. Retry logic keyed on 402 will never fire.

DNS, TLS, reset and timeout failures never reach that table; they become a temporarily-unavailable error reading "Could not reach VieNeu while …", whose description tells you to check that the Base URL includes /api/v1.

Rate-limited versus quota-exhausted​

A 429 can come from three places that want opposite responses. The node reads the response headers to tell them apart — which is why it inspects the status itself rather than letting n8n throw and lose them.

Signal on the responseWhich limiterfailure.causeWait hintWhat to do
Retry-After presentThe app throttler, counted per API keyrate-limitedretryAfterMs, from the headerWait the stated time and retry. Nothing is wrong with the balance.
No Retry-After, body matching token limit reached / resets at / quotaThe token quota — a daily or weekly cap on the grantquota-exhaustedresetsAtEpochMs, parsed out of the message textDo not retry. Wait for the named reset, or top up.
No Retry-After, no such wording (nginx HTML)The nginx edge, counted per source IPrate-limitednoneBack off hard. On n8n Cloud that IP is shared with unrelated customers.

Two of the three collapse onto rate-limited. Tell them apart by whether retryAfterMs is set: present means the app throttler and a known wait; absent means the edge and no wait hint at all.

The quota reset timestamp is parsed out of prose because it has to be: the controller keeps only the message string, so the text is the last surviving trace of the structured resetAt field.

Note that two different statuses map to quota-exhausted: the 429 above, which carries a reset, and the 403 grant exhaustion, which carries none.

Failures that are not HTTP failures​

  • A failed job returns HTTP 200 with status: "failed". Speech: Generate raises it as an error; Job: Get returns it as data, so a Wait + IF loop can see the terminal state and leave the loop.
  • A poll timeout raises a 504-shaped error stating that the job is still running and has already been billed, naming its jobId. Collect it later with Job: Get. Keep Job Timeout under n8n's own execution timeout, or n8n kills the run and the jobId goes with it.
  • An empty audio body, or a submit that returns no jobId, raises a 502-shaped error.
  • A 200 with an empty voice list and an error field — the catalogue read failing — is raised as an error rather than rendered as an empty dropdown.

When the node is set to continue on failure instead of stopping, the failure becomes item JSON: error with the redacted message, plus jobId only when a job was already submitted. Nothing else is on that item — in particular, no binary. A short-text failure, and an async failure that happened at submit, carry error with no jobId at all.

Example workflow: narrate long text, recover the ones that time out​

Seven minutes of audio can outlast a poll window, and the job is billed at submit. This workflow keeps those jobs instead of paying for nothing.

If you can receive an inbound HTTPS request, a webhook and an n8n Webhook trigger node avoid the whole problem — there is no poll window to outlast. Build this only when you cannot.

  1. Manual Trigger — "When clicking 'Execute workflow'". Replace it with whatever produces your items; each item needs a text field.
  2. VieNeu — Resource Speech, Operation Generate.
    • Text: ={{ $json.text }}
    • Engine: pick one, so the voice list is scoped to it.
    • Voice: From List, search for the voice you want.
    • Put Output File in Field: data
    • Options → Format: WAV (long text returns nothing else).
    • Options → Job Timeout (Seconds): 900, below your n8n execution timeout.
    • Open the node's Settings tab and change On Error from Stop Workflow to the option that continues using the node's regular output — not the one that adds a separate error output connector, which would send failed items down a branch the IF in step 3 never sees. Without this, one timed-out job aborts the whole run and the jobId is lost.
  3. IF — "Failed?". Condition: ={{ $json.error }} is not empty. True means something went wrong; false means the item has its audio on data.
  4. True branch → IF — "Recoverable?". Condition: ={{ $json.jobId }} is not empty. True means the job was billed and is probably still running. False means the failure happened before a job existed — a bad key, a wrong voice, a 429 — so there is nothing to collect: send it to an error output, a Slack message, or wherever you want to see it. Do not merge it into step 8; it has no binary and never will.
  5. Recoverable → Wait — 60 seconds. Polling costs no tokens and no rate-limit budget, but it is not free at the edge.
  6. Wait → VieNeu — Resource Job, Operation Get.
    • Job ID: ={{ $json.jobId }}
    • Download Audio: on (it is off by default on this operation)
    • Put Output File in Field: data
  7. IF — "Terminal?". Condition: ={{ $json.status }} equals completed. True → step 8. False → an IF on ={{ $json.status }} equals failed, whose true side ends the branch (a No Operation node) and whose false side loops back to the Wait node in step 5. A job that outlasted a 900-second poll is not reliably finished 60 seconds later, and without this loop the item falls through with no binary attached.
  8. Merge the completed items — those from step 3's false branch and those from step 7's true branch — into whatever consumes the file: upload, email, storage. Both carry the same binary property name, so downstream nodes do not need to know which path an item took.

The same shape works item-by-item over a spreadsheet. Remember that each row is a separate billed synthesis.

Don't want to install the node?​

Every operation is one HTTPS call, and n8n's built-in HTTP Request node makes all of them. This path works on n8n Cloud, needs no build step, and needs nothing from VieNeu but a key.

Two ready-made workflows ship with the package, under n8n-nodes-vieneu/templates/:

FileWhat it builds
vieneu-speech-http-request.jsonManual trigger → one HTTP Request to POST /audio/speech → MP3 on the data binary field.
vieneu-long-text-http-request.jsonSubmit to POST /tts, then Wait → poll → IF until the job is terminal, then download the presigned audio.

Import either with Workflows → Import from File. Both attach the key through a Bearer Auth credential, never a header typed into a node parameter. Note that neither has been executed against a live n8n — they are validated structurally.

If you do not have the package, the recipes below build the same two workflows by hand. For the underlying request contract, see the Cloud API overview.

Find a voice id first. Both calls below need no key. Since 2026-09-24 the cloud API has one engine, V4, whose ids are display names (Ngọc Lan); the vieneu-… slugs of the retired V3 catalogue no longer resolve and 400 on voiceId, so refresh any id you saved before then.

# Which engine is the default? Look for "isDefault": true.
curl -s https://api.vieneu.io/api/v1/engines

# Then list that engine's voices only.
curl -s "https://api.vieneu.io/api/v1/voices?engine=v4" | head -c 400

Check that your key works, and hear the result, before wiring anything:

curl -X POST https://api.vieneu.io/api/v1/audio/speech \
-H "Authorization: Bearer $VIENEU_API_KEY" \
-H "Content-Type: application/json" \
-d '{"input":"Xin chào Việt Nam.","response_format":"mp3","speed":1}' \
--output speech.mp3

Short text: one HTTP Request node​

SettingValue
MethodPOST
URLhttps://api.vieneu.io/api/v1/audio/speech
AuthenticationGeneric Credential Type → Bearer Auth, holding your vn_sk_… key
Send Bodyon, JSON
Response → FormatFile, output property data

Body, as an expression so the text can come from the incoming item:

{{ JSON.stringify({
input: $json.text || 'Xin chào Việt Nam! Đây là giọng đọc tiếng Việt của VieNeu.',
voice: $json.voiceId || undefined,
response_format: 'mp3',
speed: 1
}) }}

Use a Bearer Auth credential, not an Authorization header typed into the node. Credentials are referenced by id and are not part of the exported workflow JSON — that is the difference between a workflow you can paste into an issue and one that leaks your key when you do.

response_format accepts mp3, wav, opus, pcm, ulaw here. This route blocks for the whole synthesis, so keep it for short text. A clone_… voice is rejected here with a 400, same as through the node.

Long text: submit, poll, download​

Past roughly 500 characters, queue the job instead of holding a connection open.

Before you build the loop: if your n8n can receive an inbound HTTPS request, a webhook replaces steps 2 to 5 with a single Webhook trigger node. VieNeu POSTs a signed event the moment the job reaches a terminal state, and POST /tts is the only route that produces those events. Poll only when an inbound endpoint is not available to you.

The polling version, matching vieneu-long-text-http-request.json, is eight nodes: a Manual Trigger and the seven below.

  1. HTTP Request — "Submit TTS job": POST https://api.vieneu.io/api/v1/tts, Bearer Auth credential, JSON body:

    {{ JSON.stringify({
    text: $json.text,
    voiceId: $json.voiceId || undefined,
    speed: 1
    }) }}

    Note the field names on this route: text and voiceId, not input and voice. It takes no format parameter and always returns WAV. It responds with a jobId, and the text is billed here, before the job is queued.

  2. Wait — 3 seconds.

  3. HTTP Request — "Check job status": GET https://api.vieneu.io/api/v1/tts/{{ $json.jobId }} (as an expression), same Bearer Auth credential.

  4. IF — "Completed?": ={{ $json.status }} equals completed. True → download. False → step 5.

  5. IF — "Failed?": ={{ $json.status }} equals failed. True → step 6. False → back to the Wait node. A failed job answers HTTP 200, so branch on the body, never the status code.

  6. No Operation — "Job failed": ends the failed branch.

  7. HTTP Request — "Download audio": GET {{ $json.audioUrl }}, Response Format File, output property data, and no credential and no authentication. The URL is a presigned S3 link carrying its own signature — S3 rejects the request outright if an Authorization header rides along.

Errors on this path arrive raw, without the node's classification. The rules still hold: a 429 with Retry-After is the per-key throttler; a 429 whose message mentions a token limit or a reset time is the quota; a 429 with neither came from the edge and is counted per source IP. Running out of tokens entirely is a 403.