Skip to content

API reference

Every Sandchest endpoint — method, path, request, response and errors — for both the AssemblyAI-compatible surface and the native API.

Open .md

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 URLhttps://stt-api.sandchest.com
AuthAuthorization: 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.

AuthRequired
BodyRaw bytes, Content-Type: application/octet-stream
HeadersX-File-Name (optional) records the original filename
Limit100 MiB on the hosted service
shell
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
200 OK
{ "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.

StatusWhen
400{"error":"The uploaded audio file is empty."}
413{"error":"The uploaded audio file exceeds the configured size limit."}
429Rate limited. This endpoint sets a retry-after header in seconds.

POST /v2/transcript#

Create a transcript. Returns immediately with status: "queued".

AuthRequired
BodyJSON, at most 64 KB
HeadersIdempotency-Key (optional, ≤ 200 characters)
shell
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}'
200 OK
{
  "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.

StatusWhen
400Unknown option, invalid value, unsupported feature, or an over-long Idempotency-Key
402{"error":"Your credit balance is exhausted. Add credits to continue transcribing."}
409The 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."}
429Rate limited
503Billing, 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.

shell
curl -sS https://stt-api.sandchest.com/v2/transcript/tr_9k2mQvR7pKd3sTn1yBhL \
  -H "Authorization: $SANDCHEST_API_KEY"
200 OK
{
  "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"
}
StatusWhen
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.

ParameterTypeDefaultRules
limitinteger101–200
statusstringqueued, processing, completed or error
created_onstringyyyy-MM-dd, not in the future, at most 90 days ago
before_idstringTranscripts older than this id
after_idstringTranscripts newer than this id
throttled_onlystringDeprecated. Accepted and ignored.
shell
curl -sS "https://stt-api.sandchest.com/v2/transcript?limit=2&status=completed" \
  -H "Authorization: $SANDCHEST_API_KEY"
200 OK
{
  "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.

StatusWhen
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.

shell
curl -sS -X DELETE https://stt-api.sandchest.com/v2/transcript/tr_9k2mQvR7pKd3sTn1yBhL \
  -H "Authorization: $SANDCHEST_API_KEY"
200 OK
{
  "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.

StatusWhen
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".

shell
curl -sS https://stt-api.sandchest.com/v2/transcript/$ID/sentences \
  -H "Authorization: $SANDCHEST_API_KEY"
200 OK
{
  "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.

200 OK
{
  "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 }]
    }
  ]
}

Find words and short phrases with their timings.

ParameterTypeRules
wordsstringRequired. Comma-separated. Each phrase is at most five words.

Matching ignores case and punctuation: COUNTRY, country! and country are the same term.

shell
curl -sS "https://stt-api.sandchest.com/v2/transcript/$ID/word-search?words=deploy,nothing%20broke" \
  -H "Authorization: $SANDCHEST_API_KEY"
200 OK
{
  "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.

StatusWhen
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.

ParameterTypeDefaultRules
chars_per_captioninteger42Maximum characters per caption
shell
curl -sS "https://stt-api.sandchest.com/v2/transcript/$ID/srt?chars_per_caption=32" \
  -H "Authorization: $SANDCHEST_API_KEY"
200 OK — srt
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,
200 OK — vtt
WEBVTT

00:00.000 --> 00:01.860
Okay so, um, the deploy went
StatusWhen
400{"error":"'chars_per_caption' must be an integer value."}
400A single word is longer than chars_per_caption; the message names the value and asks you to raise it
400The 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.

shell
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.

404 Not Found
{ "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:

400 Bad Request
{ "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#

CodeMeaning
200Success
400Invalid request: unknown option, bad value, unsupported feature, or an unknown transcript id on the /v2 endpoints
401Missing, malformed, revoked or expired API key
402Out of credits
403Declared by the API contract; not currently returned by any endpoint
404Unknown transcript id on the native /api/v1 endpoints
409Idempotency-Key conflict
413Body too large: 100 MiB for uploads, 64 KB for transcript creation
429Rate limited — 120 requests per minute per workspace by default
503A 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#

shell
curl -sS https://sandchest.com/openapi.json | jq '.paths | keys'
expected output
[
  "/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:

walk-the-api.sh
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 -2

You 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:

expected output
status: completed
1
1

Next#

TypeScript SDK — the same API with types, cancellation and polling already handled.