Voor developers

Gebruik de HTTP-API van utt om audio uit een ander proces of apparaat te transcriberen. Bouw een extensie om lokale audio te versturen, transcripties te ontvangen, tekst te filteren of instellingen en statusinformatie aan het utt-venster toe te voegen. Beide interfaces werken zonder SDK.

De transcriptie-API

Met de HTTP-API kan een telefoon, script of app audio naar utt sturen en een transcriptie ontvangen. utt draait het spraakmodel altijd op je Mac, ook als de opname van een ander apparaat komt.

Standaard uitgeschakeld. Zet de API aan via Settings, Connect, API.

Bereik

De instelling Bereik bepaalt wie verbinding kan maken. utt bindt de listener aan de bijbehorende netwerkinterface en controleert het adres van elke aanvrager.

InstellingBindt aanAccepteert
This Mac only127.0.0.1Loopback.
This Mac and my local networkAlle interfacesLoopback, link-local en elke peer op een subnet waar een van de interfaces van deze Mac zich op bevindt.
AnywhereAlle interfacesAlles wat de poort kan bereiken.

Het 'lokale netwerk' wordt bepaald aan de hand van de eigen interfaces van de Mac, niet op basis van privé-adresbereiken. Een VPN-peer heeft een privéadres maar kan ver weg zijn. Een telefoon op dezelfde wifi heeft via IPv6 vaak een wereldwijd routeerbaar adres.

Er is geen TLS. Plaats de API achter een reverse proxy of VPN als het verkeer je eigen netwerk verlaat. Als de poort al bezet is, start de listener niet en meldt de instellingenkaart dat.

Authenticatie

Elk request, inclusief /health, bevat een bearer-token:

Authorization: Bearer <token>

Het token wordt aangemaakt wanneer de API voor het eerst wordt ingeschakeld en staat op de instellingenkaart. New vervangt het token en sluit alle clients buiten die met het oude token zijn ingesteld. Met een leeg token start de server helemaal niet.

/docs accepteert ook ?token=…, omdat de adresbalk van een browser geen header kan instellen. Geen enkele route die audio verwerkt doet dat.

Endpoints

De basis-URL is http://<host>:<port>. Gebruik 127.0.0.1 voor This Mac only. Gebruik anders de Bonjour-naam van de Mac, zoals mac.local; die blijft hetzelfde als het IP-adres verandert.

GET /docs

De OpenAPI-referentie, weergegeven door Redoc en aangeboden door utt zelf. Daardoor beschrijft die altijd de build die antwoord geeft. De link API reference op de instellingenkaart opent de pagina met het token al in de URL.

GET /health

Geeft { "ok": true, "version": "2.0.0" } terug.

POST /transcribe

De body is het audiobestand zelf. Geen multipart, geen JSON-envelope, geen velden.

  • Content-Type kiest de reader: audio/wav (de standaard voor alles wat niet wordt herkend), audio/mp4, audio/mpeg, audio/aiff, audio/flac of audio/caf.
  • Maximaal 25 MB; grotere requests worden geweigerd voordat de body wordt gelezen.
  • Opnames korter dan 0,3 seconde worden door de engine geweigerd.
  • De engine verwacht 16 kHz mono. Andere audio wordt geresampled, dus neem waar mogelijk op in 16 kHz mono.
  • X-Utt-Hints is een optionele, door komma's gescheiden lijst met namen of termen die je in de opname verwacht, zoals een repository, product of persoon. utt past een hint alleen toe als het herkende woord al sterk overeenkomt. Een gemiste correctie is veiliger dan het verkeerde woord vervangen.
curl -X POST http://mac.local:8756/transcribe \
  -H "Authorization: Bearer $UTT_TOKEN" \
  -H "Content-Type: audio/wav" \
  --data-binary @clip.wav

JSON

{
  "text": "Hello world, this is a test of the transcription API.",
  "raw": "hello world this is a test of the transcription api",
  "stages": ["formatting", "replacements"],
  "timings": {"decode": 1802.5, "replacements": 0.4, "formatting": 0.2}
}

