Orchard logo — audio AI API for speech-to-text and text-to-speechorchard
Reference · v1

Orchard API

Voice infrastructure on a private inference cluster. Three products, one balance, one API. Transcribe audio to text in 60+ languages (sync / async / webhooks), Synthesize text to audio across 17 languages. Transcribe and Synthesize debit the same per-second balance, so your plan covers both without separate quotas. Every request lives under /v1.

Base URL · https://api.orchardrun.comJSON over HTTPSBearer authUnified balance

Quickstart

Three steps to your first request — same flow whether you start with Transcribe or Synthesize. One API key, one balance.

  1. Sign up at /signup.
  2. Activate a plan at /billing — Free includes 500 min/month (covers both products), paid plans start at $1.
  3. Generate an API key at /keys. The raw key (ork_…) is shown once — copy it.

Then, transcribe an MP3 — or jump to Synthesize for text-to-speech:

curl -X POST https://api.orchardrun.com/v1/audio/transcriptions \
  -H "Authorization: Bearer ork_..." \
  -F file=@audio.mp3 \
  -F language=es \
  -F response_format=verbose_json

TypeScript SDK

Skip the raw HTTP plumbing. The official @orchardrun/sdk package wraps every endpoint with typed methods, automatic retries on 5xx, and typed errors you can instanceof instead of string-matching. Universal — Node 18+, Bun, Deno, browsers, Cloudflare Workers, Vercel Edge.

Install

pnpm add @orchardrun/sdk

Quickstart

Set ORCHARD_API_KEY in your env (or pass apiKeydirectly to the constructor) and you're three lines from a transcript.

import Orchard from "@orchardrun/sdk";
import fs from "node:fs";

const orchard = new Orchard({ apiKey: process.env.ORCHARD_API_KEY });

// Speech to text
const { text } = await orchard.transcribe({
  file: fs.readFileSync("./call.wav"),
  language: "es",
});
console.log(text);

// Text to speech
const audio = await orchard.tts.generate({
  text: "Hola mundo",
  voice: "es_MX-claude",
});
await fs.promises.writeFile(
  "./out.wav",
  Buffer.from(await audio.arrayBuffer()),
);

const voice = await orchard.voices.create({
  name: "Mateo · founder",
  file: fs.readFileSync("./sample.wav"),
  language: "es",
});
const synth = await orchard.voices.synthesize(voice.id, {
  text: "Hola, soy Mateo.",
});

Vercel AI SDK provider

Orchard ships a first-class provider for the Vercel AI SDK. If you already use transcribe() or generateSpeech() from ai, switching to Orchard is a one-line provider swap.

import { experimental_transcribe as transcribe } from "ai";
import { orchard } from "@orchardrun/sdk/ai-sdk";
import fs from "node:fs";

const { text, language, durationInSeconds } = await transcribe({
  model: orchard.transcription(),
  audio: fs.readFileSync("./call.wav"),
  mediaType: "audio/wav",
  providerOptions: { orchard: { language: "es" } },
});

Typed errors

Every non-2xx maps to an OrchardError subclass. Catch the specific one for retry / upgrade-CTA logic — no string matching on error.message needed.

import {
  OrchardRateLimitError,
  OrchardQuotaError,
  OrchardAuthError,
} from "@orchardrun/sdk";

try {
  await orchard.tts.generate({ text, voice });
} catch (e) {
  if (e instanceof OrchardRateLimitError) {
    await sleep((e.retryAfterSeconds ?? 5) * 1000);
    return retry();
  }
  if (e instanceof OrchardQuotaError) {
    // 402 — out of balance. Surface upgrade CTA to your user.
    return showUpgradeBanner();
  }
  if (e instanceof OrchardAuthError) {
    // 401/403 — rotate the API key.
    return null;
  }
  throw e;
}

Cancellation

Every method accepts an AbortSignalvia the optional second argument. Useful when you're wiring up a "cancel transcription" button on a long upload.

const ac = new AbortController();
setTimeout(() => ac.abort(), 5_000); // 5-second hard cap

await orchard.transcribe(
  { file: bigAudioBuffer, language: "es" },
  { signal: ac.signal },
);

Full API surface, changelog and contributing guide on npm and GitHub.

