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...Production keys have access to real user data. Rotate them immediately if they are ever exposed in logs, version control, or client-side code.
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.
cd my-nextjs-demo
printf "sk_prod_your_key_here" | npx vercel env add FACESIGN_API_KEY production
npx vercel --prod --yesFor static HTML deployments:
cd my-demo
npx vercel --prod --yesOr 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 productionConfigure 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 -
doesNotRequireReplyonly on the final closing node - One question per conversation node
- No self-loops: no outcome's
targetNodeIdequals its own node'sid - The greeting is long enough for video accumulation (5 seconds or more of avatar speech)
- All liveness and recognition outcomes route forward, never to
endor 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:
-
langswhitelist set onsession.createto the languages you want to support -
defaultLangset onsession.createso users whose browser language isn't inlangsfall 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
POSTrequests over HTTPS only - Returns
200 OKfor every successfully received event, even if downstream processing fails - Returns
4xxfor malformed payloads (prevents retries) - Returns
5xxfor transient failures (triggers automatic retries) - Checks the
sessionIdagainst your records before processing - Uses the event
idfor idempotent processing to handle duplicate deliveries
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
429responses. Back off and retry after the indicated delay. - Validation errors -- Log
400responses with the error message. These indicate problems with your request payload (invalid flow structure, missing required fields). - Authentication errors -- A
401response 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
completetoincompletesessions (a session the user abandons before finishing the flow comes back asincomplete). 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.
Never log PII. Do not log user photos, document images, face scan data, email addresses, phone numbers, or transcript content to general application logs. Store PII only in encrypted, access-controlled storage with appropriate retention policies.
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
5xxresponses 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.
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.
| Item | Sandbox | Production |
|---|---|---|
| API key prefix | sk_test_ | sk_prod_ |
| Rate limits | 100 req/min | 1,000 req/min |
| Data retention | 7 days | Per contract |
| Webhook retries | Same behavior | Same behavior |