Bygg en helautomatisk transkriberingsflyt med n8n

De fleste team transkriberer fortsatt for hånd: noen laster ned opptaket, laster det opp et sted, venter, laster ned en SRT-fil, gir den nytt navn og legger den i riktig mappe. Det går fint — helt til det skjer førti ganger i uken.


Denne veiledningen erstatter den personen med en arbeidsflyt. Så snart et nytt opptak kommer inn, blir det transkribert, tekstet og levert — uten at noen følger med. Vi bruker n8n fordi det kjører hvor som helst, kan driftes på din egen infrastruktur og snakker med et hvilket som helst HTTP-API — også vårt.


Til slutt sitter du igjen med en arbeidsflyt som:


  1. starter så snart en ny video- eller lydfil dukker opp
  2. sender den til Inwista-API-et for transkribering
  3. venter til jobben er ferdig, uten å gjette på hvor lang tid det tar
  4. laster ned den ferdige SRT-filen
  5. leverer den dit teamet ditt trenger den


Alt under kjører på det åpne Inwista API v1. Ingen tillegg, ingen egne kodenoder.

Før du begynner

Du trenger tre ting:


  • En n8n-instans — sky eller egen drift, versjon 1.x
  • En Inwista-API-nøkkel — lag en under Mitt arbeidsområde → API-nøkler. Den begynner med inw_live_
  • Mediefiler API-et kan nå — API-et tar en offentlig https-URL, så filer i Dropbox, Google Drive, S3 eller CMS-et ditt trenger en delbar eller signert lenke


Et ord om kostnader før du bygger noe som går uten tilsyn: transkribering koster 4 kreditter per påbegynte medieminutt, trukket når jobben blir tatt imot. Jobber som feiler, blir refundert automatisk. Test arbeidsflyten på et par korte filer før du slipper den løs på hele arkivet.

Steg 1: velg utløseren din

Flyten starter med hendelsen som betyr «her er det noe nytt å transkribere». De vanligste valgene:


  • Google Drive Trigger / Dropbox Trigger — en overvåket mappe der teamet legger opptakene
  • Webhook — ditt eget CMS eller LMS kaller n8n når en opplasting er ferdig
  • Schedule Trigger — sjekk en RSS-strøm, en podkastvert eller en database med ubehandlede rader
  • Manual Trigger — til bygging og testing, og det er der vi starter

Legg inn en Manual Trigger foreløpig. Bytt den ut med den ekte når resten virker.

Uansett hva du velger: utløserens jobb er å produsere én ting — en offentlig tilgjengelig URL til mediefilen. Lagre den i et felt som heter mediaUrl, så stemmer resten av veiledningen med arbeidsflyten din.

Steg 2: send inn transkriberingen

Legg inn en HTTP Request-node som heter Submit transcription.

  • Metode — POST
  • URL — https://api.inwista.ai/v1/transcriptions
  • Autentisering — Generic Credential Type → Header Auth
  • Headernavn — Authorization
  • Headerverdi — Bearer inw_live_nokkelen_din_her
  • Send body — på, JSON


Body:

{
  "source_url": "{{ $json.mediaUrl }}",
  "language": "no",
  "diarization": true,
  "metadata": { "source": "n8n", "folder": "{{ $json.folderName }}" }
}


Tre felter du bør forstå:

  • language er språket som snakkes i filen, ikke språket du vil ha ut. Å transkribere på kildespråket er det som gir presise tidskoder og ren tekst; oversettelsen kommer etterpå, som et eget steg på det ferdige transkriptet. Håndterer flyten din flere språk, kan du utlede dette feltet fra mappen eller metadataene til utløseren.


  • diarization slår på merking av talere. La den stå av for innhold med bare én stemme — den koster prosesseringstid du ikke trenger.


  • metadata er ditt. Inntil 1 KB med hva du vil, returnert uendret ved hvert oppslag. Bruk det til identifikatorene dine egne systemer bryr seg om — en kurs-ID, et saksnummer, en episode-slug — slik at senere steg slipper å rekonstruere sammenhengen.


Legg inn én header til mens du er i gang:


  • Idempotency-Key{{ $execution.id }}


