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

# Webhook Events

> All FaceSign webhook event types with payload schemas and delivery behavior.

This reference documents every webhook event FaceSign sends, including payload schemas, delivery timing, and retry behavior.

<Note>
  For a guide on setting up webhook endpoints and handling events in your application, see [Webhooks](/docs/guides/add-webhooks).
</Note>

## Common payload structure

Every webhook event includes these fields:

| Field       | Type                  | Description                                                        |
| ----------- | --------------------- | ------------------------------------------------------------------ |
| `id`        | `string`              | Unique event identifier. Use this for idempotency.                 |
| `type`      | `string`              | The event type (e.g., `"session.status"`).                         |
| `createdAt` | `number`              | Unix timestamp in milliseconds when the event was created.         |
| `sessionId` | `string`              | The session this event belongs to.                                 |
| `media`     | `object \| undefined` | Present only on `media.*` events. Contains the media file details. |

## Media object

Events with a `media` field include:

| Field         | Type     | Description                                       |
| ------------- | -------- | ------------------------------------------------- |
| `id`          | `string` | Unique media identifier.                          |
| `createdAt`   | `number` | Unix timestamp (ms) when the media was generated. |
| `url`         | `string` | Signed download URL for the media file.           |
| `expires`     | `number` | Unix timestamp (ms) when the URL expires.         |
| `contentType` | `string` | MIME type (e.g., `"image/jpeg"`, `"video/mp4"`).  |

<Warning>
  Media URLs are signed and **expire 15 minutes after generation**. Download and store any media you need immediately upon receiving the webhook.
</Warning>

***

## Event types

### session.status

Fires whenever a session's status changes. You receive this event for every transition in the lifecycle: `created`, `inProgress`, `incomplete`, and `complete`.

**When it fires:** On every status change.

**Payload:**

```json wrap theme={null}
{
  "id": "evt_12345",
  "type": "session.status",
  "createdAt": 1761594651878,
  "sessionId": "3f0a4e1e-9c2b-4b6f-8d3a-2a8e1b7c4f01"
}
```

<Note>
  The payload does not include the new status value. Fetch the session via `GET /sessions/:id` to read the current status and report data.
</Note>

***

### media.user\_photo

Fires when a user's selfie or face capture photo is ready for download.

**When it fires:** After a face capture completes during the session.

**Payload:**

```json wrap theme={null}
{
  "id": "evt_uphoto789",
  "type": "media.user_photo",
  "createdAt": 1761595700000,
  "sessionId": "3f0a4e1e-9c2b-4b6f-8d3a-2a8e1b7c4f01",
  "media": {
    "id": "m_uphoto1",
    "createdAt": 1761595700000,
    "url": "https://media.facesign.ai/m_uphoto1.jpg",
    "expires": 1761596600000,
    "contentType": "image/jpeg"
  }
}
```

***

### media.document\_photo

Fires when a scanned document photo is ready for download.

**When it fires:** After the user completes a document scan node.

**Payload:**

```json wrap theme={null}
{
  "id": "evt_udoc456",
  "type": "media.document_photo",
  "createdAt": 1761595720000,
  "sessionId": "3f0a4e1e-9c2b-4b6f-8d3a-2a8e1b7c4f01",
  "media": {
    "id": "m_udoc2",
    "createdAt": 1761595720000,
    "url": "https://media.facesign.ai/m_udoc2.jpg",
    "expires": 1761596620000,
    "contentType": "image/jpeg"
  }
}
```

***

### media.user\_video

Fires when the full session recording video is processed and ready for download.

**When it fires:** After the session ends and the video has been prepared.

**Payload:**

```json wrap theme={null}
{
  "id": "evt_uvideo234",
  "type": "media.user_video",
  "createdAt": 1761595800000,
  "sessionId": "3f0a4e1e-9c2b-4b6f-8d3a-2a8e1b7c4f01",
  "media": {
    "id": "m_uvideo5",
    "createdAt": 1761595800000,
    "url": "https://media.facesign.ai/m_uvideo5.mp4",
    "expires": 1761596700000,
    "contentType": "video/mp4"
  }
}
```

***

### analysis.video

Fires when post-session video AI analysis completes. This event only fires for sessions with `videoAIAnalysisEnabled: true`.

