Developers

Speech-to-text API

Transcribe completed audio files with queued jobs, free estimates, idempotent submission and usage tracking on both API hosts.

On this page
  1. Endpoints
  2. Prepare an audio file
  3. Submit a transcription job
  4. Idempotency key
  5. Estimate without creating a job
  6. Read the transcript
  7. Using api.multitel.net

This API transcribes completed audio files as queued jobs. Submit a file, keep the returned job ID and fetch the job until processing has finished. Send every request over HTTPS with credentials for your account, either HTTP Basic credentials as shown in the examples or an API token, as described in Authentication. Keep credentials on your server.

Endpoints

Two API hosts, https://api.multi.tel and the compatibility host https://api.multitel.net, share the same account-scoped jobs, idempotency keys, limits and billing. They differ in upload encoding and response envelope, so use one host consistently with a matching parser.

OperationMethod and path
Service informationGET /v3/speech_to_text
Submit a jobPOST /v3/speech_to_text
Estimate without creating a jobPOST /v3/speech_to_text/estimate
Read a jobGET /v3/speech_to_text/JOB_ID
Settle a job awaiting paymentPOST /v3/speech_to_text/JOB_ID/settle
UsageGET /v3/speech_to_text/usage

On api.multi.tel, every path is also available with the prefix /v1/speech-to-text in place of /v3/speech_to_text, for example POST /v1/speech-to-text/estimate. On api.multitel.net, use the /v3/speech_to_text paths.

The service information response reports mode (batch), realtime (false), paidEnabled (whether new paid submissions are currently accepted), pricePerMinuteUsd (0.005), billingUnit (audio_second), maxAudioSeconds (7200) and maxUploadBytes (26214400), together with a short description. Reading it is free and works while paid submissions are disabled; it returns 503 transcription_unavailable only when the service as a whole is unavailable.

Prepare an audio file

Use a completed audio file of at most 25 MiB (26,214,400 bytes) and at most 2 hours (7,200 seconds). At submission, the file must be recognized as MP3, WAV, Ogg, FLAC, MOV or Matroska media that contains an audio stream, with a duration in its metadata greater than zero and no more than 7,200 seconds. A recognized file that fails these checks is rejected with 422 unsupported_audio. A file that cannot be read as any of these formats can currently be rejected with a server error (500) instead; do not resubmit such a file unchanged. A file extension alone does not establish compatibility.

Passing these checks does not guarantee that the audio can be decoded. Audio is processed as one or two channels. A file with more than two audio channels, a file whose decoded audio is longer than 2 hours, or any other file that fails decoding during processing ends as a failed job, typically with decode_failed, and is not charged. Convert multichannel audio to mono or stereo before submitting.

The optional language hint is a lowercase two- or three-letter code for the language spoken, such as en. To give no hint, omit the field; api.multi.tel rejects an empty string. The hint guides processing but is not checked against a list of supported languages, so use a valid code for the spoken language.

Submit a transcription job

Send JSON with Content-Type: application/json to POST https://api.multi.tel/v3/speech_to_text. The request body can be up to 35 MiB, which accommodates a maximum-size file after base64 encoding; a larger body is rejected with 413.

FieldMeaning
audioBase64Required. The complete file in standard base64 (A–Z, a–z, 0–9, +, /) with = padding when needed. Do not add a data-URL prefix, line breaks or spaces, and do not use the URL-safe alphabet. The value must decode to between 1 byte and 25 MiB and re-encode to exactly the same text.
languageOptional language hint, as described above.

Any other JSON property, including idempotency_key, is rejected with 400 invalid_transcription_request. Send the key as a header.

Idempotency key

Each submission requires an Idempotency-Key header of 1 to 190 characters. Use ASCII letters, digits, -, _, . and :, without leading or trailing spaces: the compatibility host trims surrounding whitespace and measures the key in bytes, so other characters can be counted differently by each host. A missing key, or a key of 191 or more characters, is rejected with 400.

Keys are scoped to your account and to speech-to-text, across both hosts and all credentials of your account. Resending the same key with the same file and language returns the existing job with HTTP 200 and duplicate: true, whatever its status and even if processing has been updated since the job was created, without another charge. Reusing a key with a different file or language returns 409 idempotency_conflict. A new key creates a new job even for an identical file, with a separate charge if processing completes successfully.

API_USER="your-api-username"
API_PASSWORD="your-api-password"

node -e 'process.stdout.write(JSON.stringify({audioBase64:require("node:fs").readFileSync("completed-call.mp3").toString("base64"),language:"en"}))' > completed-call.json

curl --fail-with-body --silent --show-error --user "$API_USER:$API_PASSWORD" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: speech-call-001" \
  --data-binary @completed-call.json \
  -o speech-job.json \
  https://api.multi.tel/v3/speech_to_text

node -p 'JSON.parse(require("node:fs").readFileSync("speech-job.json","utf8")).data.id'

A new job returns HTTP 202 with the fields shown below, and a replay returns HTTP 200 with the full job object described in Read the transcript. Read the job ID, a UUID, from data.id. A new job's response looks like this (the identifier and values are examples):

{
  "status": "success",
  "data": {
    "id": "3f6c1b2e-8a4d-4e7f-9c21-5b0d7e4a9f13",
    "status": "queued",
    "mode": "batch",
    "realtime": false,
    "source": "api",
    "durationSeconds": 90.5,
    "billedSeconds": 91,
    "costUsd": "0.007583334",
    "pricePerMinuteUsd": "0.005"
  }
}

While a job is queued or running, durationSeconds, billedSeconds and costUsd are estimates from the file's metadata. The final charge uses the decoded duration.

