> ## Documentation Index
> Fetch the complete documentation index at: https://intervyo.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Real-time event delivery to your endpoint — HMAC-signed, retried, ordered per session.

Webhooks let your system react to interview lifecycle events in real
time — `session.completed` is the most-listened event, but you can subscribe
to anything from session creation through scorecard finalization.

## Registering an endpoint

<Steps>
  <Step title="Open Developer settings">
    **Settings → Developer → Webhooks** inside your workspace.
  </Step>

  <Step title="Add an endpoint">
    Paste your URL. Pick the events you want delivered. Save.
  </Step>

  <Step title="Copy the signing secret">
    A 32-byte secret is generated for your endpoint. Store it in your
    secret manager — you'll use it to verify HMAC signatures on every
    incoming request.
  </Step>
</Steps>

Webhook endpoints are managed in the dashboard under **Developer → Webhooks** —
add the URL, choose the events, and copy the signing secret.

## Event types

| Event                  | When it fires                                                                     |
| ---------------------- | --------------------------------------------------------------------------------- |
| `session.created`      | A new session row is created (API call or apply-page submission).                 |
| `session.started`      | Candidate joined; AI is interviewing.                                             |
| `session.completed`    | Conversation ended naturally. Scorecard not necessarily ready yet.                |
| `session.scored`       | **Scorecard finalized.** Listen for this if you want the per-dimension breakdown. |
| `session.failed`       | Candidate disconnected mid-interview or tech failure interrupted.                 |
| `session.cancelled`    | Recruiter cancelled before the candidate joined.                                  |
| `participant.created`  | A new participant was added (via apply flow or API).                              |
| `application.approved` | HR approved an application — candidate can be scheduled.                          |
| `application.rejected` | HR declined an application.                                                       |

## Payload shape

Every webhook delivers a JSON body with a consistent envelope:

```json theme={null}
{
  "id": "evt_a1b2c3d4",
  "event": "session.scored",
  "occurred_at": "2026-06-02T08:21:47Z",
  "data": {
    "session": {
      "id": "e3a1c2d4-...",
      "status": "completed",
      "score": "8.2",
      "passed": true,
      "recommendation": "hire",
      "evaluation_breakdown": [ /* per-dimension */ ],
      "transcript_url": "https://…",
      "recording_url": "https://…",
      "authenticity_signals": { /* … */ },
      "candidate_id": "ba4f2cdd-...",
      "role_id": "ce1cd564-...",
      "external_id": "greenhouse-8821"
    }
  }
}
```

The `data` shape varies by event — for `participant.*` events, you get
the participant; for `application.*`, the application.

<Tip>
  Idempotency: deduplicate by `id`. The same logical event may be
  delivered more than once during retry storms.
</Tip>

## Verifying the signature

Every request carries a `intervyo-signature` header with an HMAC-SHA256
signature of the raw body, signed with your endpoint's secret:

```http theme={null}
intervyo-signature: t=1735689600,v1=2c8ee5d1b5…
```

Verify before trusting the payload — anyone could post to your URL
otherwise:

<CodeGroup>
  ```ts TypeScript theme={null}
  import crypto from "crypto";

  function verify(rawBody: string, signatureHeader: string, secret: string) {
    const parts = Object.fromEntries(
      signatureHeader.split(",").map((kv) => kv.split("=") as [string, string]),
    );
    const expected = crypto
      .createHmac("sha256", secret)
      .update(`${parts.t}.${rawBody}`)
      .digest("hex");
    // Constant-time comparison
    return crypto.timingSafeEqual(
      Buffer.from(parts.v1, "hex"),
      Buffer.from(expected, "hex"),
    );
  }
  ```

  ```python Python theme={null}
  import hmac, hashlib

  def verify(raw_body: str, signature_header: str, secret: str) -> bool:
      parts = dict(kv.split("=", 1) for kv in signature_header.split(","))
      expected = hmac.new(
          secret.encode(),
          f"{parts['t']}.{raw_body}".encode(),
          hashlib.sha256,
      ).hexdigest()
      return hmac.compare_digest(parts["v1"], expected)
  ```
</CodeGroup>

<Warning>
  Verify against the **raw body** — don't deserialize, re-serialize, and
  hash. JSON whitespace changes break the signature. In Express, capture
  the raw body with `express.raw({ type: "application/json" })` before
  parsing.
</Warning>

## Retries

If your endpoint returns a non-2xx status (or times out at 10 seconds),
the platform retries with exponential backoff:

| Attempt | Delay     |
| ------- | --------- |
| 1       | immediate |
| 2       | 30 s      |
| 3       | 2 min     |
| 4       | 10 min    |
| 5       | 1 hour    |
| 6       | 6 hours   |
| 7       | 24 hours  |

After 7 failed attempts the event is dropped and a `webhook.delivery_failed`
metric is incremented on your workspace. Subsequent events are still
attempted — failures don't suspend the endpoint.

## Ordering

Events are delivered **in order per session**. You're guaranteed to see
`session.created` before `session.started`, and `session.scored` after
`session.completed`. Across sessions there's no global ordering — events
for session A can arrive interleaved with events for session B.

## Testing your endpoint

The webhook settings page has a **Send test event** button that fires
a `session.scored` payload with synthetic data. Useful for verifying your
endpoint + signature verification before live traffic hits it.

You can also re-deliver any past event from the **Logs** tab on the
endpoint detail page — useful when you've fixed a bug and want to backfill
the events you missed.
