Jobs and callbacks

Base URL https://api.suisse-speech.ch/v1 · X-API-Key

Batch 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 key.

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.

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:

  • https only;
  • 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.