Hvis n8n kjører noden på nytt etter en nettverkshikke, kjenner API-et igjen nøkkelen og gir deg den opprinnelige jobben i stedet for å starte — og fakturere — en ny.

Svaret kommer med én gang, med status: "processing" og en id. Transkriberingen er ikke ferdig; den er tatt imot.

Steg 3: vent på resultatet, på riktig måte

Det er her de fleste flyter går galt. En fast «vent i fem minutter» er for kort til en forelesning og bortkastet på et talenotat. Spør heller om status.

Legg inn en Wait-node (Wait 15s) satt til 15 sekunder.

Legg inn en HTTP Request-node (Check status):

  • Metode — GET
  • URL — https://api.inwista.ai/v1/transcriptions/{{ $('Submit transcription').item.json.id }}
  • Autentisering — samme header som før

Legg inn en IF-node (Is it done?) med denne betingelsen:

{{ $json.status }}  equals  completed


Koble false-grenen tilbake til Wait 15s. Det er løkken din: sjekk, vent, sjekk igjen, helt til jobben melder completed. True-grenen går videre til neste steg.

Svaret inneholder også et progress-tall som viser hvor flyten faktisk står, så vil du ha en fremdriftsindikator i Slack eller i ditt eget dashbord, ligger den allerede der.


To ting du bør legge til før dette går i produksjon:

  • Håndter feil. Legg inn en ekstra IF som sjekker {{ $json.status }} equals failed og sender videre til varslingen du bruker. Jobber som feiler, blir refundert automatisk, men du vil vite om det likevel.
  • Sett tak på løkken. Løkkevernet i n8n hjelper, men et eksplisitt tak — en teller som gir opp etter for eksempel 80 runder — gjør en fastlåst jobb om til et varsel i stedet for en kjøring som går hele natten.

Steg 4: hent undertekstene

Legg inn en HTTP Request-node som heter Get SRT:

  • Metode — GET
  • URL — https://api.inwista.ai/v1/transcriptions/{{ $('Submit transcription').item.json.id }}/captions?format=srt
  • Responsformat — File (eller Text, hvis du vil ha innholdet videre i flyten)

Endepunktet returnerer selve undertekstfilen, ikke en JSON-innpakning — så resultatet fra noden er klart til å skrives til disk, legges ved en e-post eller lastes opp.

Bytt format etter hva mottakeren trenger:

Format — brukes til

  • srt — videospillere, redigeringsprogrammer, YouTube, de fleste publiseringsløsninger
  • vtt — HTML5-<track>, nettspillere
  • txt — søkeindekser, LLM-flyter, dokumentasjon
  • json — tidskoder på ordnivå, merking av talere, egen visning

Hvert format er bare en visning av den samme ferdige jobben. Å hente fire av dem koster ikke noe ekstra.

Steg 5: lever resultatet

Den siste noden er det «ferdig» betyr for ditt team:

  • Google Drive / Dropbox / S3 — legg filen ved siden av kildevideoen
  • Slack — legg transkriptet i kanalen som ba om det
  • HTTP Request — send det inn i CMS-et, LMS-et eller undertekstfeltet ditt
  • Postgres / Airtable / Notion — lagre txt-versjonen som søkbar tekst

Hvis mottakeren din er en av dem Inwista allerede integrerer direkte mot — Google Drive, OneDrive, SharePoint, Dropbox, Box, YouTube, Vimeo, Wistia og flere — kan du hoppe over denne noden helt og heller sette opp automatisk levering til skyen under Mitt arbeidsområde → Integrasjoner. Da kommer filene automatisk ved hver ferdige jobb, med eller uten arbeidsflyt.

Neste nivå: oversett før du leverer

Legg inn to noder før leveringen, så gir den samme flyten deg undertekster på så mange språk du vil.

POST /v1/transcriptions/{id}/translate med:

{ "target_language": "en" }


Den svarer 202 Accepted — oversettelsen kjøres på det ferdige transkriptet, så tidskodene stemmer allerede og bare teksten endres. Spør GET /v1/transcriptions/{id}/translations/en på samme måte som du spurte om jobben, og hent så:

GET /v1/transcriptions/{id}/captions?format=srt&language=en


