FaceSign
Guides

Webhooks

Receive real-time notifications when session events occur.

Overview

Webhooks allow you to receive real-time HTTP POST notifications when events occur in your FaceSign sessions. This enables you to:

  • Track session progress without polling
  • Update your database when verification completes
  • Handle errors and retries automatically
  • Send notifications to users

Setting Up Webhooks

Create an HTTPS endpoint in your application to receive webhook events.

Example Webhook Handler
// Next.js API Route: /api/webhooks/facesign
export async function POST(req) {
  const event = await req.json()

  // Log the event
  console.log('Received webhook:', event.type, event.sessionId)

  // Handle different event types
  switch (event.type) {
    case 'session.status':
      await handleSessionStatusChanged(event)
      break
    case 'analysis.video':
      await handleSessionVideoAnalysisComplete(event)
      break
    // Handle other events...
  }

  // Always return 200 to acknowledge receipt
  return Response.json({ received: true })
}
Example Webhook Handler
# Flask webhook handler
from flask import Flask, request, jsonify
import logging

app = Flask(__name__)

@app.route('/webhooks/facesign', methods=['POST'])
def handle_webhook():
    event = request.json

    # Log the event
    logging.info(f"Received webhook: {event['type']} for session {event['sessionId']}")

    # Handle different event types
    if event['type'] == 'session.status':
        handle_session_status_changed(event)
    elif event['type'] == 'analysis.video':
        handle_session_video_analysis_complete(event)

    # Always return 200 to acknowledge receipt
    return jsonify({'received': True}), 200
Example Webhook Handler
// Go webhook handler
func handleWebhook(w http.ResponseWriter, r *http.Request) {
    var event WebhookEvent
    json.NewDecoder(r.Body).Decode(&event)

    // Log the event
    log.Printf("Received webhook: %s for session %s", event.Type, event.SessionID)

    // Handle different event types
    switch event.Type {
    case "session.status":
        handleSessionStatusChanged(event)
    case "analysis.video":
        handleSessionVideoAnalysisComplete(event)
    }

    // Always return 200 to acknowledge receipt
    w.WriteHeader(http.StatusOK)
    json.NewEncoder(w).Encode(map[string]bool{"received": true})
}

Event Types

FaceSign sends the following webhook events during the session lifecycle:

Event TypeDescriptionWhen Sent
session.statusSession status changed (created / inProgress / incomplete / complete)Whenever session status is updated
media.user_photoUser photo is available for downloadAfter the user photo has been captured
media.document_photoA document photo is available for downloadAfter a user provided their document
media.user_videoUser recording video is available for downloadAfter the session ended and the video prepared for downloading
analysis.videoVideo analysis completedAfter video analysis finishes
analysis.screenshotScreenshot analysis completedAfter screenshot analysis
settings.avatarsAvailable avatars changedWhen avatars are updated on FaceSign platform
settings.langsAvailable languages changedWhen available languages are updated on FaceSign platform

Webhook Payload Examples

All webhook events include these common fields:

Common Webhook Fields
type WebhookEvent = {
    id: string;
    type: WebhookType;
    createdAt: number;
    sessionId: string;
    media?: WebhookMedia;
};

Webhook Event Types

The following webhook events are currently available and correspond to the documented API contract:

session.status

Sent when the status of a session changes:

session.status payload
{
    "id": "evt_12345",
    "type": "session.status",
    "createdAt": 1761594651878,
    "sessionId": "3f0a4e1e-9c2b-4b6f-8d3a-2a8e1b7c4f01"
}

media.user_photo

Sent when a user selfie has been captured and is ready to download:

media.user_photo 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

Sent when a document photo is captured and available:

media.document_photo 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

Sent when the user's verification video is ready for download:

media.user_video 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

Sent when post-verification video analysis completes:

analysis.video payload
{
  "id": "evt_videoanal1",
  "type": "analysis.video",
  "createdAt": 1761595900000,
  "sessionId": "3f0a4e1e-9c2b-4b6f-8d3a-2a8e1b7c4f01"
}

analysis.screenshot

Sent when screenshot analysis result is available:

analysis.screenshot payload
{
  "id": "evt_screenshot2",
  "type": "analysis.screenshot",
  "createdAt": 1761595910000,
  "sessionId": "3f0a4e1e-9c2b-4b6f-8d3a-2a8e1b7c4f01"
}

settings.avatars

Sent when the list of available avatars is updated:

settings.avatars payload
{
  "id": "evt_avtchg1",
  "type": "settings.avatars",
  "createdAt": 1761596000000,
  "sessionId": "3f0a4e1e-9c2b-4b6f-8d3a-2a8e1b7c4f01"
}

settings.langs

Sent when the list of available languages is changed:

settings.langs payload
{
  "id": "evt_langs3",
  "type": "settings.langs",
  "createdAt": 1761596100000,
  "sessionId": "3f0a4e1e-9c2b-4b6f-8d3a-2a8e1b7c4f01"
}

