Call Lens
Webhooks

Failure events

call.failed and call.degraded — opt-in events for calls that never reached completed.

Most calls finish and you get call.completed. Some do not, and before these two events existed you heard nothing at all about those — which is indistinguishable, from your side, from a quiet week.

EventFires whenWhat you still have
call.failedA required stage exhausted its attempts. The call is quarantined.Nothing. There is no transcript and no analysis.
call.degradedAn optional stage failed or was blocked. The call is partially completed.The transcript and the metrics. Only the optional stage's output is missing.

Both are opt-in per endpoint — see choosing events.

call.degraded is not a failure, and routing it as one will misinform your users. The call worked; a piece of enrichment did not. The most common causes are an unset provider key and a reached budget cap, and in both cases the recording was transcribed and measured normally. That is why these are two kinds and not one with a status field — route on the kind, and treat call.degraded as "less than expected", never as "lost".

Which one you get, and when

A call reaches exactly one terminal state, so you receive at most one of these, and never alongside call.completed — a degraded call never reaches completed.

Each fires once per call. If we retry a quarantined call and it fails again, you do not get a second call.failed. If a retry succeeds, you get call.completed afterwards, so receiving call.failed and then call.completed for one call is expected rather than contradictory: read it as "this recovered".

Duplicate recordings are the one silent case. When identical audio is submitted twice, the second submission is superseded rather than processed, and no event fires for it — the original call already sent you its call.completed, so you have the analysis. An event announcing a failure there would be reporting an incident for a correct outcome.

The payload

Same envelope, same signature, same headers as call.completed. X-CallLens-Event names the kind, so you can route on the header before parsing the body.

{
  "event": "call.degraded",
  "occurred_at": "2026-09-08T14:02:55Z",
  "delivery": { "idempotency_key": "9c1e4a70-...", "attempt": 1 },
  "data": {
    "call": {
      "reference": "cl_7Yx2bQ...",
      "provider_call_id": "PBX-88121",
      "idempotency_key": "your-key-abc",
      "status": "partially_completed",
      "…": "…"
    },
    "stages": [
      { "stage": "transcribe", "status": "succeeded", "criticality": "hard", "attempts": 1, "correlation_id": null },
      { "stage": "enrich", "status": "blocked", "criticality": "soft", "attempts": 3, "correlation_id": "err_01JB9…" }
    ]
  }
}

data.call is the object call.completed publishes under the same name — one shape, documented once — and provider_call_id and idempotency_key are echoed back exactly as they are there, so the event joins to your own record the same way.

data.stages

Every stage of the call, in the order they ran, so you can see where it stopped.

  • criticality is hard or soft. A hard stage with status: "failed" is what ended a call.failed. A soft one is what degraded a call.degraded.
  • status is pending, running, succeeded, failed, skipped or blocked. blocked means a budget or configuration gate refused the stage rather than the stage erroring.
  • On call.degraded the succeeded stages matter as much as the failed one: they tell you what you do have.

There is no error message, deliberately. A stage's own error text can carry internal detail we will not send to an address outside our platform. correlation_id is what to quote to support — it keys the log entry holding the detail this payload does not.

No analysis

Neither event carries data.analysis, including call.degraded, where one often exists. These events say where the call stopped, not what was found. Publishing a partial analysis under a degradation notice invites it to be read as a complete one; ask for the call by its reference if you want what was produced.

What the payload contains

The disclosure on the overview applies in full, though these two carry markedly less: the call's metadata and its pipeline stages, with no model-generated text at all.

On this page