Skip to content

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

FieldTypeRequiredDescription
urlstring (uri)yeshttps only; no localhost or private ranges.

Responses

StatusMeaningBody
201Registered. Store secret now.WebhookCreateResponse
400url is required (invalid-request).Problem
401The API key is missing, malformed, unknown, revoked, expired, or for the other environment (invalid-api-key).Problem
409Already 3 active endpoints (webhook-limit).Problem
422The URL is not allowed (invalid-webhook-url).Problem
429Over 120 requests/minute for this key (rate-limited). Honour Retry-After.Problem
500The endpoint could not be saved (internal).Problem
bash
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"}'
js
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" });
python
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

json
{
  "id": "3f2b8c1e-5a47-4d0e-9c1a-7b6e2d8f4a10",
  "url": "https://example.com/esfera",
  "secret": "whsec_esf_…"
}

GET List the account's endpoints ​

GET /v1/webhooks

Responses

StatusMeaningBody
200Every endpoint, revoked ones included; never the secret.WebhookList
401The API key is missing, malformed, unknown, revoked, expired, or for the other environment (invalid-api-key).Problem
429Over 120 requests/minute for this key (rate-limited). Honour Retry-After.Problem
bash
curl "https://api.esfera.audio/v1/webhooks" \
  -H "Authorization: Bearer $ESFERA_API_KEY"
js
import { Esfera } from "@esfera/sdk";

const esfera = new Esfera({ apiKey: process.env.ESFERA_API_KEY });

const { webhooks } = await esfera.webhooks.list();
python
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

json
{
  "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

NameInTypeRequiredDescription
idpathstring (uuid)yesThe resource id (account-scoped).

Responses

StatusMeaningBody
200Revoked.Deleted
401The API key is missing, malformed, unknown, revoked, expired, or for the other environment (invalid-api-key).Problem
404No such active endpoint (not-found).Problem
429Over 120 requests/minute for this key (rate-limited). Honour Retry-After.Problem
500The revoke could not be saved (internal).Problem
bash
curl -X DELETE "https://api.esfera.audio/v1/webhooks/3f2b8c1e-5a47-4d0e-9c1a-7b6e2d8f4a10" \
  -H "Authorization: Bearer $ESFERA_API_KEY"
js
import { Esfera } from "@esfera/sdk";

const esfera = new Esfera({ apiKey: process.env.ESFERA_API_KEY });

await esfera.webhooks.revoke(id);
python
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

json
{
  "ok": true
}

GET Read an endpoint's delivery log ​

GET /v1/webhooks/{id}/deliveries

Parameters

NameInTypeRequiredDescription
idpathstring (uuid)yesThe resource id (account-scoped).
pagequeryintegernoDefault 1.
pageSizequeryintegernoDefault 25. Maximum 100.

Responses

StatusMeaningBody
200A page of deliveries, newest first.WebhookDeliveryPage
401The API key is missing, malformed, unknown, revoked, expired, or for the other environment (invalid-api-key).Problem
404No such endpoint (not-found).Problem
429Over 120 requests/minute for this key (rate-limited). Honour Retry-After.Problem
500The log could not be read (internal).Problem
bash
curl "https://api.esfera.audio/v1/webhooks/3f2b8c1e-5a47-4d0e-9c1a-7b6e2d8f4a10/deliveries?page=1" \
  -H "Authorization: Bearer $ESFERA_API_KEY"
js
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 });
python
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

json
{
  "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

NameInTypeRequiredDescription
idpathstring (uuid)yesThe resource id (account-scoped).
deliveryIdpathstring (uuid)yesA delivery id from the deliveries list.

Responses

StatusMeaningBody
202Queued for redelivery.Deleted
401The API key is missing, malformed, unknown, revoked, expired, or for the other environment (invalid-api-key).Problem
404No such endpoint or delivery (not-found).Problem
409Already queued or retrying (replay-in-flight).Problem
429Over 120 requests/minute for this key (rate-limited). Honour Retry-After.Problem
500The replay could not be started (internal).Problem
503The delivery queue is unavailable (queue-unavailable) or refused the message (queue-send-failed).Problem
bash
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"
js
import { Esfera } from "@esfera/sdk";

const esfera = new Esfera({ apiKey: process.env.ESFERA_API_KEY });

await esfera.webhooks.deliveries.replay(webhookId, deliveryId);
python
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

json
{
  "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.

HeaderValue
X-Esfera-EventThe event name, repeated from the body.
X-Esfera-Signaturet=<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).

json
{
  "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.

json
{
  "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.

json
{
  "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.

json
{
  "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 ​

FieldTypeRequiredDescription
idstring (uuid)yes
urlstring (uri)yes
createdAtstring (date-time)yesWhen it was created.
revokedAtstring or nullyesWhen it was revoked; null while active.

WebhookDelivery ​

FieldTypeRequiredDescription
idstring (uuid)yes
conversionIdstring or nullyesNull once the conversion is deleted; the delivery outlives it.
eventconversion.analyzed | conversion.completed | conversion.failed | conversion.expiringyes
statuspending | delivered | failedyes
attemptsintegeryesWhole-lifetime attempt count; replays keep counting.
lastStatusinteger or nullyesHTTP status your endpoint last returned.
lastErrorstring or nullyes
createdAtstring (date-time)yesWhen it was created.
deliveredAtstring or nullyes

developers.esfera.audio · the Esfera Converter API, SDK and CLI