Skip to content

Events and payload ​

A webhook is Esfera calling your server when a conversion changes, so you do not have to poll. This page lists every event the API sends, when each one fires, the exact body and headers of a delivery, and what to know about which events you receive. To trust a delivery you must verify its signature, and because deliveries can repeat you should read Retries and replay.

Registering an endpoint ​

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"}'
json
{ "id": "6c1e0f6a-2d34-4c55-8a09-1f7d3b9e2a41", "url": "https://example.com/esfera", "secret": "whsec_esf_…" }

The secret is returned once, in this response, and never again. Store it where your receiver can read it. It is the key you verify signatures with. An account can have up to three active endpoints, and the URL must be https. The rules are in Retries and replay. The SDK call is esfera.webhooks.create({ url }).

The events ​

EventFires whenstatus in the payload
conversion.analyzedAnalysis has finished. This is the signal to choose a preset in the two-step flow. It also fires in the auto-start flow, just before the encode begins.analyzed
conversion.completedThe encode finished and the output can be downloaded.ready
conversion.failedThe job failed, in analysis or in the encode. errorMessage says why.error
conversion.expiringThe output will be removed within 24 hours. See Retention.ready, or error for a failed job

Notes:

  • conversion.analyzed is sent every time a conversion reaches analyzed, whichever flow it is in.
  • conversion.failed covers every way a job ends in error: a failed analysis, a failed encode, an auto-start the server refused after analysis (for example a preset that cannot take the source), and a job the server timed out. The event for a timed-out job can arrive a little after the status changes.
  • conversion.completed and conversion.failed are terminal. Each fires once per conversion.
  • conversion.expiring fires once per conversion, only for conversions that have an expiresAt.
  • There is no event for waiting, queued or running. Use GET /v1/conversions/{id} if you want to follow those.

Which conversions send events ​

Events belong to the account, not to an API key. Every active endpoint on the account receives the events for every conversion on that account, including conversions made in the Esfera web app, except conversion.expiring, which only API conversions have. An endpoint registered after an event happened does not receive it. A revoked endpoint receives nothing further.

The request ​

Each delivery is an HTTPS POST to your URL.

PartValue
MethodPOST
Content-Typeapplication/json
X-Esfera-EventThe event name, repeated from the body.
X-Esfera-Signaturet=<unix seconds>,v1=<hex HMAC-SHA256>. See Verifying signatures.
User-Agentesfera-webhooks/1
TimeoutEsfera waits 10 seconds for your response.
SuccessAny 2xx response. Anything else, or no answer in time, is a failed attempt and is retried.

The body is JSON with the same envelope for every event:

FieldTypeMeaning
eventstringOne of the four event names.
createdAtstring, date-timeWhen the event was created.
data.conversionobjectThe public Conversion, the same shape GET /v1/conversions/{id} returns, so an event and a read can never disagree about a field.

The conversion is a snapshot taken when the event was created. A retry or a replay delivers that snapshot again, not the conversion's current state. If you need the latest, call GET /v1/conversions/{id}.

Respond with a 2xx as soon as you have verified the signature and stored the event. Do the real work afterwards. A response slower than 10 seconds counts as a failure and the event will arrive again.

Examples ​

conversion.analyzed ​

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 ​

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 ​

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
    }
  }
}

A failed job also carries an expiresAt: its source is removed on the same 72-hour schedule as a finished job's output.

conversion.expiring ​

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
          }
        ]
      }
    }
  }
}

The sample values are illustrative. The shape is the contract.

The delivery log ​

Every delivery is recorded. GET /v1/webhooks/{id}/deliveries lists them newest first, 25 to a page by default and up to 100.

FieldMeaning
idThe delivery id, used to replay it.
conversionIdThe conversion, or null once that conversion is deleted. The delivery outlives it.
eventThe event name.
statuspending while it is queued or being retried, delivered after a 2xx, failed when the schedule is exhausted.
attemptsHow many times Esfera has tried, over the delivery's whole life.
lastStatusThe HTTP status your endpoint last returned, or null.
lastErrorThe last failure as text, or null.
createdAt, deliveredAtWhen it was created, and when it was delivered.

There is at most one delivery per endpoint, conversion and event. That is why the delivery log is a reliable record of what you were sent.

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