curl --request POST \
--url https://api.pyai.com/v1/transcription/jobs \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"audio_url": "<string>",
"model": "pyai-hear-telephony",
"channel": false,
"diarize": false,
"numerals": true,
"smart_format": false,
"dictation": false,
"drop_fillers": false,
"vocabulary": [
"<string>"
],
"output_formats": [
"json"
],
"webhook_url": "<string>",
"trace": false,
"rule_pack": {},
"call_id": "<string>",
"pack_id": "<string>",
"customer_name": "<string>",
"crm_fields": {}
}
'import requests
url = "https://api.pyai.com/v1/transcription/jobs"
payload = {
"audio_url": "<string>",
"model": "pyai-hear-telephony",
"channel": False,
"diarize": False,
"numerals": True,
"smart_format": False,
"dictation": False,
"drop_fillers": False,
"vocabulary": ["<string>"],
"output_formats": ["json"],
"webhook_url": "<string>",
"trace": False,
"rule_pack": {},
"call_id": "<string>",
"pack_id": "<string>",
"customer_name": "<string>",
"crm_fields": {}
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
audio_url: '<string>',
model: 'pyai-hear-telephony',
channel: false,
diarize: false,
numerals: true,
smart_format: false,
dictation: false,
drop_fillers: false,
vocabulary: ['<string>'],
output_formats: ['json'],
webhook_url: '<string>',
trace: false,
rule_pack: {},
call_id: '<string>',
pack_id: '<string>',
customer_name: '<string>',
crm_fields: {}
})
};
fetch('https://api.pyai.com/v1/transcription/jobs', 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.pyai.com/v1/transcription/jobs",
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([
'audio_url' => '<string>',
'model' => 'pyai-hear-telephony',
'channel' => false,
'diarize' => false,
'numerals' => true,
'smart_format' => false,
'dictation' => false,
'drop_fillers' => false,
'vocabulary' => [
'<string>'
],
'output_formats' => [
'json'
],
'webhook_url' => '<string>',
'trace' => false,
'rule_pack' => [
],
'call_id' => '<string>',
'pack_id' => '<string>',
'customer_name' => '<string>',
'crm_fields' => [
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"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.pyai.com/v1/transcription/jobs"
payload := strings.NewReader("{\n \"audio_url\": \"<string>\",\n \"model\": \"pyai-hear-telephony\",\n \"channel\": false,\n \"diarize\": false,\n \"numerals\": true,\n \"smart_format\": false,\n \"dictation\": false,\n \"drop_fillers\": false,\n \"vocabulary\": [\n \"<string>\"\n ],\n \"output_formats\": [\n \"json\"\n ],\n \"webhook_url\": \"<string>\",\n \"trace\": false,\n \"rule_pack\": {},\n \"call_id\": \"<string>\",\n \"pack_id\": \"<string>\",\n \"customer_name\": \"<string>\",\n \"crm_fields\": {}\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
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.pyai.com/v1/transcription/jobs")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"audio_url\": \"<string>\",\n \"model\": \"pyai-hear-telephony\",\n \"channel\": false,\n \"diarize\": false,\n \"numerals\": true,\n \"smart_format\": false,\n \"dictation\": false,\n \"drop_fillers\": false,\n \"vocabulary\": [\n \"<string>\"\n ],\n \"output_formats\": [\n \"json\"\n ],\n \"webhook_url\": \"<string>\",\n \"trace\": false,\n \"rule_pack\": {},\n \"call_id\": \"<string>\",\n \"pack_id\": \"<string>\",\n \"customer_name\": \"<string>\",\n \"crm_fields\": {}\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.pyai.com/v1/transcription/jobs")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"audio_url\": \"<string>\",\n \"model\": \"pyai-hear-telephony\",\n \"channel\": false,\n \"diarize\": false,\n \"numerals\": true,\n \"smart_format\": false,\n \"dictation\": false,\n \"drop_fillers\": false,\n \"vocabulary\": [\n \"<string>\"\n ],\n \"output_formats\": [\n \"json\"\n ],\n \"webhook_url\": \"<string>\",\n \"trace\": false,\n \"rule_pack\": {},\n \"call_id\": \"<string>\",\n \"pack_id\": \"<string>\",\n \"customer_name\": \"<string>\",\n \"crm_fields\": {}\n}"
response = http.request(request)
puts response.read_body{
"job_id": "job_aZ09...",
"status": "queued",
"created_at": 123,
"updated_at": 123,
"result": {
"text": "[speaker_1] Hello everyone.",
"audio_seconds": 120,
"speakers": 1,
"words": [
{
"word": "Hello",
"start": 0.42,
"end": 0.81,
"confidence": 0.98,
"speaker": "speaker_1",
"channel": 0
}
],
"segments": [
{
"id": 0,
"text": "Hello everyone.",
"start": 0.42,
"end": 1.8,
"speaker": "speaker_1",
"channel": 0
}
]
},
"result_url": "<string>",
"error": "<string>"
}{
"title": "<string>",
"status": 123,
"type": "<string>",
"detail": "<string>",
"request_id": "<string>"
}{
"error": {
"message": "<string>",
"type": "<string>",
"code": "invalid_request_error",
"param": "<string>"
}
}{
"error": {
"message": "<string>",
"type": "<string>",
"code": "invalid_request_error",
"param": "<string>"
}
}{
"error": {
"message": "<string>",
"type": "<string>",
"code": "invalid_request_error",
"param": "<string>"
}
}{
"title": "<string>",
"status": 123,
"type": "<string>",
"detail": "<string>",
"request_id": "<string>"
}{
"error": {
"message": "<string>",
"type": "<string>",
"code": "invalid_request_error",
"param": "<string>"
}
}Create an async transcription job
Submit audio for batch transcription. Provide exactly one source:
either audio_url (an https URL we fetch; the input is processed transiently and never written to durable input storage)
or a multipart upload (multipart/form-data with an audio file part and the same fields as form fields).
Returns 202 immediately with a queued job; poll GET /v1/transcription/jobs/{id} or supply a webhook_url for a signed completion callback.
Completed JSON results include word- and segment-level offsets in decimal seconds from the decoded source-media timeline. Silence is not removed or compacted; resampling and internal chunking do not shift later offsets.
Set channel: true for stereo (dual-channel) recordings to get exact speaker separation per channel. Use diarize: true for model-derived speaker labels on mono audio; do not set both.
Set vocabulary only when this job has known names, brands, products, or distinctive terms. PyAI uses up to five sanitized terms. If stored vocabulary is enabled for batch, request terms come first and stored suggestions fill remaining slots.
Input limits: multipart uploads are limited to 1 GiB; audio_url downloads are limited to 512 MiB. There is no separate media-duration ceiling. Inputs must contain a decodable audio stream; the stable output formats are JSON, SRT, and VTT.
Retention: URL-fetched input bytes are not persisted. Uploaded input audio is retained for up to 7 days and result artifacts for up to 30 days. This endpoint has no store: false mode. DELETE /v1/transcription/jobs/{id} cancels queued/running work; it is not an erasure endpoint.
Requires the transcribe:jobs scope.
curl --request POST \
--url https://api.pyai.com/v1/transcription/jobs \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"audio_url": "<string>",
"model": "pyai-hear-telephony",
"channel": false,
"diarize": false,
"numerals": true,
"smart_format": false,
"dictation": false,
"drop_fillers": false,
"vocabulary": [
"<string>"
],
"output_formats": [
"json"
],
"webhook_url": "<string>",
"trace": false,
"rule_pack": {},
"call_id": "<string>",
"pack_id": "<string>",
"customer_name": "<string>",
"crm_fields": {}
}
'import requests
url = "https://api.pyai.com/v1/transcription/jobs"
payload = {
"audio_url": "<string>",
"model": "pyai-hear-telephony",
"channel": False,
"diarize": False,
"numerals": True,
"smart_format": False,
"dictation": False,
"drop_fillers": False,
"vocabulary": ["<string>"],
"output_formats": ["json"],
"webhook_url": "<string>",
"trace": False,
"rule_pack": {},
"call_id": "<string>",
"pack_id": "<string>",
"customer_name": "<string>",
"crm_fields": {}
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
audio_url: '<string>',
model: 'pyai-hear-telephony',
channel: false,
diarize: false,
numerals: true,
smart_format: false,
dictation: false,
drop_fillers: false,
vocabulary: ['<string>'],
output_formats: ['json'],
webhook_url: '<string>',
trace: false,
rule_pack: {},
call_id: '<string>',
pack_id: '<string>',
customer_name: '<string>',
crm_fields: {}
})
};
fetch('https://api.pyai.com/v1/transcription/jobs', 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.pyai.com/v1/transcription/jobs",
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([
'audio_url' => '<string>',
'model' => 'pyai-hear-telephony',
'channel' => false,
'diarize' => false,
'numerals' => true,
'smart_format' => false,
'dictation' => false,
'drop_fillers' => false,
'vocabulary' => [
'<string>'
],
'output_formats' => [
'json'
],
'webhook_url' => '<string>',
'trace' => false,
'rule_pack' => [
],
'call_id' => '<string>',
'pack_id' => '<string>',
'customer_name' => '<string>',
'crm_fields' => [
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"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.pyai.com/v1/transcription/jobs"
payload := strings.NewReader("{\n \"audio_url\": \"<string>\",\n \"model\": \"pyai-hear-telephony\",\n \"channel\": false,\n \"diarize\": false,\n \"numerals\": true,\n \"smart_format\": false,\n \"dictation\": false,\n \"drop_fillers\": false,\n \"vocabulary\": [\n \"<string>\"\n ],\n \"output_formats\": [\n \"json\"\n ],\n \"webhook_url\": \"<string>\",\n \"trace\": false,\n \"rule_pack\": {},\n \"call_id\": \"<string>\",\n \"pack_id\": \"<string>\",\n \"customer_name\": \"<string>\",\n \"crm_fields\": {}\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
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.pyai.com/v1/transcription/jobs")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"audio_url\": \"<string>\",\n \"model\": \"pyai-hear-telephony\",\n \"channel\": false,\n \"diarize\": false,\n \"numerals\": true,\n \"smart_format\": false,\n \"dictation\": false,\n \"drop_fillers\": false,\n \"vocabulary\": [\n \"<string>\"\n ],\n \"output_formats\": [\n \"json\"\n ],\n \"webhook_url\": \"<string>\",\n \"trace\": false,\n \"rule_pack\": {},\n \"call_id\": \"<string>\",\n \"pack_id\": \"<string>\",\n \"customer_name\": \"<string>\",\n \"crm_fields\": {}\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.pyai.com/v1/transcription/jobs")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"audio_url\": \"<string>\",\n \"model\": \"pyai-hear-telephony\",\n \"channel\": false,\n \"diarize\": false,\n \"numerals\": true,\n \"smart_format\": false,\n \"dictation\": false,\n \"drop_fillers\": false,\n \"vocabulary\": [\n \"<string>\"\n ],\n \"output_formats\": [\n \"json\"\n ],\n \"webhook_url\": \"<string>\",\n \"trace\": false,\n \"rule_pack\": {},\n \"call_id\": \"<string>\",\n \"pack_id\": \"<string>\",\n \"customer_name\": \"<string>\",\n \"crm_fields\": {}\n}"
response = http.request(request)
puts response.read_body{
"job_id": "job_aZ09...",
"status": "queued",
"created_at": 123,
"updated_at": 123,
"result": {
"text": "[speaker_1] Hello everyone.",
"audio_seconds": 120,
"speakers": 1,
"words": [
{
"word": "Hello",
"start": 0.42,
"end": 0.81,
"confidence": 0.98,
"speaker": "speaker_1",
"channel": 0
}
],
"segments": [
{
"id": 0,
"text": "Hello everyone.",
"start": 0.42,
"end": 1.8,
"speaker": "speaker_1",
"channel": 0
}
]
},
"result_url": "<string>",
"error": "<string>"
}{
"title": "<string>",
"status": 123,
"type": "<string>",
"detail": "<string>",
"request_id": "<string>"
}{
"error": {
"message": "<string>",
"type": "<string>",
"code": "invalid_request_error",
"param": "<string>"
}
}{
"error": {
"message": "<string>",
"type": "<string>",
"code": "invalid_request_error",
"param": "<string>"
}
}{
"error": {
"message": "<string>",
"type": "<string>",
"code": "invalid_request_error",
"param": "<string>"
}
}{
"title": "<string>",
"status": 123,
"type": "<string>",
"detail": "<string>",
"request_id": "<string>"
}{
"error": {
"message": "<string>",
"type": "<string>",
"code": "invalid_request_error",
"param": "<string>"
}
}Authorizations
Use Authorization: Bearer pyai_live_... (or pyai_test_...).
Headers
Opt-in safe retry (JSON body path). Reusing the key with an identical body replays the original 202 response; reusing it with a different body returns 409.
255Body
HTTPS URL of the audio to transcribe. PyAI fetches the input transiently without writing it to durable input storage. Maximum response body: 512 MiB.
Dual-channel (stereo) separation. Channel 0 is labelled speaker_1, channel 1 speaker_2; labels are neutral and do not infer agent/customer roles. Do not combine with diarize.
Model-derived speaker separation for mono audio. Labels identify turns within this result, not stable people across separate jobs. Use channel instead for stereo recordings.
Tri-state inverse-text normalization for English final transcripts (never interim partials). true renders spoken numbers as digits (phones, currency, dates, ordinals). false keeps those spans in spoken form. Omitted keeps the live engine default: number formatting is ON for finals. Independent of smart_format.
Opt-in English punctuation and sentence capitalization on final transcripts only. Interim partials are never formatted. May change only case and punctuation; any failure returns the unformatted final. Default false. Independent of numerals. Non-English requests are unchanged.
Opt-in spoken punctuation commands on English final transcripts only: period, comma, new paragraph, and question mark. Separate from smart_format and off by default. Interim partials are never rewritten.
Opt-in stripping of filled pauses (um, uh, umm, uhh, er) on English final transcripts. Off by default. Do not enable on legal or compliance audio by default. Interim partials are never rewritten.
Optional per-job terms for known names, brands, products, and other distinctive phrases. PyAI trims entries, removes case-insensitive duplicates, and keeps the first spelling and order. Entries shorter than 4 characters, longer than 64 characters, sentence-shaped input, non-string entries, and conservative common words are ignored. At most the first 5 valid terms are used. Invalid entries do not reject the job. When stored vocabulary is enabled for batch, request terms come first and stored suggestions fill any remaining slots. The effective list applies only to this job and does not select transcription language.
54 - 64json, srt, vtt HTTPS URL for transcription.job.completed or transcription.job.failed. PyAI POSTs {type, created, data} and signs the exact body in X-PyAI-Signature: t=<unix_seconds>,v1=<hex>, where v1 is HMAC-SHA256 over <t>.<rawBody>.
Trace compliance add-on: deterministic PII scan + redaction over the final transcript (SSN, card numbers, CVV-in-context, email, US phone — the pii_v0 entity set). The result carries the redacted transcript plus a trace summary (verdict, PII count). Patterns run on the formatted transcript, so pair with the default numerals (digits) for full effect. Requires the org's Trace entitlement on this surface (else 402); bills one Trace call. Not supported together with diarize/channel.
Optional Trace rule pack (only used when trace is true). entities (or redact) narrows the scan to a subset of: ssn, credit_card, cvv, email, us_phone; unknown names are ignored and an empty selection means the full set.
Optional stable call identifier for Recap; defaults to the transcription job id.
Optional Recap pack id.
^[a-z0-9_]+$Optional Recap call direction.
inbound, outbound Optional Recap customer label.
Optional Recap summarization language. It does not affect transcription: async jobs transcribe all eight Hear languages (en/es/fr/de/hi/it/pt/nl) and the spoken language is auto-detected per call.
en, fr, es, de, hi Optional CRM metadata delivered durably with the Recap trigger.
Response
Job accepted
"job_aZ09..."
queued, running, completed, failed, cancelled Unix ms.
Unix ms.
Present on completed jobs (inline). Large results are offloaded to result_url instead.
Show child attributes
Show child attributes
{
"text": "[speaker_1] Hello everyone.",
"audio_seconds": 120,
"speakers": 1,
"words": [
{
"word": "Hello",
"start": 0.42,
"end": 0.81,
"confidence": 0.98,
"speaker": "speaker_1",
"channel": 0
}
],
"segments": [
{
"id": 0,
"text": "Hello everyone.",
"start": 0.42,
"end": 1.8,
"speaker": "speaker_1",
"channel": 0
}
]
}
Signed GET URL for an offloaded large result.
Human-readable normalized failure message on failed jobs. This field is not a stable machine-readable failure code.