Every event — regardless of type — carries the same four top-level fields:

  • id — unique event ID, use it for idempotent processing
  • type — one of the WebhookType values listed in the table above
  • createdAt — event timestamp in milliseconds since the Unix epoch
  • sessionId — the session the event belongs to

Media events (media.user_photo, media.document_photo, media.user_video) add a single media object containing id, createdAt, url, expires, and contentType — there are no other event-specific fields. For the full session state — node reports, AI analysis, transcript, and so on — fetch GET /sessions/:id using the sessionId from the event.


Media URLs

Media URLs are provided in media.user_photo, media.document_photo, and media.user_video events:

Downloading Media Files
async function downloadMedia(media) {
  if (media?.url) {
    const response = await fetch(media.url);
    const blob = await response.blob();
    // Store media by type or contentType
    await storeFile(blob, media.id + '.' + (media.contentType.split('/')[1] || 'bin'));
  }

  // media.expires is ms timestamp
  console.log('Expires at:', new Date(media.expires));
}

Webhook Security

Signature validation

Every webhook delivery carries an HMAC-SHA256 signature you can use to confirm that the request really came from FaceSign before trusting any of its contents.

FaceSign sends two headers with every event:

HeaderContents
X-FaceSign-TimestampUnix timestamp (seconds) when the signature was generated
X-FaceSign-Signaturet=<timestamp>,v1=<hex>v1 is HMAC-SHA256(secret, "<timestamp>.<rawBody>")

The signing secret is shown once per webhook in the FaceSign dashboard. Store it as an environment variable — never hard-code it.

Webhook signature verification
import crypto from 'node:crypto'

// Verify an HMAC-SHA256 signature of the form `t=<timestamp>,v1=<hex>`
// over `${timestamp}.${rawBody}`, using the webhook secret from the
// FaceSign dashboard.
function verifyFacesignSignature(rawBody, signatureHeader, secret) {
  if (!signatureHeader) return false
  const parts = Object.fromEntries(
    signatureHeader.split(',').map((p) => p.split('='))
  )
  if (!parts.t || !parts.v1) return false
  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${parts.t}.${rawBody}`, 'utf8')
    .digest('hex')
  try {
    return crypto.timingSafeEqual(
      Buffer.from(parts.v1, 'hex'),
      Buffer.from(expected, 'hex')
    )
  } catch {
    return false
  }
}

export async function POST(req) {
  // Read the raw body before parsing — signature is computed over the
  // exact bytes we received.
  const rawBody = await req.text()
  const signature = req.headers.get('x-facesign-signature')

  if (!verifyFacesignSignature(rawBody, signature, process.env.FACESIGN_WEBHOOK_SECRET)) {
    return new Response('Invalid signature', { status: 401 })
  }

  const event = JSON.parse(rawBody)
  // Process verified webhook...
}

Error Handling

Retry Logic

FaceSign retries failed webhook deliveries with exponential backoff. A delivery counts as failed when your endpoint returns a non-2xx response, times out, or is unreachable.

Your endpoint should:

  1. Always return HTTP 200 for successfully received events — even if downstream processing will happen asynchronously.
  2. Return 4xx for invalid payloads — these are considered permanent failures and will not be retried.
  3. Return 5xx for transient failures — these will be retried on the backoff schedule.

Idempotency

Webhooks may be delivered more than once. Each webhook event has a unique id property, which should be used to guarantee idempotent processing:

Idempotent Webhook Processing
const processedEventIds = new Set()

export async function handleWebhook(event) {
  // Use the unique event id for idempotency
  if (processedEventIds.has(event.id)) {
    console.log('Duplicate webhook, skipping:', event.id)
    return { received: true }
  }

  // Process the event
  await processEvent(event)

  // Mark this event id as processed
  processedEventIds.add(event.id)

  return { received: true }
}

Testing Webhooks

Local Development

Use ngrok or similar tools to test webhooks locally:

Testing with ngrok
# Start your local server
npm run dev  # Runs on http://localhost:3000

# In another terminal, expose it via ngrok
ngrok http 3000

# Use the ngrok URL for webhooks
# https://abc123.ngrok.io/api/webhooks/facesign

Webhook Testing Service

You can use webhook.site for quick testing:

  1. Go to https://webhook.site
  2. Copy your unique URL
  3. Use it as your webhook URL when creating sessions
  4. View incoming webhooks in real-time

Best Practices

  1. Always return 200 OK - Even if processing fails, acknowledge receipt
  2. Process asynchronously - Don't block the webhook response
  3. Implement idempotency - Handle duplicate deliveries gracefully
  4. Store event data - Keep webhook payloads for debugging
  5. Monitor failures - Set up alerts for webhook processing errors
  6. Handle all event types - Even if you don't process them yet
  7. Download media immediately - URLs expire after 15 minutes

Next Steps

On this page