**When it fires:** 5--20 seconds after the session ends.

**Payload:**

```json wrap theme={null}
{
  "id": "evt_videoanal1",
  "type": "analysis.video",
  "createdAt": 1761595900000,
  "sessionId": "3f0a4e1e-9c2b-4b6f-8d3a-2a8e1b7c4f01"
}
```

<Note>
  The analysis results are not included in the webhook payload. Fetch the session via `GET /sessions/:id` to read the `videoAIAnalysis` field on the report.
</Note>

***

### analysis.screenshot

Fires when screenshot-based AI analysis completes.

**When it fires:** Shortly after the session ends, once screenshot analysis has been processed.

**Payload:**

```json wrap theme={null}
{
  "id": "evt_screenshot2",
  "type": "analysis.screenshot",
  "createdAt": 1761595910000,
  "sessionId": "3f0a4e1e-9c2b-4b6f-8d3a-2a8e1b7c4f01"
}
```

***

### settings.avatars

Fires when the list of available avatars changes on the FaceSign platform. This is a platform-level event, not tied to a specific session.

**When it fires:** When avatars are added, removed, or updated.

**Payload:**

```json wrap theme={null}
{
  "id": "evt_avtchg1",
  "type": "settings.avatars",
  "createdAt": 1761596000000,
  "sessionId": ""
}
```

***

### settings.langs

Fires when the list of available languages changes on the FaceSign platform.

**When it fires:** When supported languages are added or removed.

**Payload:**

```json wrap theme={null}
{
  "id": "evt_langs3",
  "type": "settings.langs",
  "createdAt": 1761596100000,
  "sessionId": ""
}
```

***

## Event summary

| Event                  | Has media? | Timing                                    |
| ---------------------- | ---------- | ----------------------------------------- |
| `session.status`       | No         | On every status transition                |
| `media.user_photo`     | Yes        | After face capture                        |
| `media.document_photo` | Yes        | After document scan                       |
| `media.user_video`     | Yes        | After session ends and video is processed |
| `analysis.video`       | No         | 5--20s after session ends                 |
| `analysis.screenshot`  | No         | Shortly after session ends                |
| `settings.avatars`     | No         | On platform avatar changes                |
| `settings.langs`       | No         | On platform language changes              |

***

## Retry behavior

FaceSign retries failed webhook deliveries using exponential backoff:

| Retry | Delay      |
| ----- | ---------- |
| 1st   | 1 minute   |
| 2nd   | 5 minutes  |
| 3rd   | 15 minutes |
| 4th   | 1 hour     |
| 5th   | 6 hours    |

**Response handling:**

* **HTTP 200** -- Event acknowledged. No retry.
* **HTTP 4xx** -- Client error. FaceSign does **not** retry (the payload is considered invalid by your endpoint).
* **HTTP 5xx** -- Server error. FaceSign retries according to the schedule above.

After all retries are exhausted, the event is dropped. Monitor your endpoint health to avoid missing events.

## Idempotency

Webhooks may be delivered more than once. Every event has a unique `id` field. Use this `id` to deduplicate events in your handler:

1. Before processing, check whether you have already handled an event with this `id`.
2. If yes, return `200` immediately and skip processing.
3. If no, process the event and record the `id` as handled.

Store processed event IDs in a persistent store (database, Redis) rather than in-memory, so deduplication survives application restarts.

```javascript wrap theme={null}
export async function POST(req) {
  const event = await req.json()

  // Check for duplicate delivery
  const alreadyProcessed = await db.webhookEvents.exists(event.id)
  if (alreadyProcessed) {
    return Response.json({ received: true })
  }

  // Process the event
  await handleEvent(event)

  // Record the event ID
  await db.webhookEvents.insert({ id: event.id, processedAt: Date.now() })

  return Response.json({ received: true })
}
```

## Next steps

<CardGroup cols={2}>
  <Card title="Add Webhooks" href="/docs/guides/add-webhooks">
    Set up webhook endpoints in your application
  </Card>

  <Card title="Reports & Outcomes" href="/docs/reference/concepts#reports-outcomes">
    Understand the session report data you receive
  </Card>

  <Card title="Errors" href="/docs/reference/errors">
    API error codes and troubleshooting
  </Card>
</CardGroup>
