Why there are no webhooks yet
v1 has no callbacks. A webhook is a delivery guarantee — retries, signatures, replay protection, an endpoint we keep trying for hours — and shipping half of one is worse than shipping none, because an integration would trust it. Polling is boring and it works. Webhooks are the first thing on the list after v1.
Nor does a recording created through the API send the push notification or the “your transcript is ready” email the apps’ recordings do: an integration creates too many for either to be anything but noise in somebody’s inbox.
The four statuses
| Status | Meaning |
|---|---|
queued | Accepted. Waiting for a machine, or fetching your URL. |
processing | Being transcribed right now. |
completed | Finished. Segments and exports are available. |
failed | It will not finish. failure_reason says why. |
These four words are the whole vocabulary, on purpose: our pipeline has more states than this internally, and an integration that branched on those would break the day we split a job in two. completed and failed are terminal — nothing moves after them.
The progress field
GET /transcriptions/{id} carries a progress object. Use it to show a bar; do not use it to decide whether the work is done — status is the only thing that says that.
{
"id": "0f2f5f6a-2c6c-4f4e-9d1e-5d8f2a1b3c4d",
"status": "processing",
"duration_seconds": 3742,
"progress": { "step": "transcribe", "percent": 46, "indeterminate": false },
"failure_reason": null
}percentis 0–100 when we can measure it, andnullwhen we cannot.indeterminateistruefor the stretches with no measurable progress — show a spinner rather than a bar stuck at a number.stepis a coarse name for what is happening. Treat it as a label to display, not a value to branch on: the steps may change within v1.
The listing endpoint reports a lighter progress — the step only. Per-recording progress is a question about one recording, so ask it about one recording.
Recommended intervals
Transcription takes a fraction of the recording’s length, so a sensible schedule is:
- First check after 10 seconds. Nothing finishes faster than that, and polling immediately only costs you a request.
- Then every 5 seconds while the recording is short (under ten minutes).
- Then back off — double the interval up to a ceiling of 30 seconds — for anything longer.
- Give up after twice the recording’s length plus ten minutes and alert a human. A recording that has not moved by then is a support conversation, not a longer wait.
At one request every 5 seconds a single recording uses 12 requests a minute out of 120. That leaves room for 10 concurrent waits; if you run more than that, back off sooner. See rate limits.
A polling loop
Respect Retry-After if you ever meet a 429, and treat a 5xx as a reason to wait rather than to fail.
# A shell loop, for completeness -- in production this belongs in your application.
until [ "$STATUS" = "completed" ] || [ "$STATUS" = "failed" ]; do
sleep 5
STATUS=$(curl -s "https://heartotext.com/api/public/v1/transcriptions/$ID" \
-H "Authorization: Bearer $H2T_API_KEY" | jq -r .status)
doneWhen it fails
failure_reason | What happened, and what to do |
|---|---|
no_speech | We found nothing to transcribe. Check the file has audio. Retrying will not help. |
too_short | The recording is shorter than we can work with. |
quota_exceeded | The account had no allowance left when it came to be processed. Retry after the allowance resets. |
internal | Our fault. Retry once; if it fails again, send us the recording’s id. |
