Webhooks
Register endpoints for conversion events; read and replay the delivery log.
Guides: events and payloads, verifying signatures and retries and replay.
Operations
POST Register an endpoint
POST /v1/webhooks
https only, no private hosts. The signing secret is returned once, here, and never again. Up to 3 active endpoints per account.
Request body · CreateWebhookRequest
| Field | Type | Required | Description |
|---|---|---|---|
url | string (uri) | yes | https only; no localhost or private ranges. |
Responses
| Status | Meaning | Body |
|---|---|---|
201 | Registered. Store secret now. | WebhookCreateResponse |
400 | url is required (invalid-request). | Problem |
401 | The API key is missing, malformed, unknown, revoked, expired, or for the other environment (invalid-api-key). | Problem |
409 | Already 3 active endpoints (webhook-limit). | Problem |
422 | The URL is not allowed (invalid-webhook-url). | Problem |
429 | Over 120 requests/minute for this key (rate-limited). Honour Retry-After. | Problem |
500 | The endpoint could not be saved (internal). | Problem |
curl -X POST "https://api.esfera.audio/v1/webhooks" \
-H "Authorization: Bearer $ESFERA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com/esfera"}'import { Esfera } from "@esfera/sdk";
const esfera = new Esfera({ apiKey: process.env.ESFERA_API_KEY });
const { id, secret } = await esfera.webhooks.create({ url: "https://example.com/esfera" });import os
import requests
res = requests.post(
"https://api.esfera.audio/v1/webhooks",
headers={
"Authorization": f"Bearer {os.environ['ESFERA_API_KEY']}",
},
json={
"url": "https://example.com/esfera",
},
)
res.raise_for_status()
print(res.json())Response · 201
{
"id": "3f2b8c1e-5a47-4d0e-9c1a-7b6e2d8f4a10",
"url": "https://example.com/esfera",
"secret": "whsec_esf_…"
}GET List the account's endpoints
GET /v1/webhooks
Responses
| Status | Meaning | Body |
|---|---|---|
200 | Every endpoint, revoked ones included; never the secret. | WebhookList |
401 | The API key is missing, malformed, unknown, revoked, expired, or for the other environment (invalid-api-key). | Problem |
429 | Over 120 requests/minute for this key (rate-limited). Honour Retry-After. | Problem |
curl "https://api.esfera.audio/v1/webhooks" \
-H "Authorization: Bearer $ESFERA_API_KEY"import { Esfera } from "@esfera/sdk";
const esfera = new Esfera({ apiKey: process.env.ESFERA_API_KEY });
const { webhooks } = await esfera.webhooks.list();import os
import requests
res = requests.get(
"https://api.esfera.audio/v1/webhooks",
headers={
"Authorization": f"Bearer {os.environ['ESFERA_API_KEY']}",
},
)
res.raise_for_status()
print(res.json())Response · 200
{
"webhooks": [
{
"id": "3f2b8c1e-5a47-4d0e-9c1a-7b6e2d8f4a10",
"url": "https://example.com/esfera",
"createdAt": "2026-09-02T10:15:00Z",
"revokedAt": null
}
]
}DELETE Revoke an endpoint
DELETE /v1/webhooks/{id}
Revoked, never deleted — its delivery log stays readable. A second revoke is a 404.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (uuid) | yes | The resource id (account-scoped). |
Responses
| Status | Meaning | Body |
|---|---|---|
200 | Revoked. | Deleted |
401 | The API key is missing, malformed, unknown, revoked, expired, or for the other environment (invalid-api-key). | Problem |
404 | No such active endpoint (not-found). | Problem |
429 | Over 120 requests/minute for this key (rate-limited). Honour Retry-After. | Problem |
500 | The revoke could not be saved (internal). | Problem |
curl -X DELETE "https://api.esfera.audio/v1/webhooks/3f2b8c1e-5a47-4d0e-9c1a-7b6e2d8f4a10" \
-H "Authorization: Bearer $ESFERA_API_KEY"import { Esfera } from "@esfera/sdk";
const esfera = new Esfera({ apiKey: process.env.ESFERA_API_KEY });
await esfera.webhooks.revoke(id);import os
import requests
res = requests.delete(
"https://api.esfera.audio/v1/webhooks/3f2b8c1e-5a47-4d0e-9c1a-7b6e2d8f4a10",
headers={
"Authorization": f"Bearer {os.environ['ESFERA_API_KEY']}",
},
)
res.raise_for_status()
print(res.json())Response · 200
{
"ok": true
}GET Read an endpoint's delivery log
GET /v1/webhooks/{id}/deliveries
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (uuid) | yes | The resource id (account-scoped). |
page | query | integer | no | Default 1. |
pageSize | query | integer | no | Default 25. Maximum 100. |
Responses
| Status | Meaning | Body |
|---|---|---|
200 | A page of deliveries, newest first. | WebhookDeliveryPage |
401 | The API key is missing, malformed, unknown, revoked, expired, or for the other environment (invalid-api-key). | Problem |
404 | No such endpoint (not-found). | Problem |
429 | Over 120 requests/minute for this key (rate-limited). Honour Retry-After. | Problem |
500 | The log could not be read (internal). | Problem |
curl "https://api.esfera.audio/v1/webhooks/3f2b8c1e-5a47-4d0e-9c1a-7b6e2d8f4a10/deliveries?page=1" \
-H "Authorization: Bearer $ESFERA_API_KEY"import { Esfera } from "@esfera/sdk";
const esfera = new Esfera({ apiKey: process.env.ESFERA_API_KEY });
const page = await esfera.webhooks.deliveries.list(webhookId, { page: 1 });import os
import requests
res = requests.get(
"https://api.esfera.audio/v1/webhooks/3f2b8c1e-5a47-4d0e-9c1a-7b6e2d8f4a10/deliveries",
headers={
"Authorization": f"Bearer {os.environ['ESFERA_API_KEY']}",
},
params={"page": 1},
)
res.raise_for_status()
print(res.json())Response · 200
{
"deliveries": [
{
"id": "b81d0f6a-27c3-4e9b-8a55-0c4d9e7f1a32",
"conversionId": "3f2b8c1e-5a47-4d0e-9c1a-7b6e2d8f4a10",
"event": "conversion.completed",
"status": "delivered",
"attempts": 1,
"lastStatus": 200,
"lastError": null,
"createdAt": "2026-09-02T10:41:00Z",
"deliveredAt": "2026-09-02T10:41:01Z"
}
],
"total": 1,
"page": 1,
"pageSize": 25
}POST Replay one delivery
POST /v1/webhooks/{id}/deliveries/{deliveryId}/replay
Re-sends the same event to the same endpoint; attempts keeps counting. Delivered rows may be replayed too (your handler can lose an event after a 2xx).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (uuid) | yes | The resource id (account-scoped). |
deliveryId | path | string (uuid) | yes | A delivery id from the deliveries list. |
Responses
| Status | Meaning | Body |
|---|---|---|
202 | Queued for redelivery. | Deleted |
401 | The API key is missing, malformed, unknown, revoked, expired, or for the other environment (invalid-api-key). | Problem |
404 | No such endpoint or delivery (not-found). | Problem |
409 | Already queued or retrying (replay-in-flight). | Problem |
429 | Over 120 requests/minute for this key (rate-limited). Honour Retry-After. | Problem |
500 | The replay could not be started (internal). | Problem |
503 | The delivery queue is unavailable (queue-unavailable) or refused the message (queue-send-failed). | Problem |
curl -X POST "https://api.esfera.audio/v1/webhooks/3f2b8c1e-5a47-4d0e-9c1a-7b6e2d8f4a10/deliveries/b81d0f6a-27c3-4e9b-8a55-0c4d9e7f1a32/replay" \
-H "Authorization: Bearer $ESFERA_API_KEY"import { Esfera } from "@esfera/sdk";
const esfera = new Esfera({ apiKey: process.env.ESFERA_API_KEY });
await esfera.webhooks.deliveries.replay(webhookId, deliveryId);import os
import requests
res = requests.post(
"https://api.esfera.audio/v1/webhooks/3f2b8c1e-5a47-4d0e-9c1a-7b6e2d8f4a10/deliveries/b81d0f6a-27c3-4e9b-8a55-0c4d9e7f1a32/replay",
headers={
"Authorization": f"Bearer {os.environ['ESFERA_API_KEY']}",
},
)
res.raise_for_status()
print(res.json())Response · 202
{
"ok": true
}Event payloads
Each delivery is an HTTPS POST with a JSON body, the same envelope for every event: event, createdAt, and data.conversion carrying the public Conversion object, so a payload and GET /v1/conversions/{id} can never disagree.
| Header | Value |
|---|---|
X-Esfera-Event | The event name, repeated from the body. |
X-Esfera-Signature | t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>" with your endpoint's secret>. Reject when |now − t| > 300 s. Compare in constant time. |
conversion.analyzed
Analysis finished; the conversion is analyzed (two-step flow) or about to start (auto-start).
{
"event": "conversion.analyzed",
"createdAt": "2026-09-02T10:15:00Z",
"data": {
"conversion": {
"id": "3f2b8c1e-5a47-4d0e-9c1a-7b6e2d8f4a10",
"name": "master.wav",
"status": "analyzed",
"stage": "analyzed",
"progress": 0,
"sourceDurationSec": 1800,
"outputDurationSec": null,
"outputChannels": 14,
"objectCount": 2,
"outputLoudnessLufs": null,
"outputSizeBytes": null,
"errorMessage": null,
"createdAt": "2026-09-02T10:15:00Z",
"updatedAt": "2026-09-02T10:16:00Z",
"programmeCount": 1,
"preset": "esfera",
"programme": null,
"expiresAt": null,
"profile": "advanced-2",
"outputProfile": null,
"source": {
"kind": "adm",
"order": null,
"normalization": null
},
"output": null
}
}
}conversion.completed
The encode finished; the conversion is ready and the output can be downloaded.
{
"event": "conversion.completed",
"createdAt": "2026-09-02T10:15:00Z",
"data": {
"conversion": {
"id": "3f2b8c1e-5a47-4d0e-9c1a-7b6e2d8f4a10",
"name": "master.wav",
"status": "ready",
"stage": "done",
"progress": 100,
"sourceDurationSec": 1800,
"outputDurationSec": 1800,
"outputChannels": 14,
"objectCount": 2,
"outputLoudnessLufs": -23.1,
"outputSizeBytes": 612345678,
"errorMessage": null,
"createdAt": "2026-09-02T10:15:00Z",
"updatedAt": "2026-09-02T10:41:00Z",
"programmeCount": 1,
"preset": "esfera",
"programme": null,
"expiresAt": "2026-09-05T10:41:00Z",
"profile": "advanced-2",
"outputProfile": "advanced-1",
"source": {
"kind": "adm",
"order": null,
"normalization": null
},
"output": {
"profile": "advanced-1",
"channels": 14,
"elementCount": 3,
"foldedObjects": 0,
"elements": [
{
"kind": "bed",
"label": "Bed",
"layout": "7.1.4",
"channels": 12
},
{
"kind": "object",
"label": "Narrator",
"layout": null,
"channels": 1
},
{
"kind": "object",
"label": "Effects",
"layout": null,
"channels": 1
}
],
"mixPresentations": [
{
"label": "Default",
"language": "en",
"elements": 3
}
]
}
}
}
}conversion.failed
The encode failed; errorMessage says why.
{
"event": "conversion.failed",
"createdAt": "2026-09-02T10:15:00Z",
"data": {
"conversion": {
"id": "3f2b8c1e-5a47-4d0e-9c1a-7b6e2d8f4a10",
"name": "master.wav",
"status": "error",
"stage": "encoding",
"progress": 40,
"sourceDurationSec": 1800,
"outputDurationSec": null,
"outputChannels": 14,
"objectCount": 2,
"outputLoudnessLufs": null,
"outputSizeBytes": null,
"errorMessage": "IAMF profile mismatch: this master has discrete objects and needs Advanced-1 or Advanced-2",
"createdAt": "2026-09-02T10:15:00Z",
"updatedAt": "2026-09-02T10:30:00Z",
"programmeCount": 1,
"preset": "esfera",
"programme": null,
"expiresAt": "2026-09-05T10:30:00Z",
"profile": "advanced-2",
"outputProfile": null,
"source": {
"kind": "adm",
"order": null,
"normalization": null
},
"output": null
}
}
}conversion.expiring
The output will be swept soon; download it if you haven't.
{
"event": "conversion.expiring",
"createdAt": "2026-09-02T10:15:00Z",
"data": {
"conversion": {
"id": "3f2b8c1e-5a47-4d0e-9c1a-7b6e2d8f4a10",
"name": "master.wav",
"status": "ready",
"stage": "done",
"progress": 100,
"sourceDurationSec": 1800,
"outputDurationSec": 1800,
"outputChannels": 14,
"objectCount": 2,
"outputLoudnessLufs": -23.1,
"outputSizeBytes": 612345678,
"errorMessage": null,
"createdAt": "2026-09-02T10:15:00Z",
"updatedAt": "2026-09-04T12:00:00Z",
"programmeCount": 1,
"preset": "esfera",
"programme": null,
"expiresAt": "2026-09-05T10:41:00Z",
"profile": "advanced-2",
"outputProfile": "advanced-1",
"source": {
"kind": "adm",
"order": null,
"normalization": null
},
"output": {
"profile": "advanced-1",
"channels": 14,
"elementCount": 3,
"foldedObjects": 0,
"elements": [
{
"kind": "bed",
"label": "Bed",
"layout": "7.1.4",
"channels": 12
},
{
"kind": "object",
"label": "Narrator",
"layout": null,
"channels": 1
},
{
"kind": "object",
"label": "Effects",
"layout": null,
"channels": 1
}
],
"mixPresentations": [
{
"label": "Default",
"language": "en",
"elements": 3
}
]
}
}
}
}Acknowledging a delivery
Acknowledged. Anything else is retried with backoff (8 attempts); deliveries are at-least-once — dedupe on (event, conversion id).
See Retries and replay for the schedule and Verifying signatures before you trust a body.
Objects
Webhook
| Field | Type | Required | Description |
|---|---|---|---|
id | string (uuid) | yes | |
url | string (uri) | yes | |
createdAt | string (date-time) | yes | When it was created. |
revokedAt | string or null | yes | When it was revoked; null while active. |
WebhookDelivery
| Field | Type | Required | Description |
|---|---|---|---|
id | string (uuid) | yes | |
conversionId | string or null | yes | Null once the conversion is deleted; the delivery outlives it. |
event | conversion.analyzed | conversion.completed | conversion.failed | conversion.expiring | yes | |
status | pending | delivered | failed | yes | |
attempts | integer | yes | Whole-lifetime attempt count; replays keep counting. |
lastStatus | integer or null | yes | HTTP status your endpoint last returned. |
lastError | string or null | yes | |
createdAt | string (date-time) | yes | When it was created. |
deliveredAt | string or null | yes |