FaceSign
Guides

Production Checklist

Everything to verify before moving your FaceSign integration from sandbox to production.

Use this checklist before switching your FaceSign integration from the dev environment to production. Each step addresses a category that commonly causes issues in live deployments.


Deployment

Replace API keys

Swap your sandbox key (sk_test_...) for a production key (sk_prod_...). Store it in an environment variable or secrets manager -- never hard-code it in source files.

# Development
FACESIGN_API_KEY=sk_test_abc123...

# Production
FACESIGN_API_KEY=sk_prod_xyz789...

Set environment variables on the host

For Next.js exports, use printf to set the API key. Do not use echo -- it appends a trailing newline that corrupts the value and causes "not a legal HTTP header value" errors at runtime.

Vercel deployment
cd my-nextjs-demo
printf "sk_prod_your_key_here" | npx vercel env add FACESIGN_API_KEY production
npx vercel --prod --yes

For static HTML deployments:

Static deployment
cd my-demo
npx vercel --prod --yes

Or drop the built folder on any static host.

Handle 409 Conflict on redeployment

If Vercel returns a 409 Conflict when adding an environment variable, the variable already exists. Remove it first, then re-add:

npx vercel env rm FACESIGN_API_KEY production --yes
printf "sk_prod_new_key" | npx vercel env add FACESIGN_API_KEY production

Configure custom domains and team SSO

If deploying behind a Vercel team with SSO, ensure the deployment is publicly accessible or that the SSO session cookie is configured correctly. Vercel SSO walls can block the FaceSign session URL from loading in an iframe.

For custom domains, add them via the Vercel dashboard and ensure DNS is configured before deploying.


Flow correctness checklist

These rules are load-bearing. Violating them causes the avatar to hallucinate, loop, double-speak, or fail silently. Every rule is the result of real debugging time across real integrations.

  • No conditional logic in single prompts -- use structural branching only
  • No "default" fallback outcomes on conversation nodes
  • doesNotRequireReply only on the final closing node
  • One question per conversation node
  • No self-loops: no outcome's targetNodeId equals its own node's id
  • The greeting is long enough for video accumulation (5 seconds or more of avatar speech)
  • All liveness and recognition outcomes route forward, never to end or an error state
  • Conversation prompts are authored in English — Say: for verbatim wording the avatar must speak, descriptive phrasing when paraphrase is acceptable. The runtime translates both into the session language.

Localization

Every FaceSign session runs through the same translation pipeline — there is no separate "English-only" mode. Whenever you expect users in more than one language, make sure the surrounding configuration matches:

  • langs whitelist set on session.create to the languages you want to support
  • defaultLang set on session.create so users whose browser language isn't in langs fall back predictably
  • UI strings on your landing and recap pages cover every language in langs
  • Currency, date, and number formats rendered through locale-aware APIs (Intl.NumberFormat, Intl.DateTimeFormat)
  • Smoke-tested in each target language by setting the browser's primary language accordingly

Webhook validation

Implement webhook handling

Verify that your webhook endpoint:

  • Accepts POST requests over HTTPS only
  • Returns 200 OK for every successfully received event, even if downstream processing fails
  • Returns 4xx for malformed payloads (prevents retries)
  • Returns 5xx for transient failures (triggers automatic retries)
  • Checks the sessionId against your records before processing
  • Uses the event id for idempotent processing to handle duplicate deliveries
Webhook handler
import crypto from 'node:crypto'

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) {
  // 1. Verify the HMAC signature against the raw body before
  //    trusting anything in the event.
  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)

  // 2. Verify the session belongs to you.
  const session = await db.sessions.find(event.sessionId)
  if (!session) {
    return Response.json({ error: 'Unknown session' }, { status: 400 })
  }

  // 3. Deduplicate on the event id.
  if (await db.webhookEvents.exists(event.id)) {
    return Response.json({ received: true })
  }

  // 4. Acknowledge immediately; do slow work on a queue.
  await queue.enqueue('facesign-webhook', event)
  await db.webhookEvents.insert({ id: event.id })
  return Response.json({ received: true })
}

Add error handling

Handle API errors gracefully in your integration:

  • Network failures -- Wrap API calls in try/catch and retry with exponential backoff for transient errors.
  • Rate limits -- Respect 429 responses. Back off and retry after the indicated delay.
  • Validation errors -- Log 400 responses with the error message. These indicate problems with your request payload (invalid flow structure, missing required fields).
  • Authentication errors -- A 401 response means your API key is invalid or expired. Surface this to your ops team, not to end users.

Never expose raw API error messages to end users. Map errors to user-friendly messages in your application.


Monitoring and logging

Set up monitoring

Configure alerts for:

  • Webhook delivery failures -- Track your endpoint's error rate. Alert if it exceeds 1% over a 5-minute window.
  • Session completion rate -- Monitor the ratio of complete to incomplete sessions (a session the user abandons before finishing the flow comes back as incomplete). A sudden drop in completion may indicate a frontend or flow issue.
  • API response times -- Alert on sustained latency above your SLA threshold.
  • Media download failures -- Media URLs expire after 15 minutes. Alert if your media download pipeline is falling behind.

Configure logging

Log webhook events, API responses, and session outcomes for debugging and audit purposes.

Safe fields to log:

  • Session IDs, event IDs, event types
  • Timestamps and status values
  • Node types and outcome keys (not transcript text)
  • Error codes and messages
  • Response latencies

Retry logic

Design your integration to handle transient failures:

  • API calls -- Retry on 5xx responses and network timeouts. Use exponential backoff with jitter (e.g., 1s, 2s, 4s + random).
  • Media downloads -- If a media URL download fails, re-fetch the session to get a fresh URL before the expiry window closes.
  • Webhook processing -- If your internal processing fails after acknowledging a webhook, enqueue the event for retry in your own job queue.
Retry with exponential backoff
async function fetchWithRetry(url, options, maxRetries = 3) {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      const res = await fetch(url, options)
      if (res.status >= 500 && attempt < maxRetries) {
        const delay = Math.pow(2, attempt) * 1000 + Math.random() * 500
        await new Promise(r => setTimeout(r, delay))
        continue
      }
      return res
    } catch (err) {
      if (attempt === maxRetries) throw err
      const delay = Math.pow(2, attempt) * 1000 + Math.random() * 500
      await new Promise(r => setTimeout(r, delay))
    }
  }
}

Security review

Before going live, verify:

  • API keys are stored in environment variables or a secrets manager, not in code
  • Client secrets are generated server-side and never exposed alongside your API key
  • Webhook endpoints use HTTPS only
  • Media files are downloaded to secure storage with access controls
  • CORS is configured to allow only your application's origin
  • Session data containing biometric information is handled according to your data retention policy

Compliance

  • Privacy and data retention settings match your obligations (GDPR, CCPA, BIPA as applicable)
  • Branding, copy, and the recap page match your product voice
  • Tested on desktop and mobile on the platforms you target

Remove test data

Clean up before launch:

  • Delete test sessions from your database that reference sandbox session IDs
  • Remove any hardcoded sk_test_ keys from configuration files
  • Clear cached sandbox responses from your development environment
  • Verify that your webhook endpoint is registered with the production URL, not a dev/ngrok URL

Quick reference

Both environments share the same API base URL (https://api.facesign.ai) and session host (https://session.facesign.ai). The API key prefix is what routes your requests into the right environment.

ItemSandboxProduction
API key prefixsk_test_sk_prod_
Rate limits100 req/min1,000 req/min
Data retention7 daysPer contract
Webhook retriesSame behaviorSame behavior

Next steps

On this page