De transcriptie heeft dezelfde pipeline doorlopen als bij de sneltoets: de engine en het model uit Settings, daarna de woordvervangingen, eventueel de opschoningsstap en de opmaakregels. Een client krijgt precies wat utt anders zou hebben geplakt.

text is die transcriptie en verandert niet van vorm, zodat een client die voor het oudere antwoord met één veld is geschreven blijft werken. Daarnaast:

  • raw: wat de herkenner hoorde, vóór elke stap en vóór je eigen hints. Gelijk aan text als niets de tekst heeft veranderd.
  • stages: welke stappen de woorden daadwerkelijk hebben veranderd: replacements, cleanup, formatting, filter (een door de gebruiker geïnstalleerde extensie) en hints (van jou). Een lege waarde betekent dat je precies kreeg wat er werd gehoord. Hints staan als aparte stap vermeld en zijn niet in raw verwerkt, omdat raw het enige blijft wat een client niet kan reconstrueren.
  • timings: hoeveel tijd elk onderdeel kostte, in milliseconden, plus decode, de herkenner, waar de meeste wachttijd in zit. Een uitgevoerde stap die de woorden niet veranderde staat hier wel, maar niet in stages; die duurde hoe dan ook even lang. Samen vormen ze niet je volledige roundtrip.
  • cleanupSkipped: alleen aanwezig als de gebruiker opschonen heeft ingeschakeld en die stap niet op deze opname is uitgevoerd: unavailable, guardrail, timeout, tooLong, tooShort of failedVerification. De transcriptie is alsnog afgeleverd.

Fouten hebben de vorm { "error": "…" } met een status:

400Geen audio of een lege body.
401Ontbrekend of onjuist token.
404Pad bestaat niet.
413Body groter dan 25 MB.
500De engine is mislukt.

Eén request per verbinding. Het antwoord bevat Connection: close; er is geen keep-alive.

Een client schrijven

De API-instellingen bevatten een knop Copy guide. Die kopieert de basis-URL van deze Mac, het wireformat, de audio-instellingen en een werkende URLSession-client. De briefing waarschuwt ook voor veelgemaakte fouten. Het token blijft een placeholder, zodat je het geheim niet in een chat plakt.

Extensies

Een extensie verbindt een programma op je Mac met utt. Het programma kan een daemon, menubalk-app of script zijn. Voeg een manifest toe en utt bouwt de instellingenpagina. Daarna schrijft utt de keuzes van de gebruiker naar een bestand dat je programma kan lezen.

Extensies hoeven zich niet bij utt te registreren en roepen geen aparte API aan. Beide programma's lezen en schrijven bestanden in de extensiemap. Elk programma kan opnieuw starten zonder de verbinding te verliezen.

~/Library/Application Support/dev.jurrejan.utt/extensions/

Vóór utt 2.0 heette deze map plugins/. utt verplaatst de oude map één keer. Programma's die naar plugins/ blijven schrijven, bereiken utt niet meer.

Wat je kunt bouwen

Elke mogelijkheid heeft een eigen sleutel in het manifest. Vraag alleen om de mogelijkheden die je programma gebruikt.

Wat je programma doetDeclareren of schrijvenWat utt levert
Stuurt lokale audio voor transcriptiesendsAudioEen jobs-map voor opnames en antwoorden.
Ontvangt elke voltooide transcriptiewantsTranscriptsDe nieuwste definitieve transcriptie in één bestand.
Volgt woorden terwijl iemand praatwantsPartialsEen stroom voorlopige tekst via één bestand.
Verandert tekst voordat die wordt afgeleverdfiltersTranscriptsEen request-en-reply-uitwisseling van twee seconden.
Voegt configuratie en commando's toesettings en actionsBediening op de extensiepagina en in het optionele menu.
Toont of het proces gezond isdaemon en status.jsonLive status, een herstartactie en een optioneel menu.
Roept de HTTP-API aanneedsApiHet token en de poort in het waardenbestand zolang de API aanstaat.

