Developers
Your schema in. People speak. Validated JSON out.
Define the data you need, send what people said, and get it back in that shape — without building speech recognition, language understanding and validation yourself. Your backend keeps the business logic.
The API runs today as a local service; a hosted endpoint is planned. Examples use the placeholder https://api.example.com.
curl -X POST https://api.example.com/api/v1/voice/parse \
-H "Authorization: Bearer $API_KEY" \
-F "audio=@voice.webm" \
-F "schema=restaurant_order"Your schema
18 field types, nested lists and objects, versioned. The response follows its shape.
Validated fields
Every value checked in code, with a status, a confidence and requiresConfirmation.
Sessions
Collect one record over several inputs; ask only for what is missing.
Any language, auto
English, Levantine and Gulf Arabic, and mixed speech — detected, never configured.
Authentication
Send your key in either header. Keys are checked when the server is configured with API_KEYS; local development runs without them.
Authorization: Bearer YOUR_API_KEYKeep keys server-side. Browser apps should call the Voice API through their own backend. Self-serve key management is on the roadmap — the dashboard shows the planned interface.
Schemas
A schema is the contract: the fields you want and their types. Save it with POST /api/v1/schemas (or in the builder), then name it in requests. Versions never change meaning.
{
"name": "restaurant_order",
"version": "1.0",
"description": "Restaurant delivery order",
"locale": "ar-JO",
"fields": {
"customerName": { "type": "person", "label": "Customer name", "required": true },
"phone": {
"type": "phone",
"required": true,
"validation": { "country": "JO" },
"normalization": { "format": "international" }
},
"items": {
"type": "array",
"required": true,
"items": {
"type": "object",
"fields": {
"name": { "type": "string", "required": true },
"quantity": { "type": "integer", "required": true, "min": 1, "default": 1 }
}
}
},
"address": { "type": "address", "required": true },
"notes": { "type": "text" }
}
}Request
POST /api/v1/voice/parse as multipart/form-data (or POST /api/v1/text/parse with JSON):
| Field | Required | Description |
|---|---|---|
audio | Yes | The recording: webm, ogg, opus, mp4 / m4a, aac, mp3, wav, flac, aiff |
schema | No | A name, name@version, or an inline definition. Default delivery_order. |
schemaVersion | No | Pins a version. |
locale, dialect | No | Optional overrides. Default auto: language and dialect are detected from the input. |
sessionId | No | Adds the input to a session. |
context | No | JSON: referenceTime (for relative dates), vocabulary, phone region, currency, required fields. |
Response
Every field comes back with a status, a confidence computed by the service, and requiresConfirmation. data holds just the values; missingFields and fieldsRequiringConfirmation tell your UI exactly what to ask.
Statuses: extracted, uncertain, invalid, rejected, missing, defaulted, confirmed.
{
"requestId": "req_3f9c2a1b7d10",
"schema": "restaurant_order",
"schemaVersion": "1.0",
"locale": "ar-JO",
"sessionId": null,
"transcription": {
"text": "بدي ٢ شاورما ووجبة زنجر والتوصيل لخلدا، اسمي محمد ورقمي 0791234567",
"language": "ar", "confidence": 0.9, "audioSeconds": 7.4
},
"fields": {
"customerName": { "value": "محمد", "type": "person", "status": "extracted", "confidence": 0.86, "requiresConfirmation": false },
"phone": { "value": "0791234567", "type": "phone", "status": "extracted", "confidence": 0.93, "requiresConfirmation": false },
"items": {
"value": [ { "name": "شاورما", "quantity": 2 }, { "name": "وجبة زنجر", "quantity": 1 } ],
"type": "array", "status": "extracted", "confidence": 0.86, "requiresConfirmation": false, "issues": []
},
"address": { "value": "خلدا", "type": "address", "status": "extracted", "confidence": 0.86, "requiresConfirmation": false },
"notes": { "value": null, "type": "text", "status": "missing", "confidence": 0, "requiresConfirmation": false }
},
"data": {
"customerName": "محمد", "phone": "0791234567",
"items": [ { "name": "شاورما", "quantity": 2 }, { "name": "وجبة زنجر", "quantity": 1 } ],
"address": "خلدا", "notes": null
},
"missingFields": [],
"fieldsRequiringConfirmation": [],
"complete": true,
"language": {
"requested": "auto", "detected": "ar", "locale": "ar-JO", "dialect": "jordanian", "dialectGroup": "levantine",
"direction": "rtl", "mixedLanguage": false, "secondaryLanguages": [], "source": "detected",
"evidence": ["بدي", "خلدا"]
},
"clarification": null,
"warnings": [],
"timings": { "uploadMs": 0.4, "transcriptionMs": 3120, "extractionMs": 3410, "validationMs": 1.1, "totalMs": 6533 },
"models": { "speechToText": "faster-whisper:large-v3-turbo", "extraction": "ollama:qwen2.5:7b" }
}A phone number heard with a digit missing is never filled — it comes back with exactly what was heard:
"phone": {
"value": null,
"type": "phone",
"status": "invalid",
"heard": "078824021",
"confidence": 0.3,
"requiresConfirmation": true,
"reason": "invalid_number_heard"
}Sessions
Start a session, send each answer with its sessionId, and the record is updated deterministically: new values replace old ones, silence never erases, and missingFields tells you what to ask next.
# 1. Start a session (pins appointment_booking@1.0)
curl -X POST https://api.example.com/api/v1/sessions -H 'Content-Type: application/json' \
-d '{"schema": "appointment_booking", "context": {"referenceTime": "2026-09-24T10:00:00+03:00"}}'
→ { "sessionId": "ses_Xy…", "missingFields": ["customerName", "phone", "date", "time"], … }
# 2. «بدي أحجز موعد مع الدكتور أحمد»
curl -X POST https://api.example.com/api/v1/voice/parse -F audio=@turn1.webm -F sessionId=ses_Xy…
→ data.doctor = "أحمد", missingFields = ["customerName", "phone", "date", "time"]
# 3. Your app asks «متى الموعد؟» — «بكرا الساعة خمسة المسا»
curl -X POST https://api.example.com/api/v1/voice/parse -F audio=@turn2.webm -F sessionId=ses_Xy…
→ data.doctor = "أحمد" (kept), data.date = "2026-09-25", data.time = "17:00",
changedFields = ["date", "time"], missingFields = ["customerName", "phone"]
# 4. The user confirms or corrects in your UI
curl -X POST https://api.example.com/api/v1/sessions/ses_Xy…/review -H 'Content-Type: application/json' \
-d '{"confirm": ["time"], "set": {"customerName": "سارة", "phone": "0781234567"}}'
→ { "complete": true, "data": { … } }Streaming
Add ?stream=true to receive NDJSON events as each stage completes. The last line is result or error.
curl -N -X POST "https://api.example.com/api/v1/voice/parse?stream=true" \
-H "Authorization: Bearer $API_KEY" -F "audio=@voice.webm" -F "schema=restaurant_order"
{"event": "received", "audioSeconds": 6.2}
{"event": "transcribing"}
{"event": "transcribed", "transcription": {"text": "…", "language": "ar", "confidence": 0.9}}
{"event": "extracting"}
{"event": "result", "result": { … }}Errors
One envelope for every error: {"error": {"code", "message", "details"}}.
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_context | `context` is not valid JSON, or names unknown fields. |
| 400 | unsupported_locale | An override (`locale` / `dialect`) the service does not know. Leave both out for auto. |
| 401 | unauthorized | Missing or wrong API key (when the server requires one). |
| 403 | read_only_schema | Built-in schemas cannot be overwritten or deleted. |
| 404 | unknown_schema / unknown_session | No such schema or version (details list what exists), or the session expired. |
| 409 | version_exists | That schema version is saved with different content. Save a new version. |
| 409 | session_conflict / session_schema_mismatch / session_turn_limit | Two inputs at once, a different schema than the session's, or too many inputs. |
| 413 | audio_too_large / audio_too_long / text_too_long | Over the size or duration limit. |
| 415 | unsupported_media_type | Not an accepted audio type. |
| 422 | invalid_schema | The schema (saved or inline) is not valid. `details.errors` lists each problem with its path. |
| 422 | invalid_audio / no_speech / invalid_request / invalid_review | Unreadable audio, silence, a malformed request, or a review value of the wrong shape. |
| 429 | rate_limited | Too many requests this minute. |
| 503 | extraction_unavailable | The language model is unavailable. `details.transcription` still carries what was heard. |
| 504 | timeout | Processing exceeded the request timeout. |
Limits
Current defaults of the self-hosted service — all configurable. Hosted plan limits are listed on the pricing page.
| Limit | Default | Setting |
|---|---|---|
| Audio size | 25 MB | MAX_AUDIO_SIZE_MB |
| Audio duration | 120 s | MAX_AUDIO_SECONDS — probed before transcription |
| Text input | 4,000 characters | MAX_TEXT_CHARS |
| Request timeout | 180 s | REQUEST_TIMEOUT_SECONDS |
| Concurrency | 1 job at a time (queued) | MAX_CONCURRENT_JOBS — CPU speech recognition |
| Schema size | 60 fields, 3 levels deep | Checked when a schema is saved or sent inline |
| Sessions | 30 min idle, 20 inputs | SESSION_TTL_MINUTES, MAX_SESSION_TURNS — in memory |
| Rate limit | Off by default | RATE_LIMIT_PER_MINUTE per key or IP |
Languages & schemas
autoAuto (default)
Language (English, Arabic, mixed) and Arabic dialect detected per input. Nothing to configure.
en · en-US · en-GBEnglish
First-class. en-US / en-GB add US / UK phone formats and currency.
arArabic, dialect detected
Forces Arabic; the dialect is still detected.
ar-JO · ar-PS · ar-LB · ar-SYLevantine: Jordan, Palestine, Lebanon, Syria
Forces the dialect: its phone format is enforced, its currency reported.
ar-SA · ar-AE · ar-KW · ar-QA · ar-BH · ar-OMGulf: Saudi Arabia, UAE, Kuwait, Qatar, Bahrain, Oman
Same, for the Gulf.
ar-IQ · ar-EG · ar-YEExperimental: Iraq, Egypt, Yemen
Registered with phone rules and basic dialect notes; not benchmarked yet.
Endpoints
- POST/api/v1/voice/parseAudio → transcript → your schema, validated. ?stream=true for NDJSON progress. Optional sessionId.
- POST/api/v1/text/parseThe same extraction and validation from text. Optional sessionId.
- GET/api/v1/schemasAll schemas (latest version of each), built-in and saved.
- POST/api/v1/schemasSave a schema version. A saved version never changes (409 on conflicting content).
- POST/api/v1/schemas/validateCheck a schema without saving it; every error with its path.
- GET/api/v1/schemas/{name}One definition (latest, or ?version=).
- GET/api/v1/schemas/{name}/versionsThe versions of a schema.
- DELETE/api/v1/schemas/{name}Delete a saved schema (one ?version=, or all). Built-ins are read-only.
- GET/api/v1/field-typesThe field types and their options.
- POST/api/v1/sessionsStart a session; pins one schema version.
- GET/api/v1/sessions/{id}The session's current record and history.
- POST/api/v1/sessions/{id}/reviewConfirm, reject or set field values.
- DELETE/api/v1/sessions/{id}Forget a session, transcripts included.
- GET/api/v1/localesSupported locales.
- GET/healthLiveness.
- GET/readyReadiness — speech model loaded, language model reachable.
Ready to build?
The docs walk you from a first request to handling every field status in your UI.
