Audio Transcription
curl --request POST \
--url https://api.example.com/audio/transcriptions \
--header 'Content-Type: application/json' \
--data '
{
"model": "<string>",
"language": "<string>",
"prompt": "<string>",
"temperature": 123,
"response_format": "<string>",
"timestamp_granularities[]": [
"<string>"
],
"stream": true,
"user": "<string>"
}
'import requests
url = "https://api.example.com/audio/transcriptions"
payload = {
"model": "<string>",
"language": "<string>",
"prompt": "<string>",
"temperature": 123,
"response_format": "<string>",
"timestamp_granularities[]": ["<string>"],
"stream": True,
"user": "<string>"
}
headers = {"Content-Type": "application/json"}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
model: '<string>',
language: '<string>',
prompt: '<string>',
temperature: 123,
response_format: '<string>',
'timestamp_granularities[]': ['<string>'],
stream: true,
user: '<string>'
})
};
fetch('https://api.example.com/audio/transcriptions', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.example.com/audio/transcriptions",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'model' => '<string>',
'language' => '<string>',
'prompt' => '<string>',
'temperature' => 123,
'response_format' => '<string>',
'timestamp_granularities[]' => [
'<string>'
],
'stream' => true,
'user' => '<string>'
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.example.com/audio/transcriptions"
payload := strings.NewReader("{\n \"model\": \"<string>\",\n \"language\": \"<string>\",\n \"prompt\": \"<string>\",\n \"temperature\": 123,\n \"response_format\": \"<string>\",\n \"timestamp_granularities[]\": [\n \"<string>\"\n ],\n \"stream\": true,\n \"user\": \"<string>\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.example.com/audio/transcriptions")
.header("Content-Type", "application/json")
.body("{\n \"model\": \"<string>\",\n \"language\": \"<string>\",\n \"prompt\": \"<string>\",\n \"temperature\": 123,\n \"response_format\": \"<string>\",\n \"timestamp_granularities[]\": [\n \"<string>\"\n ],\n \"stream\": true,\n \"user\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.example.com/audio/transcriptions")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Content-Type"] = 'application/json'
request.body = "{\n \"model\": \"<string>\",\n \"language\": \"<string>\",\n \"prompt\": \"<string>\",\n \"temperature\": 123,\n \"response_format\": \"<string>\",\n \"timestamp_granularities[]\": [\n \"<string>\"\n ],\n \"stream\": true,\n \"user\": \"<string>\"\n}"
response = http.request(request)
puts response.read_bodyAPI Reference
Audio Transcription
Transcribe audio with multipart uploads and billing per started second through the attested gateway.
POST
/
audio
/
transcriptions
Audio Transcription
curl --request POST \
--url https://api.example.com/audio/transcriptions \
--header 'Content-Type: application/json' \
--data '
{
"model": "<string>",
"language": "<string>",
"prompt": "<string>",
"temperature": 123,
"response_format": "<string>",
"timestamp_granularities[]": [
"<string>"
],
"stream": true,
"user": "<string>"
}
'import requests
url = "https://api.example.com/audio/transcriptions"
payload = {
"model": "<string>",
"language": "<string>",
"prompt": "<string>",
"temperature": 123,
"response_format": "<string>",
"timestamp_granularities[]": ["<string>"],
"stream": True,
"user": "<string>"
}
headers = {"Content-Type": "application/json"}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
model: '<string>',
language: '<string>',
prompt: '<string>',
temperature: 123,
response_format: '<string>',
'timestamp_granularities[]': ['<string>'],
stream: true,
user: '<string>'
})
};
fetch('https://api.example.com/audio/transcriptions', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.example.com/audio/transcriptions",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'model' => '<string>',
'language' => '<string>',
'prompt' => '<string>',
'temperature' => 123,
'response_format' => '<string>',
'timestamp_granularities[]' => [
'<string>'
],
'stream' => true,
'user' => '<string>'
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.example.com/audio/transcriptions"
payload := strings.NewReader("{\n \"model\": \"<string>\",\n \"language\": \"<string>\",\n \"prompt\": \"<string>\",\n \"temperature\": 123,\n \"response_format\": \"<string>\",\n \"timestamp_granularities[]\": [\n \"<string>\"\n ],\n \"stream\": true,\n \"user\": \"<string>\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.example.com/audio/transcriptions")
.header("Content-Type", "application/json")
.body("{\n \"model\": \"<string>\",\n \"language\": \"<string>\",\n \"prompt\": \"<string>\",\n \"temperature\": 123,\n \"response_format\": \"<string>\",\n \"timestamp_granularities[]\": [\n \"<string>\"\n ],\n \"stream\": true,\n \"user\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.example.com/audio/transcriptions")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Content-Type"] = 'application/json'
request.body = "{\n \"model\": \"<string>\",\n \"language\": \"<string>\",\n \"prompt\": \"<string>\",\n \"temperature\": 123,\n \"response_format\": \"<string>\",\n \"timestamp_granularities[]\": [\n \"<string>\"\n ],\n \"stream\": true,\n \"user\": \"<string>\"\n}"
response = http.request(request)
puts response.read_bodyEndpoint
POST https://inference.phala.com/v1/audio/transcriptions
multipart/form-data. Responses are buffered JSON with an x-receipt-id header for verification.
Models and Pricing
The live model catalog lists transcription models withtranscription in output_modalities and USD-per-second rates in pricing.audio_transcription.
| Model ID | Provider | Response formats | Current price per second |
|---|---|---|---|
openai/whisper-large-v3 | NEAR AI | json, verbose_json | $0.0001 |
mistralai/voxtral-small-24b-2507 | Tinfoil | json only | $0.00005 |
2.965 seconds is billed as 3 seconds.
Request Body
string
required
Transcription model ID:
openai/whisper-large-v3 or mistralai/voxtral-small-24b-2507.file
required
Exactly one nonempty file, up to 25 MiB (26,214,400 bytes). The filename must include a supported extension:
flac, mp3, mp4, mpeg, mpga, m4a, ogg, wav, or webm.string
Language code of 2–3 lowercase ASCII letters, for example
en.string
Optional context for transcription, up to 4,096 Unicode characters.
number
Sampling temperature from
0 through 1.string
json by default. Whisper also accepts verbose_json; Voxtral supports json only and returns 400 invalid_request_error for verbose_json. Text and subtitle formats are not supported.string[]
Accepted values are
word and/or segment, once each, with Whisper and response_format=verbose_json. Repeat the form field for both. The spelling timestamp_granularities is also accepted; do not mix spellings. Word-level timestamps are not returned; segments can be absent.boolean
Omit this field or send the literal form value
false. Streaming is not supported; other values return 400.string
Accepted for SDK compatibility but removed before forwarding upstream.
400. Unknown form fields, including provider, chunking_strategy, include[], and speaker fields, return 400 unknown_parameter. ACI E2EE is not supported on this endpoint. The upload must complete within 60 seconds; oversized files or request bodies return 413.
Routing Constraints
The Phala TEE-only endpoint requires verified serving for audio requests. Aprovider form field is not accepted.
string
Optional JSON object with the same routing constraints as the JSON provider field, for example
{"aci_verified":true}. The gateway consumes this header and does not forward it upstream. The header must appear once, contain only ASCII bytes, and be at most 8,192 bytes./v1/audio/transcriptions in service_capabilities.supported_multipart_constraint_endpoints. Refuse the upload if support is absent. Session pins must intersect locally accepted sessions; verify the receipt’s cited session against the effective policy. Receipt body hashes do not commit to this header. See Attested Routing.
Examples
Let the client set the multipartContent-Type boundary. Node SDK uploads need an explicit filename: toFile(buf) alone uses unknown_file, which has no supported extension and returns 400 invalid_parameter with param: file.
curl --fail-with-body --silent --show-error \
https://inference.phala.com/v1/audio/transcriptions \
-H "Authorization: Bearer $PHALA_AI_API_KEY" \
-D audio-headers.txt \
-F 'model=openai/whisper-large-v3' \
-F 'file=@audio.wav' \
-F 'response_format=json'
import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ["PHALA_AI_API_KEY"],
base_url="https://inference.phala.com/v1",
)
with open("audio.wav", "rb") as audio:
response = client.audio.transcriptions.create(
model="openai/whisper-large-v3",
file=audio,
response_format="json",
)
print(response.text)
import { readFile } from "node:fs/promises";
import OpenAI, { toFile } from "openai";
const client = new OpenAI({
apiKey: process.env.PHALA_AI_API_KEY,
baseURL: "https://inference.phala.com/v1",
});
const buf = await readFile("audio.wav");
const response = await client.audio.transcriptions.create({
model: "openai/whisper-large-v3",
file: await toFile(buf, "audio.wav"),
response_format: "json",
});
console.log(response.text);
pap curl https://inference.phala.com/v1/audio/transcriptions -- \
--fail-with-body --silent --show-error \
-H @auth-headers.txt \
-D audio-headers.txt \
-F 'model=openai/whisper-large-v3' \
-F 'file=@audio.wav' \
-F 'response_format=json'
mistralai/voxtral-small-24b-2507 and keep response_format=json.
For the Private AI Proxy example, create auth-headers.txt containing Authorization: Bearer <API_KEY>, restrict its permissions to your user, and remove it after use. No manual X-ACI-Provider header is needed for this request on the Phala TEE-only endpoint. pap curl verifies the gateway and pins its TLS channel; it does not audit response receipts. Use pap serve or an ACI verifier SDK for receipt audits. Ordinary cURL and OpenAI SDK clients do not verify the gateway or audit receipts.
Response
Example JSON response for three billable seconds of Whisper audio at the current rate:{
"text": "Hello, world.",
"usage": { "type": "duration", "seconds": 3, "cost": 0.0003 }
}
json returns text, usage, and optional languages: [{"code":"en"}]. Whisper’s verbose_json response includes duration and language alongside text and usage. Word-level timestamps are not returned. With verbose_json, Whisper responses may include segments; they can be absent. A segment’s no_speech_prob field may also be missing.
usage.seconds is an integer rounded up to the next whole second; verbose duration retains the raw value. The gateway adds usage.cost in USD when audio pricing is available: billable seconds times the model’s pricing.audio_transcription rate. Without a valid duration, the transcript is withheld with 502 and no audio is billed.
Verification
Capturex-receipt-id and fetch GET /v1/aci/receipts/{id} with the same bearer token. Audio receipts have chat_id: null; use the receipt ID. See Get Receipt and Verify a Response.
Errors
Errors use a JSONerror object, with error.code and error.param where applicable.
| Status | Error field | Condition |
|---|---|---|
| 400 | code: unknown_parameter | Unrecognized form field; param names the field. |
| 400 | code: invalid_parameter | Missing or invalid required fields, filename extension, response format, timestamp options, duplicate scalar fields, invalid UTF-8, malformed multipart, or exceeded multipart metadata limits. |
| 400 | type: invalid_request_error, code: null, param: null | Voxtral with verbose_json: Currently do not support verbose_json for voxtral-small-24b. |
| 400 | type: invalid_request_error | Invalid or duplicate X-ACI-Provider header, including malformed constraints. |
| 400 | type: e2ee_unsupported_endpoint | E2EE headers are present. |
| 404 | type: model_not_found | No eligible route supports transcription for the model. |
| 408 | code: upload_timeout | Upload exceeds the 60-second deadline; the connection closes. |
| 413 | code: request_too_large | File or whole-body limit exceeded; the connection closes. |
| 429 | type: rate_limit_error, code: rate_limit_exceeded | Capacity is full or a rate limit is reached; respect Retry-After when present. |
| 502 | type: upstream_error | Upstream failure, response over 8 MiB, missing string text, or invalid duration. |
Related
List Models
Get Receipt
Was this page helpful?