Gebruik skipsTextStages wanneer je extensie een letterlijke transcriptie nodig heeft. De instelling geldt alleen voor audio die door die extensie is verstuurd. De eigen dicteeropnames van de gebruiker veranderen nooit.

BestandGeschreven doorBevat
<id>.jsonJijHet manifest: naam, icoon en weer te geven instellingen.
<id>.consent.jsonuttDe goedkeuring van de gebruiker, uitgeschakelde status en wachtrijprioriteit.
<id>.values.jsonuttWat de gebruiker heeft gekozen, elke sleutel, elke keer.
<id>.jobs/BeideOpnames die jij plaatst en transcripties die utt ernaast zet.
<id>.transcript.jsonuttDe nieuwste transcriptie, als je daarom hebt gevraagd.
<id>.partial.jsonuttDe voorlopige woorden terwijl iemand praat.
<id>.filter/BeideVoltooide transcripties die je vóór aflevering mag aanpassen.
<id>.action.jsonuttDe nieuwste druk op een knop op je pagina of in je menu.
<id>.status.jsonJijEnkele regels die bovenaan je pagina alleen-lezen worden getoond.

Het manifest

Schrijf <id>.json telkens wanneer je programma start. Door het bestand opnieuw te schrijven herstel je het manifest na een deïnstallatie, een reset van de instellingen of een verwijderde extensiemap.

deckhand.json

{
  "id": "deckhand",
  "name": "Deckhand",
  "blurb": "One sentence under your name: what the extension does for the person.",
  "description": "A short paragraph for the About section of your page.",
  "website": "https://example.com/deckhand",
  "repository": "https://github.com/example/deckhand",
  "systemImage": "sailboat",
  "needsApi": false,
  "wantsTranscripts": false,
  "sendsAudio": false,
  "wantsPartials": false,
  "filtersTranscripts": false,
  "skipsTextStages": [],
  "tint": "#3EAFB4",
  "showsInMenuBar": true,
  "daemon": {"label": "com.example.mydaemon"},
  "actions": [
    {"key": "stop", "label": "Stop", "detail": "What it does.", "confirms": true},
    {"key": "openLog", "label": "Open log"}
  ],
  "settings": [
    {"key": "route", "kind": "choice", "label": "Route",
     "options": ["auto", "keyboard"], "value": "auto",
     "detail": "Explains the setting, shown under the label."},
    {"key": "deliver", "kind": "bool", "label": "Send into sessions", "value": true},
    {"key": "prefix", "kind": "string", "label": "Prefix", "value": ""},
    {"key": "delay", "kind": "number", "label": "Delay", "value": 0}
  ]
}
  • id moet gelijk zijn aan de bestandsnaam zonder .json. Alleen kleine letters, cijfers, ., _ en -. Een afwijking wordt genegeerd.
  • kind is bool, string, number of choice. value is de standaardwaarde en moet overeenkomen met het type. Een keuze heeft options nodig en een standaardwaarde die daartussen staat.
  • systemImage is een SF Symbol. Een onbekend symbool wordt weggelaten.
  • Alleen id en name zijn verplicht. Weggelaten sleutels krijgen hun standaardwaarde; het voorbeeld hierboven toont ze allemaal.
  • Maximaal 24 instellingen en 8 acties. Lange labels worden ingekort; regeleinden worden verwijderd.
  • utt negeert een instelling die niet kan worden weergegeven zonder de betekenis te veranderen.

Goedkeuring

Je installeert een extensie door het manifest in een map te plaatsen. Het manifest is niet ondertekend en wordt door geen installer gecontroleerd. Daarom blijft elke nieuwe extensie in afwachting totdat de gebruiker die goedkeurt via Settings, Connect, Extensions.

Zolang de goedkeuring nog niet is gegeven, maakt utt geen waardenbestand aan, transcribeert het geen opnames, deelt het geen transcripties en stelt het het API-token niet beschikbaar. Je programma kan zichzelf niet goedkeuren. Het moet veilig blijven werken als goedkeuring nooit komt. Stuurt het vóór goedkeuring audio in, dan geeft utt een fout terug:

