API reference
Every Sandchest endpoint — method, path, request, response and errors — for both the AssemblyAI-compatible surface and the native API.
Ten endpoints, one header, JSON in and JSON out. This page documents each one with a
real request and a real response. A machine-readable OpenAPI 3.1 document, generated
from the same contract the server runs, is at /openapi.json.
| Base URL | https://stt-api.sandchest.com |
| Auth | Authorization: sc_live_... on every endpoint (Bearer prefix optional) |
| Errors | { "error": "..." } with a meaningful status code |
POST /v2/upload#
Store audio and get a URL you can pass as audio_url. The body is the raw file
bytes — not a multipart form.
| Auth | Required |
| Body | Raw bytes, Content-Type: application/octet-stream |
| Headers | X-File-Name (optional) records the original filename |
| Limit | 100 MiB on the hosted service |
curl -sS https://stt-api.sandchest.com/v2/upload \
-H "Authorization: $SANDCHEST_API_KEY" \
-H "Content-Type: application/octet-stream" \
-H "X-File-Name: meeting.mp3" \
--data-binary @meeting.mp3{ "upload_url": "https://stt-api.sandchest.com/api/v1/uploads/aud_7pKd3sTn1yBhLc8WgZaE" }Uploads are private to your workspace and expire after the deployment's retention
window — 24 hours on the hosted service. Pass the upload_url straight through as
audio_url; do not try to fetch it yourself.
| Status | When |
|---|---|
| 400 | {"error":"The uploaded audio file is empty."} |
| 413 | {"error":"The uploaded audio file exceeds the configured size limit."} |
| 429 | Rate limited. This endpoint sets a retry-after header in seconds. |
POST /v2/transcript#
Create a transcript. Returns immediately with status: "queued".
| Auth | Required |
| Body | JSON, at most 64 KB |
| Headers | Idempotency-Key (optional, ≤ 200 characters) |
curl -sS https://stt-api.sandchest.com/v2/transcript \
-H "Authorization: $SANDCHEST_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: import-meeting-141" \
-d '{"audio_url":"https://example.com/meeting.mp3","disfluencies":true}'{
"id": "tr_9k2mQvR7pKd3sTn1yBhL",
"status": "queued",
"audio_url": "https://example.com/meeting.mp3",
"text": null,
"words": null,
"utterances": null,
"multichannel": null,
"confidence": null,
"audio_duration": null,
"audio_start_from": null,
"audio_end_at": null,
"language_code": null,
"language_confidence": null,
"speech_model_used": null,
"speech_models": [],
"punctuate": true,
"format_text": true,
"disfluencies": true,
"custom_spelling": null,
"language_detection": true,
"speaker_labels": false,
"webhook_url": null,
"webhook_status_code": null,
"webhook_auth": false,
"webhook_auth_header_name": null,
"error": null,
"created": "2026-08-30T09:14:22.001Z"
}Every request field is documented in Transcript options.
| Status | When |
|---|---|
| 400 | Unknown option, invalid value, unsupported feature, or an over-long Idempotency-Key |
| 402 | {"error":"Your credit balance is exhausted. Add credits to continue transcribing."} |
| 409 | The Idempotency-Key was used for a different request, or belongs to a deleted transcript |
| 413 | {"error":"The transcription request exceeds the 64 KB size limit."} |
| 429 | Rate limited |
| 503 | Billing, inference or storage temporarily unavailable |
GET /v2/transcript/:id#
Read the current state. This returns immediately — it does not wait for the transcript to finish. Poll it.
curl -sS https://stt-api.sandchest.com/v2/transcript/tr_9k2mQvR7pKd3sTn1yBhL \
-H "Authorization: $SANDCHEST_API_KEY"{
"id": "tr_9k2mQvR7pKd3sTn1yBhL",
"status": "completed",
"audio_url": "https://example.com/meeting.mp3",
"text": "Okay so, um, the deploy went out at four and, uh, honestly, nothing broke.",
"words": [
{ "text": "Okay", "start": 0, "end": 280, "confidence": 0.99, "speaker": null, "channel": null },
{ "text": "so,", "start": 300, "end": 520, "confidence": 0.98, "speaker": null, "channel": null }
],
"utterances": null,
"confidence": 0.98,
"audio_duration": 4.8,
"language_code": "en",
"language_confidence": 0.99,
"speech_model_used": "nvidia/parakeet-tdt-0.6b-v2",
"speech_models": [],
"punctuate": true,
"format_text": true,
"disfluencies": true,
"custom_spelling": null,
"language_detection": true,
"speaker_labels": false,
"error": null,
"created": "2026-08-30T09:14:22.001Z"
}| Status | When |
|---|---|
| 400 | {"error":"Transcript lookup error, transcript id not found"} — matches AssemblyAI, which uses 400 rather than 404 here. A transcript belonging to another workspace is indistinguishable from one that never existed. |
GET /v2/transcript#
List your workspace's transcripts, newest first.
Every list request is floored at 90 days. Transcripts created more than 90 days
ago are never listed, whatever parameters you send — the floor is not only a
created_on rule. It applies to before_id and after_id paging too, so
walking back through prev_url stops silently at the boundary: you get a page
with "result_count": 0 and no error. Older transcripts are still readable by
id with GET /v2/transcript/:id, so keep the ids you care about.
| Parameter | Type | Default | Rules |
|---|---|---|---|
limit | integer | 10 | 1–200 |
status | string | — | queued, processing, completed or error |
created_on | string | — | yyyy-MM-dd, not in the future, at most 90 days ago |
before_id | string | — | Transcripts older than this id |
after_id | string | — | Transcripts newer than this id |
throttled_only | string | — | Deprecated. Accepted and ignored. |
curl -sS "https://stt-api.sandchest.com/v2/transcript?limit=2&status=completed" \
-H "Authorization: $SANDCHEST_API_KEY"{
"page_details": {
"limit": 2,
"result_count": 2,
"current_url": "https://stt-api.sandchest.com/v2/transcript?limit=2&status=completed",
"prev_url": "https://stt-api.sandchest.com/v2/transcript?limit=2&status=completed&before_id=tr_3sTn1yBhLc8WgZaE0jUo",
"next_url": "https://stt-api.sandchest.com/v2/transcript?limit=2&status=completed&after_id=tr_9k2mQvR7pKd3sTn1yBhL"
},
"transcripts": [
{
"id": "tr_9k2mQvR7pKd3sTn1yBhL",
"resource_url": "https://stt-api.sandchest.com/v2/transcript/tr_9k2mQvR7pKd3sTn1yBhL",
"status": "completed",
"created": "2026-08-30T09:14:22.001Z",
"completed": "2026-08-30T09:14:29.412Z",
"audio_url": "https://example.com/meeting.mp3",
"error": null
}
]
}Page with the absolute URLs the response gives you. prev_url walks into older
transcripts, next_url back toward newer ones; both are null when the page is
empty.
| Status | When |
|---|---|
| 400 | {"error":"'limit' must be an integer"} |
| 400 | {"error":"'limit' must be greater than or equal to 1"} |
| 400 | {"error":"'limit' must be less than or equal to 200"} |
| 400 | {"error":"'status' must be one of ['queued', 'processing', 'completed', 'error']."} |
| 400 | {"error":"'created_on' is not a valid date. Use 'yyyy-MM-dd'."} |
| 400 | {"error":"'created_on' cannot be more than 90 days in the past"} |
| 400 | {"error":"'created_on' cannot be a future date"} |
| 400 | {"error":"'before_id' is not a valid transcript id"} |
DELETE /v2/transcript/:id#
Erase a transcript's content. Words, text, source URL, custom vocabulary, detected language and private error detail are all removed. Usage and billing history survive, and so does the id.
curl -sS -X DELETE https://stt-api.sandchest.com/v2/transcript/tr_9k2mQvR7pKd3sTn1yBhL \
-H "Authorization: $SANDCHEST_API_KEY"{
"id": "tr_9k2mQvR7pKd3sTn1yBhL",
"status": "completed",
"audio_url": "http://deleted_by_user",
"text": "Deleted by user.",
"words": null,
"confidence": null,
"audio_duration": 4.8,
"language_code": null,
"language_confidence": null,
"speech_model_used": null,
"speech_models": null,
"punctuate": false,
"format_text": false,
"disfluencies": false,
"language_detection": false,
"speaker_labels": false,
"error": null,
"created": "2026-08-30T09:14:22.001Z"
}The uploaded audio goes too: an upload used by no other live transcript is deleted
from object storage immediately. An Idempotency-Key that pointed at a deleted
transcript stays reserved and returns 409 forever — it never creates a fresh job.
| Status | When |
|---|---|
| 400 | {"error":"Transcript lookup error, transcript id not found"} |
| 503 | {"error":"Transcript deletion is temporarily unavailable. Please retry."} |
GET /v2/transcript/:id/sentences#
Sentence-level spans, built from the same word boundaries as the transcript.
Requires status: "completed".
curl -sS https://stt-api.sandchest.com/v2/transcript/$ID/sentences \
-H "Authorization: $SANDCHEST_API_KEY"{
"id": "tr_9k2mQvR7pKd3sTn1yBhL",
"confidence": 0.98,
"audio_duration": 4.8,
"speech_model_used": "nvidia/parakeet-tdt-0.6b-v2",
"sentences": [
{
"text": "Okay so, um, the deploy went out at four and, uh, honestly, nothing broke.",
"start": 0,
"end": 4520,
"confidence": 0.99,
"words": [{ "text": "Okay", "start": 0, "end": 280, "confidence": 0.99 }]
}
]
}Sentence boundaries come from ICU segmentation in the transcript's own language, so
punctuation inside URLs and decimals does not split a sentence. With
punctuate: false and no custom spelling there is nothing to segment on, so the
whole transcript comes back as one sentence.
GET /v2/transcript/:id/paragraphs#
The same shape with a paragraphs array instead. A paragraph ends after five
sentences, after a pause longer than two seconds, or when the channel changes.
{
"id": "tr_9k2mQvR7pKd3sTn1yBhL",
"confidence": 0.98,
"audio_duration": 4.8,
"speech_model_used": "nvidia/parakeet-tdt-0.6b-v2",
"paragraphs": [
{
"text": "Okay so, um, the deploy went out at four and, uh, honestly, nothing broke.",
"start": 0,
"end": 4520,
"confidence": 0.99,
"words": [{ "text": "Okay", "start": 0, "end": 280, "confidence": 0.99 }]
}
]
}GET /v2/transcript/:id/word-search#
Find words and short phrases with their timings.
| Parameter | Type | Rules |
|---|---|---|
words | string | Required. Comma-separated. Each phrase is at most five words. |
Matching ignores case and punctuation: COUNTRY, country! and country are the
same term.
curl -sS "https://stt-api.sandchest.com/v2/transcript/$ID/word-search?words=deploy,nothing%20broke" \
-H "Authorization: $SANDCHEST_API_KEY"{
"id": "tr_9k2mQvR7pKd3sTn1yBhL",
"total_count": 2,
"matches": [
{ "text": "deploy", "count": 1, "timestamps": [[1100, 1480]], "indexes": [4] },
{ "text": "nothing broke", "count": 1, "timestamps": [[3700, 4520]], "indexes": [12] }
]
}indexes are positions in the transcript's words array. timestamps are
[start, end] pairs in milliseconds.
| Status | When |
|---|---|
| 400 | {"error":"`words` is a required query parameter"} |
| 400 | {"error":"`words` entries must not be empty"} |
| 400 | {"error":"Phrases are currently restricted to five words at most."} |
| 400 | {"error":"The transcript has been deleted."} |
GET /v2/transcript/:id/srt and /vtt#
Captions as text/plain, cut on real word boundaries.
| Parameter | Type | Default | Rules |
|---|---|---|---|
chars_per_caption | integer | 42 | Maximum characters per caption |
curl -sS "https://stt-api.sandchest.com/v2/transcript/$ID/srt?chars_per_caption=32" \
-H "Authorization: $SANDCHEST_API_KEY"1
00:00:00,000 --> 00:00:01,860
Okay so, um, the deploy went
2
00:00:01,880 --> 00:00:04,520
out at four and, uh, honestly,WEBVTT
00:00.000 --> 00:01.860
Okay so, um, the deploy went| Status | When |
|---|---|
| 400 | {"error":"'chars_per_caption' must be an integer value."} |
| 400 | A single word is longer than chars_per_caption; the message names the value and asks you to raise it |
| 400 | The transcript was deleted, so there is no text to caption |
Native endpoints#
The native API is the same engine with plainer semantics. @sandchest/sdk uses it.
POST /api/v1/transcripts#
Identical request body, identical response, identical errors to
POST /v2/transcript, including Idempotency-Key.
curl -sS https://stt-api.sandchest.com/api/v1/transcripts \
-H "Authorization: $SANDCHEST_API_KEY" \
-H "Content-Type: application/json" \
-d '{"audio_url":"https://example.com/meeting.mp3"}'GET /api/v1/transcripts/:id#
The same transcript object as GET /v2/transcript/:id, with one difference: an
unknown id is a proper 404 here.
{ "error": "Transcript not found." }Resource preconditions#
sentences, paragraphs, word-search, srt and vtt all require a completed
transcript. Asking earlier returns HTTP 400 naming the current status and the
resource you asked for:
{ "error": "This transcript has a status of 'processing'. Transcripts must have a status of 'completed' before requesting sentences." }The resource name in that message is one of sentences, paragraphs,
word search or captions.
Status codes#
| Code | Meaning |
|---|---|
| 200 | Success |
| 400 | Invalid request: unknown option, bad value, unsupported feature, or an unknown transcript id on the /v2 endpoints |
| 401 | Missing, malformed, revoked or expired API key |
| 402 | Out of credits |
| 403 | Declared by the API contract; not currently returned by any endpoint |
| 404 | Unknown transcript id on the native /api/v1 endpoints |
| 409 | Idempotency-Key conflict |
| 413 | Body too large: 100 MiB for uploads, 64 KB for transcript creation |
| 429 | Rate limited — 120 requests per minute per workspace by default |
| 503 | A dependency (inference, billing, storage, rate limiting) is temporarily unavailable |
Every error body is { "error": "..." }. See
Errors and limits for what to do about each one.
OpenAPI#
curl -sS https://sandchest.com/openapi.json | jq '.paths | keys'[
"/api/v1/transcripts",
"/api/v1/transcripts/{id}",
"/v2/transcript",
"/v2/transcript/{id}",
"/v2/transcript/{id}/paragraphs",
"/v2/transcript/{id}/sentences",
"/v2/transcript/{id}/srt",
"/v2/transcript/{id}/vtt",
"/v2/transcript/{id}/word-search",
"/v2/upload"
]Verify#
Walk the whole surface with one file:
API="https://stt-api.sandchest.com"
AUTH="Authorization: $SANDCHEST_API_KEY"
UPLOAD_URL=$(curl -sS "$API/v2/upload" -H "$AUTH" \
-H "Content-Type: application/octet-stream" --data-binary @meeting.mp3 | jq -r .upload_url)
ID=$(curl -sS "$API/v2/transcript" -H "$AUTH" -H "Content-Type: application/json" \
-d "{\"audio_url\":\"$UPLOAD_URL\"}" | jq -r .id)
while true; do
STATUS=$(curl -sS "$API/v2/transcript/$ID" -H "$AUTH" | jq -r .status)
[ "$STATUS" = completed ] || [ "$STATUS" = error ] && break
sleep 2
done
echo "status: $STATUS"
curl -sS "$API/v2/transcript/$ID/sentences" -H "$AUTH" | jq '.sentences | length'
curl -sS "$API/v2/transcript/$ID/srt" -H "$AUTH" | head -2You are looking for status: completed, a sentence count of at least 1, and an
SRT block that starts with 1 and a 00:00:00,000 --> timing line:
status: completed
1
1Next#
TypeScript SDK — the same API with types, cancellation and polling already handled.