> For the full page index, fetch https://echonova.ai/llms.txt

# FAQ: API

The questions developers ask before they open the Developer API pages, from keys and base URLs to jobs, webhooks, rate limits, credits and errors.

## How do I get an API key?

Open **Developers** in the portal, select the **API Keys** tab and select
**Create Key**. The workspace needs an active or trial subscription. The secret starts with `ek_` and is shown once, so use
**Copy** or **Download JSON** before you close the dialog; afterwards the
portal displays only its first 8 characters, and a lost secret means
deleting the key and creating a new one. See
[Get an API key](/docs/developers/get-an-api-key#step-2-create-the-key).

## How do I send the key with a request?

In the `Authorization` header with the `Bearer` scheme, as
`Authorization: Bearer YOUR_API_KEY`. The header is read strictly:
`Bearer` is case-sensitive, the token must start with `ek_`, and a key
sent as a query parameter, a cookie or another header is ignored, so the
request gets `401`. See
[Authentication](/docs/developers/authentication#header).

## Which base URL do I call?

Every request goes to one host, and the callout at the top of every
developer page shows it. A key works only for the workspace that owns it,
and keys, jobs, webhooks and request logs never cross workspaces. See
[Base URL](/docs/developers/base-url).

## Why do I get 401 Unauthorized?

Every `401` carries the same message, so work through the five causes in
order: the header is missing or is not `Bearer` followed by an `ek_` token,
no key matches the token because it was mistyped or is unknown, the key was
deleted, the key is disabled, or the account that
owns the key is closed. Compare the first
8 characters with the list under **API Keys** and check the key's
**Status**. See [Authentication](/docs/developers/authentication#errors).

## Which endpoints does the API have?

Fifteen, all under `/v1`, which is the only version. Text to Speech has
generate synchronously, start a job, list voices, list jobs, get a job and
delete a job; Speech to Text has transcribe synchronously, start a job, list
jobs, get a job and delete a job; Sound Cleaning has clean audio, list jobs,
get a job and delete a job. Every response is one JSON object with
`success`, `statusCode`, `errorMessage` and `data`, and a successful call
returns HTTP `200` even for `POST`. Any key in the workspace with access to
a product can list, get and delete that product's API jobs, whichever key
created them. Assets and agents are in the portal. See
[Developer API overview](/docs/developers/overview#endpoints).

## How do I generate speech?

Call `GET /v1/text-to-speech/voices`, keep the `id` of the voice you want,
then send `text` and `voiceId` to `POST /v1/text-to-speech/sync` for the
audio link in the same response, or to `POST /v1/text-to-speech` for a job
id. `speed`, `cfg`, `numSteps` and `tashkeel` are optional, `text` takes
1 to 500 characters on both endpoints, and `audioUrl` is a signed link to
a WAV file that expires, so download the file when it arrives. See
[Text to Speech quickstart](/docs/developers/tts-quickstart#step-2-generate-speech-synchronously).

## How do I transcribe audio?

Send the file as the `file` part of a `multipart/form-data` request to
`POST /v1/speech-to-text/sync` or to
`POST /v1/speech-to-text`, for audio of up to 10 minutes; both take 150 MB at most, and
the part's `Content-Type` must start with `audio/` and is set by you,
because the file extension is not read. The transcript is one
string with its `durationSeconds`. Send `identify_speakers=true` to also get
the transcript as timed segments, each with its speaker, and download any
finished job as TXT, SRT subtitles or JSON from
`GET /v1/speech-to-text/jobs/{jobId}/export`. There are no word timestamps.
An optional `language` field takes `auto`, `ar` or `en`. Auto lets the
service decide, which is Arabic today and also handles mixed and clean
English speech. Use `ar` for Arabic and mixed Arabic/English. Use `en` only
for wholly English speech, because Arabic sent as `en` comes back garbled.
See
[Speech to Text quickstart](/docs/developers/stt-quickstart#step-1-transcribe-a-short-file-synchronously).

## Should I use the synchronous or the asynchronous endpoint?

Synchronous when a person is waiting for the result in the same request
and the input is inside the synchronous limit; asynchronous for batches,
clients with a short HTTP timeout, or when
you want a webhook to tell you the job ended. Both cost the same credits
for the same input, and both Text to Speech endpoints share the
500-character limit, so text length never forces the choice. See
[Synchronous and asynchronous calls](/docs/developers/sync-vs-async#choosing-a-style).

## How do I know when an asynchronous job is done?

Poll `GET /v1/text-to-speech/jobs/{jobId}` or
`GET /v1/speech-to-text/jobs/{jobId}` every few seconds until `status` is
`completed` or `failed`, or register a webhook and receive `job.completed`
with the result inside it. A job that stays `processing` for more than
10 minutes is marked `failed` within 15 minutes of the `POST` with its
credits refunded, and that timeout sends no webhook, so poll the job once
if no event has arrived after 15 minutes. See
[Synchronous and asynchronous calls](/docs/developers/sync-vs-async#job-lifecycle).

## How do I set up a webhook?

Open **Developers**, select the **Webhooks** tab and select
**Create Webhook**, giving it a name, an endpoint URL (HTTPS is recommended) and at least
one of **Job completed**, **Job failed**, **Spending warning** and
**Spending exhausted**. The signing secret starts with `whsec_` and is
shown once, in the add-endpoint dialog right after you select **Create**,
with a **Copy** button. Copy it and store it. It cannot be shown again;
afterwards only its first 12 characters are shown.
See
[Configure a webhook](/docs/developers/webhooks-configure#step-2-create-the-webhook).

## How do I verify a webhook signature?

Take the raw request body as bytes, join `X-Webhook-Timestamp`, a dot and
the body, compute HMAC-SHA256 over that string keyed with the full `whsec_`
secret, and compare the lowercase hex result with `X-Webhook-Signature` in
constant time. Reject a timestamp more than 300 seconds from your clock,
the recommended window, answer `2xx` within 10 seconds, and only then
process the event. See
[Verify a webhook signature](/docs/developers/webhooks-verify-signature).

## Why did my endpoint get the same event twice, or the webhook turn itself off?

A delivery gets 3 attempts, 5 and then 10 seconds apart, and any response
other than a `2xx` within 10 seconds counts as a failure, so a slow handler
is retried and sees the event again; de-duplicate on `jobId` plus the
`X-Webhook-Event` header, answer first and process after. When 5
deliveries in a row fail after all their attempts, the webhook is disabled
and the portal shows **Disabled**; fix the endpoint and enable it again.
Events that fired while it was off are not replayed. See
[Delivery, retries and failures](/docs/developers/webhooks-retries#retry-schedule).

## What are the rate limits?

100 requests per 60 seconds, counted for one API key on one endpoint; two
keys have separate counts, and one key's calls to one endpoint do not
count against its calls to another. Every poll counts, and once a key
exceeds the limit that endpoint rejects it for 60 seconds with `429`,
`ThrottlerException: Too Many Requests` and a `Retry-After` header, so
wait that many seconds before you retry. See
[Rate limits](/docs/developers/rate-limits#limit).

## How much do API calls cost?

Text to Speech costs 1 credit per character of `text`, spaces included, and Speech to Text 100 credits per minute of audio, charged per second. Each charge is taken before
the work runs and refunded if the job fails, `creditsConsumed` on every
job is the number charged, a whole number of credits, and `GET` calls cost nothing. See
[Credits](/docs/developers/credits#rates).

## What does the Credit Limit on a key do?

It caps how many credits that one key may spend per billing cycle; it adds
no credits and does not change prices. A Text to Speech call that would
cross the limit is rejected with `429`, `Monthly spending limit exhausted`
and no `Retry-After` header, while a Speech to Text call always runs and
counts afterwards; a key with a limit also sends `spending.warning` once
per cycle at 80% and `spending.exhausted` on every call at or past the
limit. See [Credits](/docs/developers/credits#credit-limit-per-key).

## What does an error look like, and which ones can I retry?

An error is the same envelope with `success` set to `false`, `data` set to
`null` and the reason in `errorMessage`, and nothing is charged for a
request that ends in an error. Retry only the rate-limit `429` after
`Retry-After` seconds and the `503` and `504` responses, with backoff; a
`400`, `401`, `402`, `403` or `404` returns the same answer until you
change the request, the key or the balance, and never put a `POST` behind
an automatic retry, because every `POST` creates a new job and a new
charge. See [Errors](/docs/developers/errors#error-response-shape) and
[Retry requests safely](/docs/developers/retries-and-idempotency#step-3-read-the-error-response-before-you-retry).

## Where do I find code samples?

Every developer page shows its request in cURL, Python and Node, and
[Code samples](/docs/developers/code-samples) has two complete programs
per language: an asynchronous Text to Speech job from request to saved
audio file, and an asynchronous Speech to Text job from upload to
transcript. Each one runs with a single edit, your API key.

## Where do my API jobs show up in the portal?

On the **Request Log** tab under **Developers**, which lists the 50 most
recent requests made with a known key, any method and any status, with their
status, method, path, key name and time. Requests rejected with `401` (missing
or invalid key), the per-IP `429`, a per-key `429` after the first one of its
limit window, a body over 100 KB (`413`) and a body that is not valid JSON
(`400`) are not recorded. Jobs made with an API key are not on
the **Assets** page or in **History**; read them from
`GET /v1/text-to-speech/jobs`, `GET /v1/speech-to-text/jobs` and
`GET /v1/sound-cleaning/jobs`, which return the API jobs of the workspace, whichever key created them. See
[Read the request log](/docs/developers/request-logs#step-2-read-the-columns).

## Related

- [Developer API overview](/docs/developers/overview): the fifteen endpoints and the response shape
- [Get an API key](/docs/developers/get-an-api-key): create a key, save the secret and make the first request
- [Authentication](/docs/developers/authentication): the header, the checks and every 401 and 403
- [Base URL](/docs/developers/base-url): the host every request goes to
- [Text to Speech quickstart](/docs/developers/tts-quickstart): voices, one sentence to audio, and a polled job
- [Speech to Text quickstart](/docs/developers/stt-quickstart): one short file in one call, then a long file as a job
- [Synchronous and asynchronous calls](/docs/developers/sync-vs-async): choosing a style and the job lifecycle
- [Configure a webhook](/docs/developers/webhooks-configure): register an endpoint and keep its secret
- [Verify a webhook signature](/docs/developers/webhooks-verify-signature): the check your endpoint must run
- [Delivery, retries and failures](/docs/developers/webhooks-retries): attempts, duplicates and automatic disable
- [Rate limits](/docs/developers/rate-limits): the per-key limit, the headers and the 429 response
- [Credits](/docs/developers/credits): rates and the credit limit per key
- [Errors](/docs/developers/errors): every status and message, and how jobs report failure
- [Retry requests safely](/docs/developers/retries-and-idempotency): retry without paying twice
- [Code samples](/docs/developers/code-samples): complete programs in cURL, Python and Node
- [Read the request log](/docs/developers/request-logs): what the portal records about each request
- [Changelog](/docs/developers/changelog): what changed on the API and when
- [FAQ: billing](/docs/help/faq-billing): credits, top-ups and what happens at zero
- [Troubleshooting](/docs/help/troubleshooting): every API error in one table
