{"openapi":"3.1.0","info":{"title":"Sawtak Arabi Public API","version":"v1"},"paths":{"/voices":{"get":{"summary":"List Voices","description":"List voices — EL `GET /v1/voices` shape, OpenAI list envelope (spec §5.2).","operationId":"list_voices_v1_voices_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"post":{"summary":"Clone Voice","description":"Clone a voice — EL `POST /v1/voices/add` shape: multipart, one reference clip,\nminimal 201 with the async `status` vocabulary (spec §5.3). The whole pipeline is\ngateway-native (`_run_clone`); there is no platform hop and no custom internal\nplatform API behind this route.\n\nMoney (owner decision, 2026-09-17): the charge is the limiter. Clones beyond the\nfree allowance (`FREE_VOICES_PER_ACCOUNT`, default 10) are pre-charged at\n`PRICE_USD_PER_VOICE_CLONE`; any pipeline refusal refunds in full. Admins clone\nfree; the hard backstop is the reserve-time cap (VOICES_PER_ACCOUNT, admins\nexempt).","operationId":"clone_voice_v1_voices_post","responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/voices/{voice_id}":{"get":{"summary":"Get Voice","description":"One voice. Own or public, indistinguishable from nonexistent otherwise (the\nresolver's read rule, spec §5.2).","operationId":"get_voice_v1_voices__voice_id__get","parameters":[{"name":"voice_id","in":"path","required":true,"schema":{"type":"string","title":"Voice Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"summary":"Update Voice","description":"Rename / relabel a voice — EL `PATCH /v1/voices/{id}`, owner only (spec §5.3).","operationId":"update_voice_v1_voices__voice_id__patch","parameters":[{"name":"voice_id","in":"path","required":true,"schema":{"type":"string","title":"Voice Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"summary":"Delete Voice","description":"Delete a voice — 204, owner only. In-use agents survive (their voice_id goes\nNULL, the platform FK behavior, §5.3). The stored originals go with the row.","operationId":"delete_voice_v1_voices__voice_id__delete","parameters":[{"name":"voice_id","in":"path","required":true,"schema":{"type":"string","title":"Voice Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/voices/{voice_id}/preview":{"get":{"summary":"Preview Voice","description":"Preview audio for the shelf / character picker (spec §5.2).\n\nPublic voices are keyless: a browser `<audio>` element cannot send a Bearer header,\nand marketing pages must be able to play these directly. Private voices require the\nkey and ownership. The bytes come from the platform's pre-rendered sample asset\n(the same one the dashboard serves), fetched over the internal hop.","operationId":"preview_voice_v1_voices__voice_id__preview_get","parameters":[{"name":"voice_id","in":"path","required":true,"schema":{"type":"string","title":"Voice Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/me":{"get":{"summary":"Get Me","operationId":"get_me_v1_me_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/usage":{"get":{"summary":"Get Usage","operationId":"get_usage_v1_usage_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/audio/speech":{"post":{"summary":"Speech","operationId":"speech_v1_audio_speech_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/audio/transcriptions":{"post":{"summary":"Transcriptions","description":"OpenAI-compatible transcription against the vLLM Cohere ASR pool (or ASR_BASE_URL).\n\n⚠️ The multipart body is parsed by hand rather than through `File(...)`/`Form(...)`\nparameters. FastAPI resolves those at ROUTE-REGISTRATION time, so without\n`python-multipart` installed the whole app refuses to construct — `/health`, `/metrics`\nand `/v1/audio/speech` with it. A new optional endpoint must not be able to take the\nrevenue path down; parsing here turns that into a 503 on this route alone.","operationId":"transcriptions_v1_audio_transcriptions_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/models":{"get":{"summary":"List Models","description":"OpenAI model list.\n\nSynthesis never needs this — but nearly every OpenAI-compatible client (and every\n\"add a provider\" UI) validates credentials by GETting it, and reports a 404 here as\n*\"invalid API key\"*, sending you hunting for a key problem that doesn't exist.","operationId":"list_models_v1_models_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/models/{model_id}":{"get":{"summary":"Retrieve Model","description":"OpenAI model retrieve — some clients probe this instead of the list.","operationId":"retrieve_model_v1_models__model_id__get","parameters":[{"name":"model_id","in":"path","required":true,"schema":{"type":"string","title":"Model Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}}},"components":{},"servers":[{"url":"https://api.sawtakarabi.ai/v1","description":"Production"}]}