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.
For a guide on setting up webhook endpoints and handling events in your application, see Webhooks.
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"). |
Media URLs are signed and expire 15 minutes after generation. Download and store any media you need immediately upon receiving the webhook.
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:
{
"id": "evt_12345",
"type": "session.status",
"createdAt": 1761594651878,
"sessionId": "3f0a4e1e-9c2b-4b6f-8d3a-2a8e1b7c4f01"
}The payload does not include the new status value. Fetch the session via GET /sessions/:id to read the current status and report data.
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:
{
"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:
{
"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:
{
"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:
{
"id": "evt_videoanal1",
"type": "analysis.video",
"createdAt": 1761595900000,
"sessionId": "3f0a4e1e-9c2b-4b6f-8d3a-2a8e1b7c4f01"
}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.
analysis.screenshot
Fires when screenshot-based AI analysis completes.
When it fires: Shortly after the session ends, once screenshot analysis has been processed.
Payload:
{
"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:
{
"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:
{
"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:
- Before processing, check whether you have already handled an event with this
id. - If yes, return
200immediately and skip processing. - If no, process the event and record the
idas handled.
Store processed event IDs in a persistent store (database, Redis) rather than in-memory, so deduplication survives application restarts.
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 })
}