Kjør det gjennom en liste med målspråk, og ett opptak blir til et komplett flerspråklig undertekstsett i én kjøring. Merk at språkforespørselen er streng: ber du om et språk uten ferdig oversettelse, får du en tydelig feil i stedet for kildespråket i det stille. Det er nøyaktig det du vil ha i en flyt som går uten tilsyn.

Neste nivå: undertekster i sendekvalitet

Rå transkribering er ordrett. Undertekster er håndverk: linjelengder, lesehastighet, hvor en setning brytes over to blokker.

POST /v1/transcriptions/{id}/enhance kjører det ferdige transkriptet gjennom nettopp den behandlingen — fortetter teksten, balanserer linjeskift på nytt, formaterer dialog og håndhever minste og største blokkvarighet:

{
  "settings": {
    "maxLinesPerBlock": "2",
    "maxCharactersPerLine": 42,
    "textCondensation": "smart",
    "speakerDialogueFormat": "hyphens",
    "gapBetweenBlocks": "broadcasting"
  }
}


Den svarer 202; spør GET /v1/transcriptions/{id}/enhancements/{enhancementId} til den er ferdig. Etterpå serverer undertekst-endepunktene automatisk den forbedrede versjonen — du trenger ikke endre noe i leveringsnoden.

Sensitive opptak

Hvis flyten din behandler materiale du helst ikke vil at vi skal beholde — pasientsamtaler, juridiske opptak, interne allmøter — legger du til ett felt i innsendingen fra steg 2:

{
  "source_url": "{{ $json.mediaUrl }}",
  "language": "no",
  "retention": "none"
}


Med retention: "none" slettes kildemediet så snart transkriberingen er ferdig. Transkriptet, undertekstene, oversettelsene og senere forbedringer fungerer helt som før — det er bare lyden og videoen som er borte. Det finnes også store_media: false, som beholder filen til behandlingen, men ikke lager avspillingskopier.

Og når oppbevaringstiden for en jobb er ute hos deg, sletter DELETE /v1/transcriptions/{id} alt som hører til — medier, transkript, revisjoner, oversettelser — med ett kall. En planlagt n8n-flyt som sletter jobber eldre enn retningslinjene dine, er rundt fire noder, og gjør oppbevaringspolicyen din til noe du kan vise fram i stedet for å beskrive.

Webhooks: motstykket

Å spørre om status er riktig styring inne i én n8n-kjøring: den er selvstendig, trenger ingen offentlig URL og holder hele flyten på ett sted du kan feilsøke.

Webhooks løser et annet problem: å få ferdig arbeid fram til én sentral mottaker, uansett hvor jobben kom fra. Opptak kollegene dine laster opp i dashbordet, jobber sendt inn fra et annet system, eksporter som blir ferdige timer senere — alt kan lande på ett endepunkt i stedet for at hver flyt passer sitt eget. Sett det opp under Mitt arbeidsområde → Integrasjoner.

Pek den mot en Webhook-node i n8n, så får du en signert transcript.completed-hendelse:

{
  "event": "transcript.completed",
  "id": "evt_...",
  "timestamp": 1786902819,
  "workspaceId": "...",
  "project": {
    "id": "...",
    "name": "styremote-august.mp4",
    "language": "no",
    "durationSeconds": 3184
  },
  "files": [
    { "format": "srt", "name": "styremote-august.srt", "url": "https://...", "expiresAt": 1786989219 }
  ]
}


Hver forespørsel er signert med HMAC-SHA256 over den rå meldingskroppen, i headeren X-Inwista-Signature som sha256=<hex>. Sjekk den før du stoler på innholdet — i n8n holder det med en Crypto-node og en IF-sammenligning. Fil-URL-ene er signerte og gyldige i 24 timer, så hent det du trenger i stedet for å lagre lenkene.

Leveringen prøves tre ganger, og et endepunkt som fortsetter å feile, blir slått av automatisk med årsaken synlig i integrasjonsinnstillingene — en ødelagt mottaker dukker altså opp som en status du kan se, ikke som hendelser som forsvinner i stillhet.

Ting som tar deg før eller siden

Forespørselsgrenser. 300 lese- og 60 skriveforespørsler per minutt per nøkkel. Rause tall til vanlig bruk, fort brukt opp hvis du slipper et helt arkiv løs parallelt uten kø. Skal du ta igjen hundrevis av filer, kjør dem i puljer.

