https://api.echonova.aiDevelopers
Webhook events
The four events EchoNova sends, the headers on every delivery, and the exact JSON body of each event.
This page lists every event a webhook can subscribe to and the request your endpoint receives for each. Use it to write the handler; use Verify a webhook signature to check that a request came from EchoNova before you act on it.
Event types
| Event | Portal label | Sent when |
|---|---|---|
job.completed |
Job completed | An asynchronous Text to Speech or Speech to Text job started with an API key finishes with a result. |
job.failed |
Job failed | The same kind of job fails. The credits it reserved are refunded before the event is sent. |
spending.warning |
Spending warning | A metered call takes an API key that has a Credit Limit to 80% or more of that limit. Sent once per billing cycle. |
spending.exhausted |
Spending exhausted | A metered call leaves an API key that has a Credit Limit at or above that limit. Sent on every such call. |
Synchronous calls never produce job.completed or job.failed. Jobs started
from the portal are not announced either. Spending events are produced by
both synchronous and asynchronous calls, and only for keys with a
Credit Limit.
The request
Every delivery is one POST to your Endpoint URL with these headers:
| Header | Value |
|---|---|
Content-Type |
application/json |
X-Webhook-Event |
The event name, for example job.completed. |
X-Webhook-Timestamp |
Unix time in seconds when this attempt was signed, as a decimal string. |
X-Webhook-Signature |
HMAC-SHA256 of <timestamp>.<body> keyed with your signing secret, as 64 lowercase hex characters. |
The body is the event payload and nothing else. There is no envelope: no id,
no type and no createdAt field inside the JSON. The event name travels in
X-Webhook-Event only, so read that header before you parse the body.
A retry sends the same body with a new timestamp and a new signature. See Delivery, retries and failures.
job.completed
Sent once the job's result is ready. The result travels in the event, so you do not need to call the API back for it, except for the speaker segments of a Speech to Text job. The key names match the job endpoints, so a poller and a webhook consumer read the same fields.
| Field | Type | Present | Meaning |
|---|---|---|---|
jobId |
string | Always | The id the POST that started the job returned. |
apiKeyId |
string | Always | The id of the API key that started the job. |
service |
string | Always | tts or stt. |
durationSeconds |
number or null |
Always | Length of the audio. null when it was not measured. |
text |
string | tts only |
The text that was synthesised, echoed back so you can match the callback to your request. |
audioUrl |
string | tts only |
A signed, time-limited link to the audio. Download it when the event arrives; do not store the URL. |
transcription |
string | stt only |
The transcript. |
A Speech to Text event carries transcription only. The segments and
speakers of a job made with identify_speakers=true are not in the event:
read them from GET /v1/speech-to-text/jobs/{jobId}, or download the job as
subtitles or JSON from GET /v1/speech-to-text/jobs/{jobId}/export.
Text to Speech example:
{
"jobId": "66e6b2f1c9a4d3e8f0a1b2c3",
"apiKeyId": "66e6b1a0c9a4d3e8f0a1b2b9",
"service": "tts",
"durationSeconds": 3.42,
"text": "مرحبا بكم في إيكونوڤا",
"audioUrl": "https://storage.example.com/tts/tts-1a2b3c.wav?X-Amz-Signature=..."
}Speech to Text example:
{
"jobId": "66e6b3d4c9a4d3e8f0a1b2c7",
"apiKeyId": "66e6b1a0c9a4d3e8f0a1b2b9",
"service": "stt",
"durationSeconds": 12.8,
"transcription": "مرحبا بكم في إيكونوڤا"
}job.failed
Sent after the job is marked failed and its credits are refunded.
| Field | Type | Present | Meaning |
|---|---|---|---|
jobId |
string | Always | The id the POST that started the job returned. |
apiKeyId |
string | Always | The id of the API key that started the job. |
service |
string | Always | tts or stt. |
error |
string or null |
Always | The reason the job failed. When none was given, AI server generation failed for tts or AI server transcription failed for stt. |
{
"jobId": "66e6b2f1c9a4d3e8f0a1b2c3",
"apiKeyId": "66e6b1a0c9a4d3e8f0a1b2b9",
"service": "tts",
"error": "AI server generation failed"
}spending.warning
Sent once per billing cycle, the first time a key's usage reaches 80% of its Credit Limit. It is sent again in the next cycle once the key's usage has reset, which happens on the key's first metered call of that cycle; see Credits.
| Field | Type | Meaning |
|---|---|---|
apiKeyId |
string | The id of the API key. |
apiKeyName |
string | The same value as apiKeyId. The key's display name does not travel in the event; key your handling on apiKeyId. |
creditsUsed |
number | A whole number of credits. Credits recorded against the key in this billing cycle as of the start of this request, including the call that triggered the event. |
creditLimit |
number | The key's Credit Limit, in credits. |
percentageUsed |
number | creditsUsed / creditLimit * 100, rounded to the nearest whole number. |
{
"apiKeyId": "66e6b1a0c9a4d3e8f0a1b2b9",
"apiKeyName": "66e6b1a0c9a4d3e8f0a1b2b9",
"creditsUsed": 820,
"creditLimit": 1000,
"percentageUsed": 82
}spending.exhausted
Sent each time a metered call leaves the key at or above its
Credit Limit. A call that would cross the limit is rejected with 429
and Monthly spending limit exhausted before it runs, so this event follows
the call that lands exactly on the limit.
| Field | Type | Meaning |
|---|---|---|
apiKeyId |
string | The id of the API key. |
apiKeyName |
string | The same value as apiKeyId. The key's display name does not travel in the event; key your handling on apiKeyId. |
creditsUsed |
number | A whole number of credits. Credits recorded against the key in this billing cycle as of the start of this request, including the call that triggered the event. |
creditLimit |
number | The key's Credit Limit, in credits. |
{
"apiKeyId": "66e6b1a0c9a4d3e8f0a1b2b9",
"apiKeyName": "66e6b1a0c9a4d3e8f0a1b2b9",
"creditsUsed": 1001,
"creditLimit": 1000
}FAQ
How do I know which webhook or delivery a request belongs to?
The body carries no delivery id and no webhook id. Identify a job event by
jobId plus X-Webhook-Event, and a spending event by apiKeyId plus the
event name. De-duplication is on
Delivery, retries and failures.
How do I show the key's name from a spending event?
apiKeyName carries the key's id. Learn each key's apiKeyId from the
apiKeyId field of a job.completed or job.failed event for a job you
started with that key, then keep your own map from id to the system that
holds the key.
Does a retry change the body?
No. A retry sends the same body with a new X-Webhook-Timestamp and a new
X-Webhook-Signature.
Do synchronous calls send job events?
No. job.completed and job.failed are sent for asynchronous jobs started
with an API key. Spending events are sent for both call styles.
Is the audioUrl in the event permanent?
No. It is a signed, time-limited link. Download the file when the event
arrives; GET /v1/text-to-speech/jobs/{jobId} returns a fresh link if it
has expired.