{"error": "utt is waiting for you to approve this extension. …"}

Behandel een ontbrekend waardenbestand hetzelfde: de gebruiker heeft je pagina nog niet geopend of nog geen toestemming gegeven. Gebruik tot het verschijnt de standaardwaarden uit je eigen manifest.

Bij goedkeuring plaatst de gebruiker je ook in de wachtrij als je audio verstuurt: First, In turn of Last. De instelling bepaalt welke extensie als volgende aan de beurt is wanneer meerdere extensies werk in de wachtrij hebben. De huidige transcriptie wordt niet onderbroken en de eigen opnames van elke extensie blijven op volgorde.

De beslissing staat in <id>.consent.json, dat utt schrijft en dat jij wel mag lezen maar nooit schrijven. Een door jou geplaatst bestand wordt overschreven in plaats van opgevolgd. Een extensie die zichzelf kan goedkeuren zou precies het beveiligingsgat zijn dat dit voorkomt:

{"decision": "approved", "decidedAt": "2026-09-10T14:22:07Z", "priority": "normal"}

Helemaal geen bestand betekent wachten, net als een ontbrekend waardenbestand. "disabled" betekent dat de gebruiker je eerder heeft goedgekeurd en sindsdien via je pagina heeft uitgeschakeld. De extensie doet dan niets en het bestand vermeldt die status. priority is de term die het bestand voor de bovenstaande wachtrij gebruikt: "next", "normal" of "last".

Het waardenbestand

utt schrijft <id>.values.json wanneer de gebruiker iets wijzigt en één keer nadat je het manifest hebt geïnstalleerd, zodat het bestand al bestaat voordat iemand de pagina opent.

deckhand.values.json

{
  "revision": 4,
  "values": {"route": "auto", "deliver": true, "prefix": "", "delay": 0},
  "api": {"token": "…", "port": 8756}
}
  • Het bevat altijd elke instelling. utt voegt niet samen. Lees het volledige values-object; een ontbrekende sleutel betekent nooit 'ongewijzigd'.
  • revision gaat bij elke schrijfactie één omhoog. Vergelijk het met de laatste waarde die je zag, niet met de wijzigingstijd.
  • Een tekstinstelling komt per toetsaanslag binnen, niet per commit. Een getypte zin levert veertig revisies op. Reageer op de waarde die belangrijk is en debounce alles wat veel kost.
  • De schrijfactie is atomair, dus een poll leest nooit een halfgeschreven bestand. Eén keer per seconde is genoeg.
  • api verschijnt alleen als je manifest needsApi instelt, de gebruiker je heeft goedgekeurd en de API aanstaat. Afwezigheid betekent 'nu niet beschikbaar'. Lees nooit het eigen instellingenbestand van utt.

Poll het bestand en behandel een ontbrekend bestand als 'de gebruiker heeft de pagina nog niet geopend':

struct Settings: Decodable { var route: String; var deliver: Bool; var prefix: String; var delay: Double }
struct Values: Decodable { var revision: Int; var values: Settings }

let file = URL.homeDirectory.appending(path: "Library/Application Support/dev.jurrejan.utt/extensions/deckhand.values.json")
var seen = 0
while true {
    if let data = try? Data(contentsOf: file),
       let doc = try? JSONDecoder().decode(Values.self, from: data),
       doc.revision != seen {
        seen = doc.revision
        apply(doc.values)  // every key, every time
    }
    try await Task.sleep(for: .seconds(1))
}

Audio versturen

Stel "sendsAudio": true in en utt maakt <id>.jobs/ aan. De bestandsuitwisseling blijft op de Mac en werkt ook als de HTTP-API uitstaat. Gebruik voor lokale audio de jobs-map in plaats van een netwerkverbinding te openen.

  1. Schrijf de opname onder een naam die nog geen audiobestand is, zoals clip-1.wav.part, en hernoem die daarna naar clip-1.wav. utt pakt alleen audio-extensies op, dus een halfgeschreven bestand blijft onzichtbaar totdat het door hernoemen compleet wordt.
  2. utt transcribeert het en schrijft atomair clip-1.json ernaast. Precies één van text en error is aanwezig.
  3. utt verwijdert de audio na succes of mislukking. Je programma moet het antwoordbestand lezen en verwijderen.