Kilde-URL-ene må være tilgjengelige. API-et sjekker mediefilen før jobben tas imot — det er slik varighet og pris er kjent på forhånd. En Drive-lenke som krever innlogging, gir unreadable_source, og ingenting blir fakturert. Bruk direkte eller signerte URL-er.

Feil er strukturerte. Hver feil gir den samme innpakningen:

{ "error": { "code": "insufficient_credits", "message": "..." } }


Forgren på code, aldri på meldingsteksten. Koder kommer bare i tillegg innenfor v1 og får aldri nytt navn; meldinger kan bli omformulert.

Idempotensnøkler gjelder per forespørsel, ikke per fil. De beskytter deg mot dobbel fakturering når den samme nodekjøringen prøves på nytt. Å luke ut det samme opptaket sendt inn to ganger av to ulike kjøringer, er jobben til flyten din — en oppslagstabell på fil-ID er det vanlige svaret.

Den ferdige arbeidsflyten

Her er skjelettet til import og tilpasning. Bytt ut credential-referansen med din egen Header Auth-credential i stedet for å lime en nøkkel rett inn i noden.

{
  "name": "Inwista — automated transcription",
  "nodes": [
    {
      "parameters": {},
      "id": "trigger",
      "name": "When clicking Test workflow",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [0, 0]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.inwista.ai/v1/transcriptions",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\"source_url\": \"{{ $json.mediaUrl }}\", \"language\": \"no\", \"diarization\": true}",
        "options": {}
      },
      "id": "submit",
      "name": "Submit transcription",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [220, 0]
    },
    {
      "parameters": { "amount": 15 },
      "id": "wait",
      "name": "Wait 15s",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [440, 0]
    },
    {
      "parameters": {
        "url": "=https://api.inwista.ai/v1/transcriptions/{{ $('Submit transcription').item.json.id }}",
        "options": {}
      },
      "id": "status",
      "name": "Check status",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [660, 0]
    },
    {
      "parameters": {
        "conditions": {
          "options": { "caseSensitive": true, "version": 2 },
          "conditions": [
            {
              "leftValue": "={{ $json.status }}",
              "rightValue": "completed",
              "operator": { "type": "string", "operation": "equals" }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "isdone",
      "name": "Is it done?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [880, 0]
    },
    {
      "parameters": {
        "url": "=https://api.inwista.ai/v1/transcriptions/{{ $('Submit transcription').item.json.id }}/captions?format=srt",
        "options": { "response": { "response": { "responseFormat": "text" } } }
      },
      "id": "srt",
      "name": "Get SRT",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [1100, -100]
    }
  ],
  "connections": {
    "When clicking Test workflow": { "main": [[{ "node": "Submit transcription", "type": "main", "index": 0 }]] },
    "Submit transcription": { "main": [[{ "node": "Wait 15s", "type": "main", "index": 0 }]] },
    "Wait 15s": { "main": [[{ "node": "Check status", "type": "main", "index": 0 }]] },
    "Check status": { "main": [[{ "node": "Is it done?", "type": "main", "index": 0 }]] },
    "Is it done?": {
      "main": [
        [{ "node": "Get SRT", "type": "main", "index": 0 }],
        [{ "node": "Wait 15s", "type": "main", "index": 0 }]
      ]
    }
  }
}


Seks noder. Alt det andre — oversettelse, forbedring, levering, sletting — henger på det samme skjelettet.

Hva dette faktisk endrer

Det interessante er ikke at transkribering blir automatisert. Det er hva som slutter å være en avgjørelse.


Når undertekster koster en ettermiddag av noens tid, blir de rasjonert: de viktige videoene får dem, resten ikke. Når de ikke koster noe per fil og er der før noen rekker å spørre, slutter de å være et prosjekt og blir en egenskap ved innholdet ditt — hvert opptak søkbart, hver video tilgjengelig, hvert kurs i tråd med kravene, uten at det må et møte til.


Det er mer verdt enn timene det sparer.


Klar til å bygge? Lag en API-nøkkel — gratisnivået holder til å kjøre gjennom hele dette oppsettet. Den fullstendige endepunktsreferansen finner du i API-dokumentasjonen.