faucet
Request access
Guides

Transcription

Upload audio to /v1/audio/transcriptions and get an OpenAI-shaped transcript back — formats, streaming, provider options and surcharges.

POST /v1/audio/transcriptions accepts an audio file as multipart/form-data and returns an OpenAI-shaped transcript. It is the only Faucet request whose body is not JSON.

Let your client set the multipart boundary

Setting Content-Type: multipart/form-data by hand leaves the boundary out and produces a malformed upload. curl -F and the SDKs below do it correctly.

curl https://api.intfaucet.com/v1/audio/transcriptions \
  -H "Authorization: Bearer $FAUCET_API_KEY" \
  -F "file=@meeting.mp3" \
  -F "model=openai/gpt-4o-mini-transcribe" \
  -F "response_format=json"

The official OpenAI SDK uses the same endpoint and builds the multipart request for you:

import os

from openai import OpenAI

client = OpenAI(
    base_url="https://api.intfaucet.com/v1",
    api_key=os.environ["FAUCET_API_KEY"],
)

with open("meeting.mp3", "rb") as audio:
    transcript = client.audio.transcriptions.create(
        model="openai/gpt-4o-mini-transcribe",
        file=audio,
        response_format="json",
    )

print(transcript.text)

Every multipart request body is capped at 25 MiB while Faucet buffers uploads. That ceiling comes from OpenAI's published file limit and also applies to ElevenLabs through Faucet, even though ElevenLabs accepts larger files upstream. An oversized upload is a 413 before the body is read, so you are not made to send it first.

What each model serves

ModelsServed formatsstream=trueBilling usage
openai/gpt-4o-transcribe, openai/gpt-4o-mini-transcribejson, verbose_json, faucet_jsonYestokens
openai/gpt-4o-transcribe-diarizediarized_json, faucet_jsonYestokens
openai/gpt-transcribejson, verbose_json, faucet_jsonYesaudio duration
openai/whisper-1json, verbose_json, faucet_jsonNoaudio duration
elevenlabs/scribe-v2json, faucet_jsonNoaudio duration

Rather than copying this table into your code, ask the gateway — every row is served per model as response_formats and supports_streaming on GET /v1/models/{id}, derived from the same functions this endpoint rejects with.

A non-streaming response puts the transcript in text. Its usage is a union that matches the model's price: token-priced models return type: "tokens" with input and output counts, while duration-priced models return type: "duration" with seconds. On the models marked streaming, adding -F "stream=true" returns transcription SSE and finishes with its usage event — nothing has to be asked for, unlike chat.

`text`, `srt` and `vtt` are not served

Even when the upstream model can produce them. They have nowhere to carry usage; accepting one would make a paid transcription look free in Faucet's meter. Unsupported streaming and formats are rejected before the audio is sent upstream.

faucet_json: one shape from either provider

Word timings arrive in two places today — OpenAI puts them in segments[] beside a duration, ElevenLabs puts them under an elevenlabs key. response_format=faucet_json returns the same envelope from either:

{
  "text": "…",
  "language": "en",
  "duration": 94,
  "words":    [{ "text": "The", "start": 0, "end": 0.35, "speaker": "speaker_0" }],
  "segments": [{ "text": "The quick brown fox", "start": 0, "end": 1.8 }],
  "usage": { "type": "duration", "seconds": 94 },
  "elevenlabs": { /* the provider's own body, unchanged */ }
}

It is named as Faucet's because it is Faucet's. Putting normalized ElevenLabs word objects under verbose_json would claim OpenAI's contract for a body OpenAI did not define, and a client reading duration there would get an invented number.

Three rules it keeps:

  • Nothing is synthesized. A field the provider did not send is absent, not zero and not an empty array. words: [] would say the audio had no words in it; a missing words says this model does not report them.
  • The vendor body survives, unchanged, under its provider key. Normalizing is additive, so a field this shape does not model is not lost.
  • The bill does not move. Asking for faucet_json changes what the gateway asks the provider for and changes nothing about what you pay.

Not available on a streamed request

faucet_json is assembled from the complete response, and a stream has none until it ends. Sending it with stream=true is a 400. Omit stream, or ask for a format the provider streams natively.

Two things it leaves out on purpose. Confidence: ElevenLabs reports a per-word probability and OpenAI reports a per-segment log probability, which is not the same quantity — publishing both under one key would be exactly the invention this format exists to avoid, so both stay under the vendor key. Word timings on OpenAI need timestamp_granularities[], which is whisper-1-only and not added unasked. The cross-provider guarantee is the shape, not that every model fills every field.

ElevenLabs provider options

Provider-native parameters ride on provider_options, keyed by provider slug so options for one upstream cannot leak into a request to another. On a multipart request, send it as a JSON-encoded field.

Four parameters are served on elevenlabs/scribe-v2 and raise its per-minute rate rather than the quantity billed:

ParameterSurcharge
entity_detection+30%
entity_redaction+30%
keyterms+20%
detect_speaker_roles+10%

Four things to know before relying on those figures:

  • They compound rather than sum. keyterms with entity_detection is 1.2 × 1.3 = +56%, not +50%.
  • They are not yet confirmed against an invoice. ElevenLabs states them in two places and not identically. Faucet rounds toward over-billing rather than losing margin silently, which is refundable; treat them as indicative.
  • Present is not the same as on. entity_detection=false runs nothing and costs nothing extra, and neither does an empty keyterms.
  • Ask which apply. surcharged_parameters on GET /v1/models/{id} names them for the model you selected. A surcharged parameter the resolved model carries no rate for is a 400, not a discount.

Some ElevenLabs parameters are refused outright, each because the alternative is a wrong bill rather than a wrong response:

RefusedBecause
source_url, cloud_storage_urlNo file in the request, so nothing bounds its cost before it is spent — a few hundred bytes can buy hours of audio
webhook, webhook_idAnswers with a job id rather than a transcript: nothing to return, and no duration to bill
use_multi_channelWhether combined duration bills once or per channel is unconfirmed, and guessing low under-bills severalfold
promptNo counterpart. Its nearest equivalent takes a term list rather than free text and costs 20% more, so translating it would either drop it or bill above what was asked for

Scribe responses also preserve word timing, language and speaker metadata under an elevenlabs key on every format, not only faucet_json.

Scribe v2 Realtime is not carried. It is a WebSocket API, and this endpoint is a POST that returns a transcript.