let jobs = URL.homeDirectory.appending(path: "Library/Application Support/dev.jurrejan.utt/extensions/deckhand.jobs")
let part = jobs.appending(path: "clip-1.wav.part")
try wav.write(to: part)
try FileManager.default.moveItem(at: part, to: jobs.appending(path: "clip-1.wav"))  // now utt sees it

let answer = jobs.appending(path: "clip-1.json")
while !FileManager.default.fileExists(atPath: answer.path) {
    try await Task.sleep(for: .milliseconds(200))
}
struct Answer: Decodable { var text: String?; var error: String? }
let result = try JSONDecoder().decode(Answer.self, from: Data(contentsOf: answer))
try FileManager.default.removeItem(at: answer)
print(result.text ?? result.error ?? "")

clip-1.json

{
  "text": "the words that were spoken",
  "raw": "um the words what were spoken",
  "stages": ["cleanup", "replacements"],
  "startedAt": "2026-09-09T17:04:09Z",
  "finishedAt": "2026-09-09T17:04:11Z",
  "startedAtMs": 1789052649182,
  "finishedAtMs": 1789052651511,
  "duration": 3.4,
  "timings": {"decode": 1802.5, "replacements": 0.4, "cleanup": 511.2}
}

{"error": "Could not transcribe that clip.", "startedAt": "2026-09-09T17:04:09Z",
 "finishedAt": "2026-09-09T17:04:11Z", "startedAtMs": 1789052649182, "finishedAtMs": 1789052651511}
  • Extensies die utt opent: wav, m4a, mp3, aiff, flac, caf. De extensie bepaalt de reader, dus die moet overeenkomen met de bytes.
  • Opnames worden met de oudste eerst opgepakt, dus twee op volgorde verstuurde opnames komen in dezelfde volgorde terug.
  • Maximaal 25 MB. Je krijgt dezelfde tekst die de sneltoets zou hebben geplakt.
  • text is de transcriptie; raw, stages, cleanupSkipped en timings beschrijven wat er is gehoord en welke stappen het hebben veranderd, precies zoals het API-antwoord, met één extra eigen stapnaam: hints, voor het onderstaande bestand. startedAt, finishedAt en hun millisecondevarianten startedAtMs/finishedAtMs zijn er altijd, bij succes én bij fouten; de rest niet.
  • Plaats vóór het hernoemen clip-1.hints.json, een platte JSON-array van strings, naast de opname. utt corrigeert de transcriptie dan aan de hand van die termen.

Transcripties

Stel "wantsTranscripts": true in en elke transcriptie die utt produceert wordt na voltooiing naar <id>.transcript.json geschreven.

deckhand.transcript.json

{
  "sequence": 12,
  "text": "the words that were spoken",
  "raw": "um the words what were spoken",
  "stages": ["cleanup", "replacements"],
  "finishedAt": "2026-09-09T16:58:03Z",
  "duration": 3.4,
  "timings": {"decode": 1802.5, "replacements": 0.4, "cleanup": 511.2},
  "app": "Ghostty"
}
  • Alleen de nieuwste. Dit is geen logboek; utt bewaart de geschiedenis al.
  • sequence gaat per transcriptie omhoog. Poll het precies zoals je revision pollt.
  • app is waar de tekst werd geplakt en ontbreekt als niets de tekst heeft ontvangen.
  • raw, stages, cleanupSkipped en timings zijn dezelfde velden als in het antwoord van een job, behalve de stap hints, omdat dicteren geen hintsbestand heeft. Hier is geen startedAt, alleen finishedAt en duration.
  • Wordt geschreven ongeacht of de gebruiker geschiedenis bewaart.
  • Hiermee krijgt de extensie elke transcriptie die op de Mac wordt geproduceerd. utt benoemt die toegang wanneer het de gebruiker om goedkeuring vraagt en toont dit op de pagina van de extensie. Vraag er alleen om als je het nodig hebt.

