Natiq natiq.studio
Developers Reference Keys

API reference

A REST API for Algerian Darija text-to-speech. Send text, get a WAV back. JSON in, JSON out, one synchronous call. There is no polling and no job queue to babysit.

Base URL
https://www.natiq.studio/api/v1
2 to 13 tokens

per clip, by text length

600 characters

maximum per request

60 req/min

default per key

Create an account

Authentication

Pass your key as a bearer token. Keys look like natiq_live_a1b2c3d4_... and are stored hashed. If you lose one, revoke it and issue another.

Authorization: Bearer natiq_live_a1b2c3d4_xxxxxxxxxxxxxxxxxxxx
Server-side only. This API has no CORS headers on purpose: a key in browser JavaScript is a key anyone can read and spend. Call it from your backend.

A test key behaves identically but is meant for your staging environment; a read-scoped key can list things but never spends tokens.

POST /speech

Synthesize one clip. Returns when the audio is ready.

ParameterTypeDescription
text requiredstringDarija text, up to 600 characters. Arabic and French may be mixed freely.
voice requiredstringA voice id from GET /voices.
Request
curl -X POST https://www.natiq.studio/api/v1/speech \ -H "Authorization: Bearer $NATIQ_KEY" \ -H "Content-Type: application/json" \ -d '{ "text": "واش راك خويا؟ المنتوج راه disponible.", "voice": "alg_female_yasmine" }'
Response 201
{ "id": "speech_1842", "object": "speech", "status": "success", "voice": "alg_female_yasmine", "audio_url": "https://.../gen_4_a1b2.wav", "duration_seconds": 4.312, "inference_seconds": 3.704, "tokens_used": 5, "tokens_remaining": 95, "created_at": "2026-09-14T13:22:07+00:00" }
GET /speech and /speech/{id}

List your clips (newest first) or fetch one. Filter with status; page with limit (max 100) and offset.

curl "https://www.natiq.studio/api/v1/speech?limit=5&status=success" \ -H "Authorization: Bearer $NATIQ_KEY" # => {"object": "list", "data": [...], "has_more": true}
GET /voices

Every active reference voice. Filter by gender or accent.

idNameGenderAccent
alg_female_yasmine ياسمين (Yasmine) female algerian_arabic
dz_fr_female_amira أميرة (Amira) female french_algerian
fr_female_celine سيلين (Céline) female french
dz_female_sofia صوفيا (Sofia) female algerian_arabic
alg_male_mahmoud محمود (Mahmoud) male algerian_arabic
alg_male_riad رياض (Riad) male french_algerian
GET /account

Your balance, this key's usage, and the pricing formula. Poll it to alert yourself before you run dry, and read the formula from here rather than hard-coding a price. See token usage.

{ "object": "account", "tokens_remaining": 1750, "pricing": { "base_tokens": 1, "characters_per_token": 50, ... }, "estimated_clips_remaining": { "characters": 200, "clips": 350 }, "key": { "prefix": "natiq_live_a1b2c3d4", "scope": "write", ... } }

How token usage is calculated

Clips are billed by length. Inference runs for as long as the text is, so a 600 character script costs more than a one line prompt.

The whole formula
tokens = 1 + ceil(characters / 50)

The base covers the fixed cost of a request. Characters are counted after leading and trailing whitespace is trimmed, which is the same text the engine receives. Every character counts, including spaces inside the text, Arabic letters, Latin letters and punctuation.

Text lengthTokens
40 characters 2
100 characters 3
200 characters (a typical clip) 5
400 characters 9
600 characters (the limit) 13

The cheapest possible request is 2 tokens and the most expensive is 13. Text longer than 600 characters is rejected with invalid_request, so you split it yourself and each part is billed on its own length.

CallCostWhy
POST /speech returning 201by lengthThe clip was produced and stored.
POST /speech returning 5020Synthesis failed. The debit is refunded in the same transaction that records the failure.
POST /speech returning 4xx0Rejected before the engine ran.
GET /speech, /voices, /account0Reads are free. They still count against the per-minute rate limit.

Worked example

A catalogue of 40 product descriptions averaging 220 characters each is 40 requests of 1 + ceil(220 / 50) tokens. One 1,500 character article splits into 3 requests of 500 characters, each costing 1 + ceil(500 / 50).

Splitting on sentence boundaries costs slightly more than one long request, because each part pays the base again. Splitting also gives you better control over pacing, so the trade is usually worth it.

Reading your usage

Every successful synthesis returns tokens_used and tokens_remaining, so you can log the real cost of each call. GET /account returns the balance and the formula itself, which is what you should price a batch against rather than hard-coding a number.