A submission can also return 402 insufficient_balance when a prepaid balance cannot cover the job together with your other outstanding jobs, 429 transcription_queue_full when your account or the service has too many outstanding jobs, 429 audio_validation_busy when file validation is at capacity, and 503 paid_transcription_not_enabled while paid transcription is disabled. Retry 429 and 503 responses later with the same key.

Estimate without creating a job

Send the same JSON body to POST https://api.multi.tel/v3/speech_to_text/estimate. No idempotency key is required. An estimate validates and measures the file but does not transcribe it, create a job, reserve funds, check your balance or charge your account. A language hint is accepted but does not affect the estimate.

A successful estimate returns HTTP 200 with estimate: true, charged: false, mode, realtime, durationSeconds, billedSeconds, pricePerMinuteUsd and costUsd under data. It uses the duration in the file's metadata, while a completed job is charged on its decoded duration, which can differ. Estimates apply the same file checks as submissions. They are available only while paid transcription is enabled (otherwise 503 paid_transcription_not_enabled). They can return 429 audio_validation_busy when file validation is at capacity; retry after a short wait.

Read the transcript

Poll GET /v3/speech_to_text/JOB_ID as described in the job workflow guide. The job object contains these fields:

FieldMeaning
idThe job UUID.
statusqueued, running, completed, failed or awaiting_payment.
progressA percentage from 0 to 99 while running, when available; 100 when completed; otherwise null.
durationSeconds, billedSecondsAudio duration and billed whole seconds. These are metadata estimates while the job is queued or running, and use the decoded duration once it has completed or is awaiting payment.
costUsd, costIsEstimateThe cost in US dollars as a decimal string with nine decimal places. costIsEstimate is true while the job is queued or running. A failed job is not charged, even though its costUsd still shows the estimate.
pricePerMinuteUsd0.005 for API jobs, and 0.000 for free Hosted PBX recording jobs.
failureCodeFor a failed job: decode_failed (the audio could not be decoded, for example because it has more than two channels or is longer than 2 hours once decoded), inference_timeout (processing took too long), service_error, or worker_lease_lost (processing was interrupted). The codes unsupported_audio and quality_check_failed are also defined. A job awaiting payment shows insufficient_balance, which can remain after the job is settled.
source, modelVersion, createdAtsource is api for API submissions, or hpbx for a Hosted PBX recording job of your account. modelVersion is an opaque processing-version string. createdAt is the submission time.
duplicatetrue on responses that return an existing job.
resultPresent only when status is completed.
Result fieldMeaning
textThe full transcript: all segment texts joined with line breaks, in segment order.
segmentsAn array of segments ordered by start time. Each segment covers a section of audio up to about 29 seconds long. Its start and end (seconds from the start of the audio) mark that section, not individual words or sentences, and consecutive sections can overlap by up to 1 second. Each segment also has text and language (a language code, which can be empty), plus channel for two-channel results.
channelCount1 or 2. Present in current results; older results can omit it.
audioDurationMsThe decoded duration in milliseconds. Billing rounds it up to whole seconds.
qualityFlagsEmpty, or review_recommended together with repetition_detected (a phrase repeats unusually often within a section), no_speech (no speech was transcribed), or both.
pipelineVersionMay appear. An opaque processing identifier.

Transcripts and their timestamps, language labels and channel labels are produced automatically and are not guaranteed to be accurate. Treat any quality flag as a reason to review the result, and compare important names, numbers and commitments with the original audio. Long silences and very quiet passages are skipped and produce no segments. Results do not identify individual speakers. When a result reports a channelCount of 2, every segment has a channel of A (the first, or left, channel) or B (the second, or right, channel). This separates speakers only when each speaker was recorded on a separate channel. Two-channel audio is billed once on its elapsed duration, not per channel. Mono results have no channel labels.

Using api.multitel.net

The compatibility endpoint POST https://api.multitel.net/v3/speech_to_text accepts multipart/form-data with an audio file part and an optional language field, rather than JSON audioBase64. An empty language value is treated as no hint. Send the same Idempotency-Key header. If the header is absent, this host also accepts an idempotency_key form field. Fields named source, free, billing_source, realtime or stream are rejected with 400 unsupported_transcription_option; other form fields are ignored. This host also returns these errors:

  • 400 idempotency_key_required for a missing or overlong key.
  • 400 audio_upload_required for a missing upload, or one that was not received completely.
  • 400 invalid_language for an invalid language.
  • 413 invalid_audio_size for an empty or oversized file.
  • 400 invalid_job for a malformed job ID.

Responses from this host's speech-to-text service use the envelope {"status":{"code":HTTP_STATUS},"response":{...}}. Note that status is an object, not a string. On success, response holds the same object as data on the primary host, so the job ID is response.id. On error, response.code holds the error code; no message or request ID is included. Authentication failures (401) are returned by the host's general sign-in check and can have a different body, so check the HTTP status before parsing. Do not interchange the upload encoding or response parser between hosts.

For an estimate on this host, POST the same multipart fields to /v3/speech_to_text/estimate. No idempotency key is required, and the estimate is under response.

API_USER="your-api-username"
API_PASSWORD="your-api-password"

curl --fail-with-body --silent --show-error --user "$API_USER:$API_PASSWORD" \
  -H "Idempotency-Key: speech-call-001" \
  -F "audio=@completed-call.mp3" -F "language=en" \
  -o speech-job.json \
  https://api.multitel.net/v3/speech_to_text

node -p 'JSON.parse(require("node:fs").readFileSync("speech-job.json","utf8")).response.id'