Jobs and callbacks
https://api.suisse-speech.ch/v1 · X-API-KeyBatch and realtime both hold a connection for the length of the work. That is right for a sentence and wrong for a chapter: the caller waits, every proxy in between gets a chance to time the request out, and a dropped socket throws away work that was nearly finished. Submit a job instead.
Synthesis jobs
# submit: answers in milliseconds
curl -X POST https://api.suisse-speech.ch/v1/jobs \
-H "X-API-Key: $SUISSE_SPEECH_KEY" -H 'Content-Type: application/json' \
-d '{"kind":"tts","text":"…long text…","voice":"lea","language":"de-CH","format":"mp3"}'
# → 202 { "id": "job_…", "status": "queued" } Location: /api/v1/jobs/job_…
curl -H "X-API-Key: $SUISSE_SPEECH_KEY" https://api.suisse-speech.ch/v1/jobs/job_…
# → { "status": "succeeded", "audio_seconds": 8.9,
# "audio": { "url": "/api/v1/jobs/job_…/audio", "byte_length": 106580 } }
curl -H "X-API-Key: $SUISSE_SPEECH_KEY" https://api.suisse-speech.ch/v1/jobs/job_…/audio --output out.mp3
Links in responses start with /api/v1; on api.suisse-speech.ch that prefix
works exactly like /v1.
status is queued, running, succeeded or failed. Poll the job URL; a
few seconds between polls is plenty and the rate limit is generous enough not to
be the thing you have to think about.
Everything is validated when you submit, not when the job runs: an unknown
voice or malformed markup comes back as a 400 while you are still listening,
not an hour later in a status field.
Results are kept for 48 hours, then the audio is deleted and the job row remains as history. Up to 200 jobs may be queued at once per account.
The audio of a succeeded synthesis job carries the same headers as POST /tts
(X-Suisse-Voice, X-Suisse-Audio-Duration, …), and the job reports the
offsets of any <mark> elements under marks, as POST /tts does.
Jobs are metered exactly like a synchronous request, and they queue behind interactive traffic on purpose: a large batch will never delay a realtime session.
Recognition jobs
Long recordings go the same way, as a multipart upload:
curl -X POST https://api.suisse-speech.ch/v1/jobs \
-H "X-API-Key: $SUISSE_SPEECH_KEY" \
-F kind=stt -F audio=@interview.wav -F lang=de-CH
# → 202 { "id": "job_…", "status": "queued" }
curl -H "X-API-Key: $SUISSE_SPEECH_KEY" https://api.suisse-speech.ch/v1/jobs/job_…
# → { "status": "succeeded", "audio_seconds": 3612.4,
# "transcript": { "text": "…", "words": [ … ] } }
This runs on a batch-optimised path measured at roughly a third of real time
(an hour of audio in about twenty minutes) rather than the path used by
POST /stt, which runs at close to the length of the recording. Use
POST /stt for short clips where a single round trip is simpler, and a job for
anything long.
Uploads up to 200 MB. The recording is deleted once the job finishes, on the failure path as well as on success.
A job takes what the synchronous endpoint takes: a synthesis job the fields of
POST /tts, model included; a recognition job lang and vocabulary (a JSON
array or a comma-separated list) like POST /stt, checked at submission the
same way (a lang the catalogue does not know is a 400 while you are still
listening), and it answers with the same stt.* error codes. An oversized
recording is 413 stt.audio_too_large, one shorter than 0.1 s fails as
stt.invalid_audio. GET /jobs/{id} carries the transcript in the shape
POST /stt answers with; the list GET /jobs carries has_transcript instead.
Callbacks
Rather than polling, give the job a callback_url and we will POST to it when
the job reaches a terminal state:
{ "kind": "tts", "text": "…", "callback_url": "https://your.example/hooks/speech" }
{ "event": "job.succeeded",
"job": { "id": "job_…", "status": "succeeded", "audio_seconds": 8.9, "audio": { … } },
"sent_at": "2026-09-03T21:40:11.204Z" }
Verify the signature. Every delivery carries:
X-Suisse-Webhook-Timestamp: 1788470411
X-Suisse-Webhook-Signature: sha256=<hex>
where the signature is HMAC_SHA256(secret, "<timestamp>.<raw body>"). Compare
in constant time against the raw body, before parsing it, and reject a
timestamp more than a few minutes old: the timestamp is inside the signed
material precisely so a captured delivery cannot be replayed at you later. Ask
us for your signing secret.
Requirements on the URL, all checked when you submit rather than when we
deliver, so a mistake is a 400 you see immediately:
httpsonly;- it must resolve to a public address: loopback, private, link-local, carrier-grade NAT and cloud metadata ranges are refused;
- no credentials in the URL;
- redirects are not followed.
Four attempts with exponential backoff, ten-second timeout. A 4xx that is not
408 or 429 is treated as final and not retried.
A callback is a courtesy, never the source of truth. If every delivery
fails, the job is still complete and still collectable from the job endpoint,
which is what your reconciliation should read. GET /jobs/{id} reports the
delivery state under callback.