curl "https://www.natiq.studio/api/v1/account" -H "Authorization: Bearer $NATIQ_KEY" # { # "tokens_remaining": 1750, # "pricing": {"base_tokens": 1, "characters_per_token": 50, # "minimum_tokens": 2, "maximum_tokens": 13, "max_characters": 600}, # "estimated_clips_remaining": {"characters": 200, "clips": 350} # }

estimated_clips_remaining is a convenience, not a promise: it says how many clips of 200 characters the balance buys, and names that length so you can redo the sum for your own text.

The per-key totals on your keys page and the line-by-line ledger on your account page record the same numbers server-side, including refunds.

When you run out

Synthesis returns 402 insufficient_tokens and nothing is charged. The message states how many tokens that request needed, so you can decide whether to shorten the text or buy more. Balances do not expire and packs stack. New accounts start with 100 free tokens once the email address is confirmed.

How your text is read

The model was trained on Arabic script only, so digits and Latin letters are rewritten into words before synthesis. You do not have to do anything: send the text as you have it and the service spells it out.

You sendThe model reads
السعر 3750 دجالسعر ثلاثة آلاف وسبعمية وخمسين دينار
راه disponible في le stockراه ديسبونيبل في لو ستوك
خصم 50%خصم خمسين بالمية
0555123456صفر خمسة خمسة خمسة واحد زوج ثلاثة أربعة خمسة ستة

Every response carries both strings. text is what you sent, byte for byte. text_spoken is what the engine read. Compare them when a clip does not sound the way you expected: the answer is usually in the second one.

{ "text": "السعر 3750 دج", "text_spoken": "السعر ثلاثة آلاف وسبعمية وخمسين دينار", ... }
Billing follows text_spoken. Four characters of "3750" become twenty five characters of speech, and the engine runs on the twenty five. Price a batch on the spelled-out length, not the digits.

Rewriting is idempotent, so text you have already spelled out yourself passes through untouched. A text whose expansion runs past 1200 characters is rejected with invalid_request before anything is billed; split it and send the parts.

Numbers follow Algerian Darija, so 11 is حداش rather than أحد عشر. A long digit run starting with a zero is read one digit at a time, because a ten digit number is not a quantity anyone says out loud. French and English words are transliterated, using the spellings the training data uses where they exist.

Formats are not interpreted. 14:30 is read as two numbers, not as a clock time, because a colon could equally be a score or a ratio and guessing at that is not ours to do. Write out what you want said.

Errors

Every failure returns the same envelope, so you can branch on error.code.

{ "error": { "type": "billing_error", "code": "insufficient_tokens", "message": "Insufficient tokens. You have 2 tokens, but 5 are required.", "doc_url": "/api/docs/" } }
StatuscodeMeaning
400invalid_requestA parameter is missing or malformed. Check error.param.
401missing_credentialsNo key was sent.
401invalid_api_keyUnknown, revoked, or expired key.
402insufficient_tokensBuy more at pricing.
403insufficient_scopeRead-only key tried to synthesize.
403email_unverifiedConfirm the account email address before synthesizing.
404not_foundNo such voice or speech object.
429rate_limit_exceededBack off; see Retry-After.
502engine_errorSynthesis failed. Your tokens were refunded automatically.

A failed generation never costs you anything. The tokens are refunded in the same transaction that records the failure.

Rate limits

60 requests per minute per key by default. Every response carries X-RateLimit-Limit and X-RateLimit-Remaining; a 429 adds Retry-After. Need more? Ask and we will raise it on your key.

Examples

Python
import os, requests resp = requests.post( "https://www.natiq.studio/api/v1/speech", headers={"Authorization": f"Bearer {os.environ['NATIQ_KEY']}"}, json={ "text": "مرحبا بيكم في المتجر تاعنا", "voice": "alg_female_yasmine", }, timeout=120, ) resp.raise_for_status() clip = resp.json() audio = requests.get(clip["audio_url"], timeout=60).content open("out.wav", "wb").write(audio)
Node.js
const res = await fetch("https://www.natiq.studio/api/v1/speech", { method: "POST", headers: { Authorization: `Bearer ${process.env.NATIQ_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ text: "واش راك خويا؟", voice: "alg_female_yasmine", }), }); if (!res.ok) { const { error } = await res.json(); throw new Error(`${error.code}: ${error.message}`); } const clip = await res.json();
PHP
$ch = curl_init("https://www.natiq.studio/api/v1/speech"); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("NATIQ_KEY"), "Content-Type: application/json", ], CURLOPT_POSTFIELDS => json_encode([ "text" => "مرحبا بيكم", "voice" => "alg_female_yasmine", ], JSON_UNESCAPED_UNICODE), ]); $clip = json_decode(curl_exec($ch), true);