curl --request POST \
--url https://api.usepatchwork.co/v1/voice/speak \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"text": "<string>"
}
'const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({text: '<string>'})
};
fetch('https://api.usepatchwork.co/v1/voice/speak', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://api.usepatchwork.co/v1/voice/speak"
payload = { "text": "<string>" }
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)"<string>"{
"status": "error",
"error": {
"message": "<string>",
"code": "<string>",
"details": "<unknown>"
}
}{
"status": "error",
"error": {
"message": "<string>",
"code": "<string>",
"details": "<unknown>"
}
}{
"status": "error",
"error": {
"message": "<string>",
"code": "<string>",
"details": "<unknown>"
}
}{
"status": "error",
"error": {
"message": "<string>",
"code": "<string>",
"details": "<unknown>"
}
}Speak text
Text in, audio out. Returns raw audio bytes, not the JSON envelope — the one endpoint in the platform that does. format says what the text is, not what comes back: markdown is rendered into something worth hearing before synthesis, plain is spoken as given.
curl --request POST \
--url https://api.usepatchwork.co/v1/voice/speak \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"text": "<string>"
}
'const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({text: '<string>'})
};
fetch('https://api.usepatchwork.co/v1/voice/speak', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://api.usepatchwork.co/v1/voice/speak"
payload = { "text": "<string>" }
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)"<string>"{
"status": "error",
"error": {
"message": "<string>",
"code": "<string>",
"details": "<unknown>"
}
}{
"status": "error",
"error": {
"message": "<string>",
"code": "<string>",
"details": "<unknown>"
}
}{
"status": "error",
"error": {
"message": "<string>",
"code": "<string>",
"details": "<unknown>"
}
}{
"status": "error",
"error": {
"message": "<string>",
"code": "<string>",
"details": "<unknown>"
}
}Authorizations
Identify accepts a workspace API key (sk_…). Loom accepts either a short-lived session token (direct) or a workspace API key (relay).
Headers
Makes this POST safe to retry. The first call does the work and its response is stored; a retry with the same key returns that stored response with Idempotency-Replayed: true, without charging again. Same key with a different body is rejected. Keys expire after 24 hours.
255Relay only. Opaque subject id, chosen by you and never resolved by Patchwork. Partitions threads and memory within your workspace. Direct tokens carry sub instead — do not send this header on a session token.
Required on every subject-scoped resource: threads, messages, runs, outcomes, confirmations and realtime. Omitting it is subject_required, never a wider result. The value is used exactly as given — never trimmed or normalised — so a stray space creates a second partition and is refused as invalid_subject.
Connection id or name the agent may call. Required in relay when the agent has unpinned customer tools. Direct tokens may carry conn instead.
Body
Response
Audio bytes. Content-Type is whatever the provider produced.
The response is of type file.