> ## 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.

# 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

<Steps>
  <Step title="Replace API keys">
    Swap your sandbox key (`sk_test_...`) for a production key (`sk_live_...`). Store it in an environment variable or secrets manager -- never hard-code it in source files.

    ```bash theme={null}
    # Development
    FACESIGN_API_KEY=sk_test_abc123...

    # Production
    FACESIGN_API_KEY=sk_live_xyz789...
    ```

    <Warning>
      Production keys have access to real user data. Rotate them immediately if they are ever exposed in logs, version control, or client-side code.
    </Warning>
  </Step>

  <Step title="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.

    ```bash title="Vercel deployment" theme={null}
    cd my-nextjs-demo
    printf "sk_live_your_key_here" | npx vercel env add FACESIGN_API_KEY production
    npx vercel --prod --yes
    ```

    For static HTML deployments:

    ```bash title="Static deployment" theme={null}
    cd my-demo
    npx vercel --prod --yes
    ```

    Or drop the built folder on any static host.
  </Step>

  <Step title="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:

    ```bash theme={null}
    npx vercel env rm FACESIGN_API_KEY production --yes
    printf "sk_live_new_key" | npx vercel env add FACESIGN_API_KEY production
    ```
  </Step>

  <Step title="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.
  </Step>
</Steps>

***

## 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

<Steps>
  <Step title="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

    ```javascript title="Webhook handler" theme={null}
    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 })
    }
    ```
  </Step>

  <Step title="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.
  </Step>
</Steps>

***

## Monitoring and logging

<Steps>
  <Step title="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.
  </Step>

  <Step title="Configure logging">
    Log webhook events, API responses, and session outcomes for debugging and audit purposes.

    <Danger>
      **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.
    </Danger>

    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
  </Step>
</Steps>

***

## 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.

```javascript title="Retry with exponential backoff" wrap theme={null}
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_live_`    |
| Rate limits     | 100 req/min   | 1,000 req/min |
| Data retention  | 7 days        | Per contract  |
| Webhook retries | Same behavior | Same behavior |

## Next steps

<CardGroup cols={2}>
  <Card title="Webhook Events" href="/docs/reference/webhook-events">
    Full reference for all webhook event types
  </Card>

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

  <Card title="Authentication" href="/docs/reference/authentication">
    API key management and security
  </Card>
</CardGroup>
