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
| Models | Served formats | stream=true | Billing usage |
|---|---|---|---|
openai/gpt-4o-transcribe, openai/gpt-4o-mini-transcribe | json, verbose_json, faucet_json | Yes | tokens |
openai/gpt-4o-transcribe-diarize | diarized_json, faucet_json | Yes | tokens |
openai/gpt-transcribe | json, verbose_json, faucet_json | Yes | audio duration |
openai/whisper-1 | json, verbose_json, faucet_json | No | audio duration |
elevenlabs/scribe-v2 | json, faucet_json | No | audio 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 missingwordssays 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_jsonchanges 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:
| Parameter | Surcharge |
|---|---|
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.
keytermswithentity_detectionis 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=falseruns nothing and costs nothing extra, and neither does an emptykeyterms. - Ask which apply.
surcharged_parametersonGET /v1/models/{id}names them for the model you selected. A surcharged parameter the resolved model carries no rate for is a400, not a discount.
Some ElevenLabs parameters are refused outright, each because the alternative is a wrong bill rather than a wrong response:
| Refused | Because |
|---|---|
source_url, cloud_storage_url | No file in the request, so nothing bounds its cost before it is spent — a few hundred bytes can buy hours of audio |
webhook, webhook_id | Answers with a job id rather than a transcript: nothing to return, and no duration to bill |
use_multi_channel | Whether combined duration bills once or per channel is unconfirmed, and guessing low under-bills severalfold |
prompt | No 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.