Skip to content · تخطَّ إلى المحتوى
نبرة Nabra

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_KEY

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

restaurant_order.json
{
  "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):

FieldRequiredDescription
audioYesThe recording: webm, ogg, opus, mp4 / m4a, aac, mp3, wav, flac, aiff
schemaNoA name, name@version, or an inline definition. Default delivery_order.
schemaVersionNoPins a version.
locale, dialectNoOptional overrides. Default auto: language and dialect are detected from the input.
sessionIdNoAdds the input to a session.
contextNoJSON: 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.

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

invalid field
"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.

sessions
# 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.

NDJSON
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"}}.

StatusCodeMeaning
400invalid_context`context` is not valid JSON, or names unknown fields.
400unsupported_localeAn override (`locale` / `dialect`) the service does not know. Leave both out for auto.
401unauthorizedMissing or wrong API key (when the server requires one).
403read_only_schemaBuilt-in schemas cannot be overwritten or deleted.
404unknown_schema / unknown_sessionNo such schema or version (details list what exists), or the session expired.
409version_existsThat schema version is saved with different content. Save a new version.
409session_conflict / session_schema_mismatch / session_turn_limitTwo inputs at once, a different schema than the session's, or too many inputs.
413audio_too_large / audio_too_long / text_too_longOver the size or duration limit.
415unsupported_media_typeNot an accepted audio type.
422invalid_schemaThe schema (saved or inline) is not valid. `details.errors` lists each problem with its path.
422invalid_audio / no_speech / invalid_request / invalid_reviewUnreadable audio, silence, a malformed request, or a review value of the wrong shape.
429rate_limitedToo many requests this minute.
503extraction_unavailableThe language model is unavailable. `details.transcription` still carries what was heard.
504timeoutProcessing exceeded the request timeout.

Limits

Current defaults of the self-hosted service — all configurable. Hosted plan limits are listed on the pricing page.

LimitDefaultSetting
Audio size25 MBMAX_AUDIO_SIZE_MB
Audio duration120 sMAX_AUDIO_SECONDS — probed before transcription
Text input4,000 charactersMAX_TEXT_CHARS
Request timeout180 sREQUEST_TIMEOUT_SECONDS
Concurrency1 job at a time (queued)MAX_CONCURRENT_JOBS — CPU speech recognition
Schema size60 fields, 3 levels deepChecked when a schema is saved or sent inline
Sessions30 min idle, 20 inputsSESSION_TTL_MINUTES, MAX_SESSION_TURNS — in memory
Rate limitOff by defaultRATE_LIMIT_PER_MINUTE per key or IP

Languages & schemas

auto

Auto (default)

Language (English, Arabic, mixed) and Arabic dialect detected per input. Nothing to configure.

en · en-US · en-GB

English

First-class. en-US / en-GB add US / UK phone formats and currency.

ar

Arabic, dialect detected

Forces Arabic; the dialect is still detected.

ar-JO · ar-PS · ar-LB · ar-SY

Levantine: 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-OM

Gulf: Saudi Arabia, UAE, Kuwait, Qatar, Bahrain, Oman

Same, for the Gulf.

ar-IQ · ar-EG · ar-YE

Experimental: Iraq, Egypt, Yemen

Registered with phone rules and basic dialect notes; not benchmarked yet.

Example schemas: delivery_orderrestaurant_orderappointment_bookingfield_service_requesthr_leave_request+ your own

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.