Base URL https://api.echonova.ai

Developers

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.