Andere mogelijkheden

Elk hiervan is één extra sleutel in het manifest en één extra bestand of map die utt voor je bijhoudt.

  • wantsPartials: de woorden terwijl ze worden gedecodeerd en de toets nog ingedrukt is, geschreven naar <id>.partial.json. Een andere, kleinere herkenner die alleen Engels ondersteunt produceert ze vóór de tekststappen van utt en alleen wanneer de gebruiker Show words while you speak heeft ingeschakeld. De echte transcriptie is een afzonderlijke, latere gebeurtenis en kan iets heel anders zeggen. Niets hier is definitief.
  • filtersTranscripts: elke voltooide transcriptie wordt naar <id>.filter/<name>.in.json geschreven. Wat je binnen twee seconden naar <name>.out.json terugschrijft, wordt afgeleverd. Een gemiste deadline is geen fout: het origineel gaat door. Een lege string laat de transcriptie vervallen.
  • skipsTextStages: stappen uit de eigen tekstpipeline van de gebruiker die alleen voor jouw opnames worden overgeslagen, nooit voor diens dicteeropnames. De opties zijn replacements, cleanup en formatting. Een onbekende naam wordt weggelaten in plaats van het manifest te weigeren.
  • actions: maximaal acht knoppen op je pagina. Een druk op een knop schrijft <id>.action.json met een sequence die je precies zo pollt als revision. Met "confirms": true vraagt utt eerst om bevestiging.
  • daemon: een launchd-label. utt leest de status uit launchd en biedt een knop Restart. Het start, laadt of ontlaadt nooit iets namens jou. Het verwacht een label in plaats van een pad naar een plist en weigert een label dat begint met com.apple.. Je mag rapporteren over je eigen daemon, maar niet in het systeem zelf grijpen.
  • showsInMenuBar: een submenu in het eigen menubalkmenu van utt, opgebouwd uit je statusregels, je daemon en al je acties. Alles of niets; er is geen opt-in per actie.

tint, description, website en repository bepalen hoe de extensie in utt verschijnt. Ze bepalen de menubalkkleur tijdens de jobs en het onderdeel Over op de pagina. Links naar websites en repositories moeten https:// gebruiken.

Status

Je kunt ook <id>.status.json schrijven: een plat object met strings dat bovenaan je pagina alleen-lezen wordt weergegeven.

deckhand.status.json

{"daemon": "up 0.1.0", "sessions": "9 live", "lastRelay": "2 min ago"}
  • utt interpreteert deze waarden nooit. camelCase-sleutels worden voor weergave gesplitst, zodat lastRelay 'Last relay' wordt.
  • Een ontbrekend bestand betekent 'not running' en wordt zo weergegeven.
  • Schrijf het opnieuw wanneer iets verandert, maximaal ongeveer één keer per seconde.

Een extensie bouwen

  1. Schrijf het manifest bij elke start.
  2. Poll het waardenbestand en reageer wanneer revision verandert. Gebruik totdat het bestand bestaat de standaardwaarden uit je eigen manifest. Het kan ontbreken omdat nog niemand je heeft goedgekeurd.
  3. Gebruik de jobs-map om audio te transcriberen. Gebruik needsApi alleen als je de HTTP-API voor iets anders nodig hebt, bijvoorbeeld om vanaf een ander apparaat met utt te communiceren.
  4. Heb je de API nodig, haal het token dan uit het waardenbestand. Lees nooit settings.json van utt.
  5. Vraag alleen om wat je gebruikt. Elke vlag in het manifest wordt in gewone woorden aan de gebruiker voorgelezen voordat die beslist.
  6. Niets in het waardenbestand is een commando. Het is de configuratie van de gebruiker en het enige wat utt belooft erin te zetten.

Zet geen eigen geheimen in het manifest. Het is een gewoon bestand en de inhoud wordt in het utt-venster getoond.

De pagina Extensions in utt heeft een knop Copy guide. Die kopieert een LLM-briefing op basis van deze handleiding en vult de extensiemap voor de huidige installatie in.