Authentication

Every endpoint except POST /v1/auth/* and /v1/billing/webhook requires authentication. Two interchangeable methods:

  • API key (recommended for programmatic access): Authorization: Bearer ork_... or X-API-Key: ork_....
  • JWT (used by the web dashboard, obtainable via POST /v1/auth/login): Authorization: Bearer eyJhbGciOi....

Keys are SHA-256 hashed at rest and revocable instantly from /keys. The raw token is shown only on creation — there is no recovery flow.

Sync API

POST /v1/audio/transcriptions accepts a standard multipart upload and blocks until the cluster returns a transcript. 60+ languages supported with automatic detection. Request and response shape follows the public transcription-API conventions, so any SDK that speaks that format plugs in by pointing its base URL at Orchard.

curl -X POST https://api.orchardrun.com/v1/audio/transcriptions \
  -H "Authorization: Bearer ork_..." \
  -F file=@audio.mp3 \
  -F language=es \
  -F response_format=verbose_json

Multipart fields

filebinary, requiredAudio file. mp3, m4a, wav, mp4, ogg, flac, webm. Max 500 MB.
modelstringAccepted for client-library compatibility and ignored — every request runs on our latest STT engine, kept up to date by us.
languagestringISO 639-1 hint, 60+ supported (es, en, pt, fr, de, it, hi, zh, ja, ar, ru, pl, nl, tr, ko, and more). Auto-detected when omitted.
response_formatstringjson (default) · verbose_json · text
promptstringAccepted for parity; ignored.
temperaturenumberAccepted for parity; ignored (decoder is greedy).

Blocking: the request waits for the cluster to finish (timeout 600s). For long audio prefer the async endpoints below.

Async jobs

For long audio (or YouTube URLs) submit a job, get back a job_id, and poll until status issuccess. Concurrency is capped per plan (see Limits); excess requests queue rather than fail.

POST /v1/transcriptions · YouTube URL

curl -X POST https://api.orchardrun.com/v1/transcriptions \
  -H "Authorization: Bearer ork_..." \
  -H "Content-Type: application/json" \
  -d '{"url":"https://youtu.be/dQw4w9WgXcQ","language":"en"}'

# → 202 Accepted
# {"job_id": "abc123...", "status": "queued"}

POST /v1/transcriptions/upload · file

curl -X POST https://api.orchardrun.com/v1/transcriptions/upload \
  -H "Authorization: Bearer ork_..." \
  -F file=@interview.mp3 \
  -F language=es

# → 202 Accepted
# {"job_id": "...", "status": "queued"}

Accepts diarize=true to label segments with speaker IDs (SPEAKER_00 etc.). See Speaker diarization for pricing and quotas.

GET /v1/transcriptions/{id} · poll

Returns the job's status plus a live progress snapshot while running, and the full result payload once succeeded.

curl https://api.orchardrun.com/v1/transcriptions/abc123 \
  -H "Authorization: Bearer ork_..."

# {"job_id":"abc123","status":"running",
#  "progress":{"current_ms":699000,"total_ms":1099000,"percent":63}}

# Once done:
# {"job_id":"abc123","status":"success",
#  "result":{"text":"...","language":"es","duration_seconds":1099, ...}}

GET /v1/transcriptions/{id}/download · formatted

Same job, rendered in the format you ask for. Returns the file with a Content-Disposition: attachment header.

# Plain text
curl "https://api.orchardrun.com/v1/transcriptions/abc123/download?format=text" \
  -H "Authorization: Bearer ork_..." -O

# SRT subtitles
curl "https://api.orchardrun.com/v1/transcriptions/abc123/download?format=srt" \
  -H "Authorization: Bearer ork_..." -O

# Markdown with YAML frontmatter
curl "https://api.orchardrun.com/v1/transcriptions/abc123/download?format=md" \
  -H "Authorization: Bearer ork_..." -O

Speaker diarization

Speaker diarization adds who said what on top of a normal transcript. Pass diarize=true when you submit an async job and every segment in the response comes back labelled with speaker: "SPEAKER_00", "SPEAKER_01", etc., plus a top-level diarization block with the detected speaker count.

Async only

Diarization is available on the async endpoints (POST /v1/transcriptions for YouTube, POST /v1/transcriptions/upload for files). The sync endpoint (POST /v1/audio/transcriptions) does not accept the flag — diar adds ~30s of processing on a one-minute clip, which doesn't fit a request-response timeout.

Speaker count hints

If you know how many people are on the recording, say so. On clean audio auto-detection works well, but on far-field or heavily compressed recordings the clusterer can invent an extra speaker out of background voices or artifacts. Three optional fields:

  • num_speakers — lock clustering to exactly N. Best when the count is known: a support call is 2, a panel interview with three interviewers is 4.
  • min_speakers / max_speakers — bound auto-detection when you only know a range. Ignored if num_speakers is set.

Request

curl -X POST https://api.orchardrun.com/v1/transcriptions/upload \
  -H "Authorization: Bearer ork_..." \
  -F file=@meeting.mp3 \
  -F language=es \
  -F diarize=true \
  -F num_speakers=2

# → 202 Accepted
# {"job_id": "abc123...", "status": "queued"}

Response

Same poll endpoint as a regular async job (GET /v1/transcriptions/{id}). Two additions when the job ran with diarize=true:

  1. Each entry in result.segments[] carries a speaker field ("SPEAKER_00", "SPEAKER_01", …) and start / end in seconds.
  2. A new result.diarization block:{ speaker_count, spans }.
{
  "job_id": "abc123...",
  "status": "success",
  "result": {
    "text": "Hola, ¿cómo estás? Bien, gracias.",
    "language": "es",
    "duration_seconds": 6.4,
    "segments": [
      {
        "start": 0.0,
        "end": 1.8,
        "text": "Hola, ¿cómo estás?",
        "speaker": "SPEAKER_00"
      },
      {
        "start": 2.1,
        "end": 4.3,
        "text": "Bien, gracias.",
        "speaker": "SPEAKER_01"
      }
    ],
    "diarization": {
      "speaker_count": 2,
      "spans": 4
    }
  }
}

Pricing & quotas

Diarized jobs debit 1.5× the audio duration from your balance (a 60-second clip with diarize=true consumes 90 seconds). On top of that, each plan has a monthly diarization quotathat's separate from the balance — when you hit it, the endpoint returns 402 until the quota resets on the 1st of the next month.

Free10 min/monthOne-time taste of the feature. Upgrade once you need it for production.
Hobby30 min/monthLight usage — call snippets, short podcast tests.
Starter300 min/monthMost podcast / call-center pipelines fit comfortably here.
Pro3,000 min/monthProduction teams running daily diarized batch.
EnterpriseUnlimitedCustom contracts — capacity scoped operationally.

Check your remaining quota live at GET /v1/usage/diarization(returns plan, used_seconds, limit_seconds, resets_at).

Latency

Diarization runs after the whisper transcription and adds compute proportional to the audio length. On the cluster today: ~0.5× real-timefor the diar pass on top of whisper's 96×. Practical wall-clock guidance:

30-second clip+5-8 secondsTotal ~10-15 s vs. ~2 s plain transcription.
5-minute audio+25-40 secondsTotal ~30-45 s vs. ~6 s plain.
60-minute audio+18-25 minutesPlan accordingly — use webhooks instead of polling for long jobs.

Accuracy notes

  • Best on clean recordings (single-channel mic, low background noise) with 2-4 speakers. Beyond 4 speakers, expect over-clustering and label drift.
  • Speaker labels (SPEAKER_00 etc.) are per-job: the same person across two separate requests will get different labels. Identity tracking across jobs is on the roadmap.
  • Audio with heavy overlap (panel debates, sportscasts) sees accuracy drop sharply. Use the underlying segments + words to reconcile manually if your domain requires it.
  • Languages: the diarizer is language-agnostic (it works on speaker timbre, not phonemes), so it pairs with any of the 60+ STT languages.

Combine with webhooks

Recommended for any clip over a couple of minutes: pass webhook_url alongside diarize=true so the result POSTs to your endpoint when it lands, instead of polling. See Webhooks & n8n for the payload shape.

Webhooks & n8n

Pass webhook_url when you create a job and Orchard POSTs the result to that URL when the job finishes. No polling needed — perfect for n8n / Zapier / Make.

Sending a job with a webhook

curl -X POST https://api.orchardrun.com/v1/transcriptions \
  -H "Authorization: Bearer ork_..." \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://youtu.be/dQw4w9WgXcQ",
    "language": "en",
    "webhook_url": "https://your.n8n.instance/webhook/orchard-result"
  }'

Payload Orchard POSTs to your URL

Same shape as GET /v1/transcriptions/{id}:

{
  "job_id": "abc123...",
  "status": "success",         // or "failed"
  "result": {
    "text": "...",
    "language": "en",
    "duration_seconds": 1099,
    "segments": [ ... ],
    "provider": "local",
    "model": "orchard-stt-v1",
    "elapsed_ms": 17430,
    "post_processed": true
  },
  "error": null               // string when status="failed"
}

Headers we send

User-AgentOrchard-Webhook/1.0Identify Orchard in your logs.
X-Orchard-Job-Idabc123...Same id you got back from the POST.
X-Orchard-Attempt1, 2 or 3Retry counter — handle idempotently using job_id.
Content-Typeapplication/json

Retry & idempotency

  • 3 attempts total with exponential backoff: 1s · 5s · 25s.
  • 2xx = success. We stop retrying.
  • 4xx = permanent failure. We do not retry — fix your endpoint.
  • 5xx or network errors = retried up to 3 attempts.
  • Use X-Orchard-Job-Id to dedupe — same id may arrive more than once.

Ready-made n8n workflows

Importable JSON — open n8n → Workflows Import from File. Replace the placeholder API key and destination IDs before running.

Both templates use n8n's Wait node in webhook mode. The {{$execution.resumeUrl}} expression resolves to a unique URL per run, so each transcription resumes the right execution.

Editor integrations

Orchard Dictate for VS Code / Cursor — hit a hotkey, speak, get the transcript inserted at your cursor. Built for dictating prompts to AI coding agents (Cursor, Copilot, Claude) without breaking flow.

Requirements

  • sox installed (provides the rec binary the extension uses to capture mic audio).
    • macOS: brew install sox
    • Debian / Ubuntu: sudo apt-get install sox
  • An Orchard API key (create one at /keys).
  • Microphone permission for VS Code / Cursor in your OS privacy settings.

Install

  1. Install from the Visual Studio Marketplace — click Install, or in VS Code / Cursor open the Extensions sidebar and search Orchard Dictate. From the command line:
    code --install-extension Orchardrun.orchard-dictate
  2. Run the command Orchard: Set API Key (or click the ⚙️ next to the 🎤 Dictate status bar item) and paste your ork_… key. That's it — the extension ships pointed at https://api.orchardrun.com.

Usage

  • Cmd+Shift+8 (macOS) / Ctrl+Shift+8 (Win/Linux) — start recording.
  • Same shortcut again — stop, transcribe, insert at cursor.

Settings

orchardDictate.apiUrlstringOrchard backend URL. Default https://api.orchardrun.com — override only if you're self-hosting.
orchardDictate.languageauto | es | en | pt | fr | de | it | hiLanguage hint passed to the model.
orchardDictate.insertModecursor | clipboardPaste at cursor (default) or copy to clipboard for manual paste.
orchardDictate.notifyOnSuccessbooleanShow a toast on every success. Off by default — the inserted text is feedback enough.

Troubleshooting

  • Nothing happens on the hotkey: sox is probably not installed. The extension probes /opt/homebrew/bin/rec, /usr/local/bin/rec, and /usr/bin/rec before falling back to PATH, so install location shouldn't matter as long as one of those exists.
  • Recording runs but transcript is empty: mic permission missing for your editor in OS settings.

Synthesize · Text-to-Speech

POST /v1/tts/generate takes text + a voice id and returns a WAV. Synchronous — request blocks for ~1-2s on common short texts (the response body IS the audio file). Served by the same private inference cluster that handles transcription.

Request

curl -X POST https://api.orchardrun.com/v1/tts/generate \
  -H "Authorization: Bearer ork_..." \
  -F text="Hola, ¿qué tal todo por allá?" \
  -F voice_id="es_MX-claude" \
  -F voice_type="generic" \
  --output out.wav

Form fields

textstring · required · max 500 charsWhat you want spoken. Punctuation and capitalisation are respected — they shape prosody. Longer texts: split into ≤500-char chunks and concatenate the WAVs client-side.
voice_idstring · requiredOne of the entries from the voice catalog below (e.g. "en_US-amy", "es_MX-claude"). The voice's language is implicit in the id.
voice_type"generic"Use "generic" for the public voice library.
source_languageISO 639-1 code · optionalLanguage hint for the input text when it differs from the voice's language. If set + different from voice language, the text is auto-translated before synthesis. Skip for same-language use.
translate"auto" · "force" · "off" · default "auto"Translation policy: auto = translate only when source_language ≠ voice language; force = always; off = synthesize the literal text in the voice's phoneme set even if mismatched.

Response

  • 200 OK with Content-Type: audio/wav — body is the raw 22.05 kHz / 16-bit mono WAV. Stream-friendly to disk or to a browser <audio> tag.
  • 404 if voice_id is unknown.
  • 402 if your balance is exhausted (see Limits & pricing).
  • 429 if you exceed the concurrent-request limit for your plan.

Billing

Each request debits the user's balance by the duration of the generated audio. A 30-second synthesized clip costs 30 seconds from the same balance Transcribe uses. The audio is generated at roughly 1000 characters / minute of speech, so a 500-char input consumes ~30 seconds.

Cross-language synthesis

If your input text is in one language but you pick a voice in another, Orchard auto-translates before synthesis:

# Input is Spanish, voice is American English →
# server translates to English then synthesizes with Amy.
curl -X POST https://api.orchardrun.com/v1/tts/generate \
  -H "Authorization: Bearer ork_..." \
  -F text="Hola, ¿cómo estás?" \
  -F voice_id="en_US-amy" \
  -F source_language=es \
  -F translate=force \
  --output greeting_en.wav

Auto-translation falls back to the original text if the translation provider is unreachable — same-language synthesis always works.

Voice catalog

Generic voices available out of the box. Hit GET /v1/tts/voices/generic at runtime for the canonical list (it's public — no auth required, useful for populating a voice picker in your UI).

en_US-amy🇺🇸 English (US)Female · friendly, conversational
en_US-ryan🇺🇸 English (US)Male · professional, narration
es_ES-davefx🇪🇸 Spanish (Spain)Male · neutral broadcast tone
es_ES-sharvard🇪🇸 Spanish (Spain)Multi-speaker · documentation reading
es_MX-claude🇲🇽 Spanish (Mexico)Male · conversational LATAM
es_MX-ald🇲🇽 Spanish (Mexico)Male · alternate LATAM voice
pt_BR-faber🇧🇷 Portuguese (Brazil)Male · warm narration
fr_FR-siwis🇫🇷 French (France)Female · neutral conversational
de_DE-thorsten🇩🇪 GermanMale · clean broadcast
it_IT-paola🇮🇹 ItalianFemale · narration
hi_IN-pratham🇮🇳 HindiMale · standard pronunciation
hi_IN-priyamvada🇮🇳 HindiFemale · standard pronunciation

Picking the right voice for your audience

The country code in the voice id (es_ES vs es_MX) drives accent. A LATAM audience listening to es_ES-davefx will hear a Castilian accent (the θ sound on c/z); for neutral LATAM use the es_MX voices.

Programmatic listing

curl https://api.orchardrun.com/v1/tts/voices/generic | jq

Returns an array of objects with voice_id, language (ISO 639-1), locale (e.g. es_MX), flag, gender, and description. Use this in production rather than hard-coding — the catalog grows as new voices ship.

Response formats

Five formats supported on the download endpoint. The sync API uses response_format and supports json, verbose_json, and text.

texttext/plainJust the cleaned text. No metadata.
mdtext/markdownCleaned text + YAML frontmatter (language, duration, node).
srtapplication/x-subripSubRip subtitles, one cue per segment.
vtttext/vttWebVTT subtitles.
jsonapplication/jsonFull payload: text, raw_text, segments, language, duration_seconds, elapsed_ms, post_processed, node_id.
verbose_jsonapplication/jsonStandard shape: {task, language, duration, text, segments[]}.

Performance tips

We accept any audio container ffmpeg can decode (mp3, m4a, ogg, opus, flac, wav, mp4, webm). The format you pick changes upload latency, not transcription quality — every request runs on the same model.

Send Opus instead of WAV

WAV is uncompressed PCM. A 15-minute speech file is ~27 MB as WAV and only ~2 MB as Opus 32kbps. The transcription is identical (Opus at speech bitrates is perceptually lossless for our STT engine); the upload is ~8x faster and you stop bumping into upload limits on long audios.

# Convert before upload
ffmpeg -i recording.wav -c:a libopus -b:a 32k recording.opus
# 27 MB → ~2 MB. Then upload normally.

Already using Opus? WhatsApp voicenotes (.ogg) and browser MediaRecorder defaults are already Opus — pass them through directly.

Use sync vs async deliberately

  • Sync (POST /v1/audio/transcriptions) blocks. Best for short clips (≤2 min) where waiting on the response is fine.
  • Async + webhook (POST /v1/transcriptions/upload with webhook_url) frees the caller immediately and pushes the result when ready. Best for n8n / Zapier and audio over a couple of minutes.
  • Avoid polling when you can use a webhook — polling every 1-2s adds the same cumulative wait but burns request quota.

Hint header

Sync responses include X-Orchard-Hint when we notice an inefficient upload (e.g. WAV > 5 MB). Surface it in your client logs the first time it appears and you'll catch slow paths before they show up as user-visible latency.

Error codes

401 UnauthorizedMissing or invalid credentials.
402 Payment RequiredAccount balance is 0 or negative. Top up at /billing.
404 Not FoundJob, key, or user not found (also returned for jobs you don't own).
409 ConflictDownload requested before the job finished.
413 Payload Too LargeUpload exceeds 500 MB.
504 Gateway TimeoutSync transcription exceeded the cluster's timeout (600s).
5xxServer error — typically a worker or DB issue. Safe to retry.

All error responses share the same shape: { "detail": "..." }.

Limits & pricing

One unified balance per plan, metered in seconds of audio. A 30-second transcription consumes 30 seconds; a 30-second synthesized clip consumes 30 seconds. Mix Transcribe and Synthesize freely — no per-product cap to juggle.

PlansFree · $1 · $10 · $25 · EnterpriseFree (500 min/mo, opt-in), Hobby, Starter, Pro, custom Enterprise. See /billing.
Balance unit1 second of audioApplies to Transcribe (input audio length) and Synthesize (generated audio length). 1000 characters of TTS ≈ 60 seconds.
Effective ratefrom $0.00042 / min on ProPro tier ($25/mo for 60,000 min). Free tier requires explicit activation in the dashboard.
Concurrency1 / 1 / 3 / 10 / 24Free / Hobby / Starter / Pro / Enterprise. Counts STT + TTS in the same bucket. Excess requests queue rather than fail.
Rate limit30 / 60 / 300 / 900 / 5000 rpmFree / Hobby / Starter / Pro / Enterprise.
Sync timeout600 secondsPOST /v1/audio/transcriptions blocks up to this long.
Async timeout1800 secondsPer-job wall-clock cap. Practical audio length per file is ~2 hours.
Upload size (STT)500 MBPer file.
Supported audio (STT)mp3, m4a, wav, ogg, flac, mp4, webmAnything ffmpeg can decode.
Languages (STT)60+Whisper-class engine: Spanish, English, Portuguese, French, German, Italian, Hindi, Chinese, Japanese, Arabic, Russian, Polish, Dutch, Turkish, Korean, Catalan, Romanian, Greek, Hebrew, Indonesian, Vietnamese, Thai, and 40+ more. Pass ISO 639-1 code as `language` hint or omit for auto-detect.
Languages (TTS)17Generic voices: en, es, pt, fr, de, it, pl, tr, ru, nl, cs, ar, zh-cn, ja, hu, ko, hi. See /v1/tts/voices/generic for the canonical catalog.
Diarization (STT)Free 10m · Hobby 30m · Starter 300m · Pro 3,000m · Enterprise ∞Per-month quota of audio seconds that can be run with `diarize=true`. Async endpoints only. Diarized jobs debit 1.5× the audio duration from balance. See /docs#diarization.
TTS text length500 chars per requestSplit longer text client-side and concatenate the WAVs.