Server definition
- Hash
- sha256:c6dd86dcf75ad644deb0518e6d443345d83800bcfc3ab263d21096516f3d34f5
- What it is
- What a remote MCP server returned when asked what it offers: 5 tools
The blob, as servednamed by its sha256
{
"instructions": "FaceSign MCP Server for identity verification. CRITICAL: You MUST call set_api_key BEFORE calling any other tool (launch_session_ui, get_session, list_sessions). If the user has not provided their FaceSign API key, ask them for it first. Do NOT call launch_session_ui or any other tool until set_api_key has been called successfully. The flow is a linear directed graph of nodes: START -> conversation/verification steps -> END. IMPORTANT: Flows must be linear with NO loops or cycles — a node must NEVER navigate back to a previous node. All paths must move forward toward an END node. After the API key is set, read the facesign://catalog resource to see available avatars and languages. DEFAULT CONFIGURATION:\n\nDefault avatar: June HR (ID: 65f9e3c9-d48b-4118-b73a-4ae2e3cbb8f0).\nUse this avatar when the user does not specify a preference.\n Use launch_session_ui to open a local web page where the user can start and complete the verification session interactively — a new session is created each time the user clicks Start, so page refreshes work correctly. Use get_session and list_sessions to review results. Read the facesign://node-types resource for detailed reference on all node types. Read the facesign://flow-best-practices resource before building any new flow: it covers the English-only prompt rule, one-question-per-node, greeting 5+ seconds, no avatar self-introduction, no narration of invisible steps, node-selection patterns per scenario (access control / SCA / KYC / recovery), a recognition branching template, and a complete multilingual example. DISCOVERY INTERVIEW: When the user is describing a NEW flow for the first time (not an iteration or tweak), read the facesign://interview-guidance resource and conduct the short discovery interview it describes before calling launch_session_ui. Skip the interview entirely for iterations on an existing flow or when the user explicitly asks for a quick default. LANDING AND RECAP PAGES ARE YOUR RESPONSIBILITY: launch_session_ui and export_app no longer ship default chrome. On every call you MUST supply `landingHtml` (pre-session), `recapHtml` (post-session), and `uiStrings` (per-language dictionary whose keys YOU invent to match placeholders in the HTML). This applies even to minimal test cases — there is no 'just run it without customization' path. LANGUAGE DEFAULTS: By default, leave `langs` and `defaultLang` UNSET on every call — the session then supports every language in facesign://catalog and the user's browser language drives the UI. ONLY set these two parameters when the user EXPLICITLY restricts the language set (e.g. 'Spanish-only', 'English and Russian, default to English'). Do NOT narrow `langs` to `['en']` simply because the user described the demo in English or didn't mention languages — that silently strips multilingual support. When in doubt, omit both parameters. UI TRANSLATIONS: The target language set for `uiStrings` is derived from the session config: if `langs` is provided, `uiStrings` must cover every language in that list; if `langs` is omitted (the default), `uiStrings` must cover every language in the facesign://catalog resource. `en` is always required (ultimate runtime fallback). Every per-language dict MUST share the identical key set — if `en` has keys A, B, C then every other language's dict must also have A, B, C with translated values. The tool handler validates this strictly and returns a specific error listing missing keys/languages; when you see such an error, fix the gaps and retry. Reference strings in HTML via {{KEY}} placeholders (interpolated at inject time) or via window.t('KEY') calls in your <script> blocks (dynamic at runtime). Both resolve against the user's browser language, falling back through navigator.language → catalog match → `en` → the key literal. The `?lang=XX` URL query param overrides navigator.language and is forwarded to the session iframe. URL QUERY PARAMS: Any query parameters on the wrapper page URL are forwarded automatically to the session iframe src, so the FaceSign backend can read them (e.g. ?utm_source=..., ?ref=..., ?userId=...). Pre-existing params on the API-supplied session URL (tokens, etc.) are never overwritten. AVATAR LANGUAGE: `langs` (when set) is a whitelist of language codes (from facesign://catalog) the avatar is allowed to speak. The wrapper resolves the end-user's language (navigator.language or ?lang=), normalizes it (case-insensitive, aliases like zh-Hant→zh-TW, base-language fallback fr-FR→fr), and if it matches the allowlist, sets it as the session `defaultLang`. Otherwise the developer's `defaultLang` is used. Author flow prompts and outcome conditions in ENGLISH only. The FaceSign runtime generates the user-facing response in the session language regardless of the prompt language, but mixing languages in prompts breaks consistency (e.g. `Say: <French>` on one node and descriptive English on another causes the avatar to flip languages mid-flow). See the facesign://flow-best-practices resource for the full language rule and other prompt-writing rules. When the user does not mention languages, OMIT both `langs` and `defaultLang` so every catalog language is supported. When the user says e.g. 'make it Spanish-only', set langs=['es'] and defaultLang='es'; when they say 'support English and Russian, default to English', set langs=['en','ru'] and defaultLang='en'. LANDING HTML CONTRACT: window.__FACESIGN_FLOW__ (the flow array, may be read/modified); window.__startSession(input?) — call this to launch the session; exported apps accept a declared flowId and/or providedData, never a browser-supplied graph; window.t(key) and {{KEY}} placeholders for translations. The FaceSign session iframe has its own Start button that captures the user gesture for audio/video autoplay — the landing page does NOT need a button for that purpose; call window.__startSession() when your form submission / selection is ready. RECAP HTML CONTRACT: window.__FACESIGN_SESSION__ (session data, updated on each poll); window.__FACESIGN_SESSION_ID__; window.__refetchSession() returns Promise with fresh data; window.t(key) and {{KEY}} placeholders. Session data arrives in two stages — some fields appear immediately (user info, AI analysis, transcript), others (videoAIAnalysis, media.avatarVideo, media.screenshots, node reports) are delayed. Use videoAIAnalysisStatus to poll video analysis: pending means keep polling; succeeded and failed are terminal; absent means it was not requested. Render static sections once and update dynamic ones via polling. RECAP REFERENCE (DEMO MODE): When generating recapHtml for a flow Phase 0 identified as a 'demo for internal stakeholders', read the facesign://recap-reference resource for three visual demo-polish components (outcome banner, confidence bars, raw JSON toggle) that sit on top of the standard polling architecture. Skip this resource for production thank-you or redirect recaps — those are minimal closing pages, not data dashboards. EXPORT: Use export_app to generate a standalone, deployable Next.js application. It takes the same landingHtml / recapHtml / uiStrings inputs as launch_session_ui. export_app does NOT require set_api_key — the API key is configured in the exported app's .env.local file. For the complete session data type reference (used by recapHtml), read the facesign://session-data-types resource. OUTPUT FORMATTING: When confirming a built flow to the user, describe what the end user will experience and which choices THEY made (avatar, OTP settings, document types, compliance add-ons). Do NOT parrot internal FaceSign implementation details as 'features' of their specific flow — CSS class prefixes (lp-/r-), polling intervals/limits, session-data staging (static vs dynamic), catalog language auto-coverage, uiStrings key lists, class-naming conventions, Microblink extractor helpers, and framework names (Preact/Express/Next.js) are the same across every flow and add noise. Pick the few facts that are specific to THIS flow and present those.",
"tools": [
{
"description": "Export the current FaceSign session configuration as a standalone, deployable Next.js application. You MUST supply `landingHtml` (pre-session page), `recapHtml` (results page), and `uiStrings` (per-language dictionary whose keys are invented by you to match placeholders in the HTML). The exported app ships NO default chrome — every visible string comes from you. Run with `npm install && npm run dev` or deploy to Vercel after setting FACESIGN_API_KEY.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"appName": {
"description": "Name for the generated app (used in package.json, defaults to 'facesign-app')",
"type": "string"
},
"avatarId": {
"description": "Avatar ID from the facesign://catalog resource.",
"type": "string"
},
"clientReferenceId": {
"description": "Your own reference ID for this session",
"type": "string"
},
"customization": {
"additionalProperties": false,
"description": "UI customization",
"properties": {
"controls": {
"additionalProperties": false,
"properties": {
"autoHide": {
"description": "Whether controls may automatically fade out. Omitted or true preserves the default behavior; false keeps the selected buttons visible. Ignored when showUxControls is false",
"type": "boolean"
},
"buttons": {
"description": "Optional exact subset of in-session control buttons. Omit this field for the recommended default panel with all applicable controls. Set it only when the user requests a specialized flow or UI; for example, [\"captions\"] creates a captions-only panel, while an empty array hides the panel. Ignored when showUxControls is false",
"items": {
"enum": [
"microphone",
"camera",
"captions",
"language",
"close"
],
"type": "string"
},
"type": "array"
},
"captionsOpenedByDefault": {
"description": "Start the session with the closed-captions (transcript) panel open. Omitted or false keeps it closed. The user can toggle it with the CC button either way. Set it for accessibility, or when viewers may be reading along in a second language. Ignored when showUxControls is false",
"type": "boolean"
},
"showUxControls": {
"description": "Master switch for the in-session control panel. When false the panel is not rendered and every other controls.* setting has no effect",
"type": "boolean"
}
},
"type": "object"
},
"permissionsPage": {
"additionalProperties": false,
"properties": {
"backgroundColor": {
"type": "string"
},
"backgroundType": {
"enum": [
"AVATAR",
"COLOR"
],
"type": "string"
},
"buttonText": {
"type": "string"
},
"buttonTextTranslates": {
"additionalProperties": {
"type": "string"
},
"description": "Localized permission-button text, for example { es: 'Continuar' }",
"type": "object"
},
"mainHeading": {
"type": "string"
},
"mainHeadingTranslates": {
"$ref": "#/properties/customization/properties/permissionsPage/properties/buttonTextTranslates",
"description": "Localized permission-page headings"
},
"subheading": {
"type": "string"
},
"subheadingTranslates": {
"$ref": "#/properties/customization/properties/permissionsPage/properties/buttonTextTranslates",
"description": "Localized permission-page subheadings"
}
},
"type": "object"
}
},
"type": "object"
},
"defaultLang": {
"description": "Optional fallback BCP-47 language code used when the end-user's browser language is not in `langs`. DEFAULT BEHAVIOUR: OMIT this parameter. When set, must be one of the codes in `langs` (if `langs` is also set).",
"type": "string"
},
"extractionSchema": {
"description": "Optional schema describing fields the FaceSign backend should extract from the session transcript using an LLM. Results are populated post-session at `session.report.extractedData` as `{ [fieldName]: string | number | boolean | null }` (null when the transcript did not contain the data). Render results in your recapHtml when present.",
"items": {
"additionalProperties": false,
"properties": {
"description": {
"description": "Natural-language hint for the LLM describing what to look for in the transcript. This is the primary extraction signal — be specific.",
"type": "string"
},
"enum": {
"description": "Optional allowed string values. When set, the LLM normalizes free-form answers into one of the listed values.",
"items": {
"type": "string"
},
"type": "array"
},
"fieldName": {
"description": "Key under which the extracted value appears in session.report.extractedData.",
"type": "string"
},
"type": {
"description": "Expected value type. `date` is returned as an ISO 8601 string.",
"enum": [
"string",
"number",
"boolean",
"date"
],
"type": "string"
}
},
"required": [
"fieldName",
"type",
"description"
],
"type": "object"
},
"type": "array"
},
"flow": {
"description": "Array of nodes forming the session's directed graph",
"items": {
"anyOf": [
{
"additionalProperties": false,
"properties": {
"id": {
"description": "Unique node identifier",
"type": "string"
},
"outcome": {
"description": "The id of the next node",
"type": "string"
},
"type": {
"const": "start",
"type": "string"
}
},
"required": [
"id",
"type",
"outcome"
],
"type": "object"
},
{
"additionalProperties": false,
"properties": {
"id": {
"description": "Unique node identifier",
"type": "string"
},
"type": {
"const": "end",
"type": "string"
}
},
"required": [
"id",
"type"
],
"type": "object"
},
{
"additionalProperties": false,
"properties": {
"awaitExternal": {
"additionalProperties": false,
"description": "Optional background poll: the avatar keeps talking (normal outcomes stay active) while an external result becomes ready; exits via onDataReady/onTimeout/onError",
"properties": {
"exits": {
"additionalProperties": false,
"description": "Where to route when the background poll resolves",
"properties": {
"onDataReady": {
"description": "Node id when the endpoint returns HTTP 200 (result ready)",
"type": "string"
},
"onError": {
"description": "Node id when the call errors",
"type": "string"
},
"onTimeout": {
"description": "Node id when poll.timeoutMs elapses first",
"type": "string"
}
},
"required": [
"onDataReady",
"onTimeout",
"onError"
],
"type": "object"
},
"poll": {
"additionalProperties": false,
"description": "Polling cadence and overall timeout",
"properties": {
"intervalMs": {
"description": "Delay between polls in milliseconds",
"type": "number"
},
"timeoutMs": {
"description": "Total time to keep polling before giving up",
"type": "number"
}
},
"required": [
"intervalMs",
"timeoutMs"
],
"type": "object"
},
"request": {
"additionalProperties": false,
"description": "The endpoint to poll in the background",
"properties": {
"method": {
"description": "HTTP method. Defaults to POST (JSON body); GET sends vars as query params",
"enum": [
"GET",
"POST"
],
"type": "string"
},
"url": {
"description": "Endpoint to call (must pass the server-side allowlist). The current sessionId is appended automatically as a query parameter",
"type": "string"
},
"vars": {
"description": "Payload variables, each resolving to a literal or a session value",
"items": {
"additionalProperties": false,
"properties": {
"name": {
"description": "Key under which the value is placed in the request payload",
"type": "string"
},
"value": {
"anyOf": [
{
"additionalProperties": false,
"properties": {
"type": {
"const": "literal",
"type": "string"
},
"value": {
"description": "The literal scalar value",
"type": [
"string",
"number",
"boolean"
]
}
},
"required": [
"type",
"value"
],
"type": "object"
},
{
"additionalProperties": false,
"properties": {
"field": {
"description": "The field/key name within the source",
"type": "string"
},
"source": {
"description": "Source to resolve the referenced value from",
"enum": [
"extractedData",
"providedData"
],
"type": "string"
},
"type": {
"const": "ref",
"type": "string"
}
},
"required": [
"type",
"source",
"field"
],
"type": "object"
}
],
"description": "Literal value or a reference resolved from session data"
}
},
"required": [
"name",
"value"
],
"type": "object"
},
"type": "array"
}
},
"required": [
"url"
],
"type": "object"
}
},
"required": [
"request",
"poll",
"exits"
],
"type": "object"
},
"doesNotRequireReply": {
"description": "If true, avatar speaks without waiting for user reply",
"type": "boolean"
},
"id": {
"description": "Unique node identifier",
"type": "string"
},
"outcomes": {
"description": "Array of {id, targetNodeId, condition} routing entries",
"items": {
"additionalProperties": false,
"properties": {
"condition": {
"description": "Condition description, e.g. \"default\", \"user says yes\"",
"type": "string"
},
"id": {
"type": "string"
},
"targetNodeId": {
"description": "The id of the node to route to",
"type": "string"
}
},
"required": [
"id",
"targetNodeId",
"condition"
],
"type": "object"
},
"minItems": 1,
"type": "array"
},
"prompt": {
"description": "What the avatar says to the user",
"type": "string"
},
"type": {
"const": "conversation",
"type": "string"
}
},
"required": [
"id",
"type",
"prompt",
"outcomes"
],
"type": "object"
},
{
"additionalProperties": false,
"properties": {
"id": {
"description": "Unique node identifier",
"type": "string"
},
"outcomes": {
"additionalProperties": false,
"description": "Record mapping each outcome to a target node id. All four are required",
"properties": {
"deepfakeDetected": {
"description": "Node id when deepfake is detected",
"type": "string"
},
"inconclusive": {
"description": "Node id when the check produced no verdict — the detector could not decide, did not run, or there was nothing to analyse. This is our failure, not the user's: route it to a retry, a step-up or a manual review, never to the happy path by default",
"type": "string"
},
"livenessDetected": {
"description": "Node id when liveness is confirmed",
"type": "string"
},
"noFace": {
"description": "Node id when frames were captured but no face is on them",
"type": "string"
}
},
"required": [
"livenessDetected",
"deepfakeDetected",
"noFace",
"inconclusive"
],
"type": "object"
},
"type": {
"const": "liveness_detection",
"type": "string"
}
},
"required": [
"id",
"type",
"outcomes"
],
"type": "object"
},
{
"additionalProperties": false,
"properties": {
"id": {
"description": "Unique node identifier",
"type": "string"
},
"outcomes": {
"additionalProperties": false,
"description": "Record mapping each outcome to a target node id",
"properties": {
"canceled": {
"description": "Node id when user cancels",
"type": "string"
},
"emailEntered": {
"description": "Node id when email is provided",
"type": "string"
}
},
"required": [
"emailEntered",
"canceled"
],
"type": "object"
},
"prompt": {
"description": "Optional phrase the avatar says at the moment the email input field appears (direct speech, e.g. \"Could you please enter your email address?\"). Keep it short — one sentence. Omit to let the avatar stay silent while the field is shown.",
"type": "string"
},
"type": {
"const": "enter_email",
"type": "string"
}
},
"required": [
"id",
"type",
"outcomes"
],
"type": "object"
},
{
"additionalProperties": false,
"properties": {
"id": {
"description": "Unique node identifier",
"type": "string"
},
"outcomes": {
"description": "Array of {id, targetNodeId, condition} routing entries",
"items": {
"$ref": "#/properties/flow/items/anyOf/2/properties/outcomes/items"
},
"minItems": 1,
"type": "array"
},
"type": {
"const": "data_validation",
"type": "string"
},
"validation": {
"additionalProperties": false,
"properties": {
"action": {
"type": "string"
},
"field": {
"type": "string"
},
"value": {
"type": "string"
}
},
"required": [
"field",
"action"
],
"type": "object"
}
},
"required": [
"id",
"type",
"validation",
"outcomes"
],
"type": "object"
},
{
"additionalProperties": false,
"properties": {
"allowedDocumentTypes": {
"description": "Allowed document types, e.g. [\"id\", \"passport\", \"dl\"]",
"items": {
"type": "string"
},
"minItems": 1,
"type": "array"
},
"id": {
"description": "Unique node identifier",
"type": "string"
},
"outcomes": {
"additionalProperties": false,
"description": "Record mapping each outcome to a target node id",
"properties": {
"scanSuccess": {
"description": "Node id on successful scan",
"type": "string"
},
"scanTimeout": {
"description": "Node id on scan timeout",
"type": "string"
},
"userCancelled": {
"description": "Node id when user cancels",
"type": "string"
}
},
"required": [
"scanSuccess",
"userCancelled",
"scanTimeout"
],
"type": "object"
},
"scanningMode": {
"description": "Scanning mode",
"enum": [
"single",
"automatic"
],
"type": "string"
},
"type": {
"const": "document_scan",
"type": "string"
}
},
"required": [
"id",
"type",
"scanningMode",
"allowedDocumentTypes",
"outcomes"
],
"type": "object"
},
{
"additionalProperties": false,
"properties": {
"id": {
"description": "Unique node identifier",
"type": "string"
},
"outcomes": {
"additionalProperties": false,
"description": "Record mapping each outcome to a target node id",
"properties": {
"newUser": {
"description": "Node id when user is new",
"type": "string"
},
"noFace": {
"description": "Node id when no face is found",
"type": "string"
},
"recognized": {
"description": "Node id when user is recognized",
"type": "string"
}
},
"required": [
"recognized",
"newUser",
"noFace"
],
"type": "object"
},
"type": {
"const": "recognition",
"type": "string"
}
},
"required": [
"id",
"type",
"outcomes"
],
"type": "object"
},
{
"additionalProperties": false,
"properties": {
"captureInstructions": {
"type": "string"
},
"id": {
"description": "Unique node identifier",
"type": "string"
},
"outcomes": {
"additionalProperties": false,
"description": "Record mapping each outcome to a target node id",
"properties": {
"cancelled": {
"description": "Node id when user cancels",
"type": "string"
},
"error": {
"description": "Node id on error",
"type": "string"
},
"notPassed": {
"description": "Node id when face scan fails",
"type": "string"
},
"passed": {
"description": "Node id when face scan passes",
"type": "string"
}
},
"required": [
"passed",
"notPassed",
"cancelled",
"error"
],
"type": "object"
},
"requireAILivenessCheck": {
"type": "boolean"
},
"requireLivenessChallenge": {
"type": "boolean"
},
"similarityThreshold": {
"description": "0-1, how similar faces must be",
"type": "number"
},
"type": {
"const": "face_scan",
"type": "string"
}
},
"required": [
"id",
"type",
"outcomes"
],
"type": "object"
},
{
"additionalProperties": false,
"properties": {
"emailTemplate": {
"type": "string"
},
"expirySeconds": {
"description": "OTP expiry in seconds, default 300",
"type": "number"
},
"id": {
"description": "Unique node identifier",
"type": "string"
},
"maxAttempts": {
"description": "Max verification attempts, default 3",
"type": "number"
},
"otpLength": {
"description": "OTP length 4-8, default 6",
"type": "number"
},
"outcomes": {
"additionalProperties": false,
"description": "Record mapping each outcome to a target node id",
"properties": {
"cancelled": {
"description": "Node id when user cancels",
"type": "string"
},
"delivery_failed": {
"description": "Node id when delivery fails",
"type": "string"
},
"error": {
"description": "Node id on error",
"type": "string"
},
"failed_unverified": {
"description": "Node id when max attempts exhausted",
"type": "string"
},
"verified": {
"description": "Node id when OTP verified",
"type": "string"
}
},
"required": [
"verified",
"delivery_failed",
"failed_unverified",
"cancelled",
"error"
],
"type": "object"
},
"resendAfterSeconds": {
"description": "Seconds before allowing OTP resend",
"type": "number"
},
"showUI": {
"description": "Whether to show the OTP input UI",
"type": "boolean"
},
"type": {
"const": "two_factor_email",
"type": "string"
}
},
"required": [
"id",
"type",
"outcomes"
],
"type": "object"
},
{
"additionalProperties": false,
"properties": {
"expirySeconds": {
"description": "OTP expiry in seconds, default 300",
"type": "number"
},
"id": {
"description": "Unique node identifier",
"type": "string"
},
"maxAttempts": {
"description": "Max verification attempts, default 3",
"type": "number"
},
"otpLength": {
"description": "OTP length 4-8, default 6",
"type": "number"
},
"outcomes": {
"additionalProperties": false,
"description": "Record mapping each outcome to a target node id",
"properties": {
"cancelled": {
"description": "Node id when user cancels",
"type": "string"
},
"delivery_failed": {
"description": "Node id when delivery fails",
"type": "string"
},
"error": {
"description": "Node id on error",
"type": "string"
},
"failed_unverified": {
"description": "Node id when max attempts exhausted",
"type": "string"
},
"verified": {
"description": "Node id when OTP verified",
"type": "string"
}
},
"required": [
"verified",
"delivery_failed",
"failed_unverified",
"cancelled",
"error"
],
"type": "object"
},
"resendAfterSeconds": {
"description": "Seconds before allowing OTP resend",
"type": "number"
},
"showUI": {
"description": "Whether to show the OTP input UI",
"type": "boolean"
},
"smsTemplate": {
"type": "string"
},
"type": {
"const": "two_factor_sms",
"type": "string"
}
},
"required": [
"id",
"type",
"outcomes"
],
"type": "object"
},
{
"additionalProperties": false,
"properties": {
"id": {
"description": "Unique node identifier",
"type": "string"
},
"outcomes": {
"additionalProperties": false,
"description": "Record mapping each outcome to a target node id",
"properties": {
"permissionsDenied": {
"description": "Node id when permissions are denied",
"type": "string"
},
"permissionsGranted": {
"description": "Node id when permissions are granted",
"type": "string"
}
},
"required": [
"permissionsGranted",
"permissionsDenied"
],
"type": "object"
},
"permissions": {
"additionalProperties": false,
"description": "Which permissions to request",
"properties": {
"camera": {
"description": "Request camera access",
"type": "boolean"
},
"microphone": {
"description": "Request microphone access",
"type": "boolean"
}
},
"type": "object"
},
"prompt": {
"description": "Optional message the avatar says (direct speech mode, e.g. 'Say: Please enable your camera.')",
"type": "string"
},
"type": {
"const": "permissions",
"type": "string"
}
},
"required": [
"id",
"type",
"permissions",
"outcomes"
],
"type": "object"
},
{
"additionalProperties": false,
"properties": {
"id": {
"description": "Unique node identifier",
"type": "string"
},
"outcomes": {
"additionalProperties": false,
"description": "Record mapping each outcome to a target node id",
"properties": {
"imageUnavailable": {
"description": "Node id when one or both images could not be obtained",
"type": "string"
},
"match": {
"description": "Node id when faces match",
"type": "string"
},
"noMatch": {
"description": "Node id when faces do not match",
"type": "string"
}
},
"required": [
"match",
"noMatch",
"imageUnavailable"
],
"type": "object"
},
"similarityThreshold": {
"description": "0-1, minimum similarity score required for a match",
"type": "number"
},
"sourceA": {
"additionalProperties": false,
"description": "First face image source",
"properties": {
"providedDataKey": {
"description": "Required when source is 'providedData' — the key in providedData that holds the image URL",
"type": "string"
},
"source": {
"description": "Source of the face image",
"enum": [
"sessionVideo",
"faceScan",
"providedData",
"documentPhoto"
],
"type": "string"
}
},
"required": [
"source"
],
"type": "object"
},
"sourceB": {
"additionalProperties": false,
"description": "Second face image source",
"properties": {
"providedDataKey": {
"$ref": "#/properties/flow/items/anyOf/12/properties/sourceA/properties/providedDataKey"
},
"source": {
"$ref": "#/properties/flow/items/anyOf/12/properties/sourceA/properties/source"
}
},
"required": [
"source"
],
"type": "object"
},
"type": {
"const": "face_compare",
"type": "string"
}
},
"required": [
"id",
"type",
"sourceA",
"sourceB",
"outcomes"
],
"type": "object"
},
{
"additionalProperties": false,
"properties": {
"id": {
"description": "Unique node identifier",
"type": "string"
},
"outcome": {
"description": "Node id to advance to after the response is merged into providedData (never branches)",
"type": "string"
},
"request": {
"additionalProperties": false,
"description": "The external endpoint to call and the payload to send",
"properties": {
"method": {
"$ref": "#/properties/flow/items/anyOf/2/properties/awaitExternal/properties/request/properties/method"
},
"url": {
"$ref": "#/properties/flow/items/anyOf/2/properties/awaitExternal/properties/request/properties/url"
},
"vars": {
"$ref": "#/properties/flow/items/anyOf/2/properties/awaitExternal/properties/request/properties/vars"
}
},
"required": [
"url"
],
"type": "object"
},
"type": {
"const": "external_call",
"type": "string"
}
},
"required": [
"id",
"type",
"request",
"outcome"
],
"type": "object"
},
{
"additionalProperties": false,
"properties": {
"default": {
"description": "Node id to route to when no rule matches",
"type": "string"
},
"id": {
"description": "Unique node identifier",
"type": "string"
},
"rules": {
"description": "Rules evaluated top-to-bottom; first match wins",
"items": {
"additionalProperties": false,
"properties": {
"left": {
"additionalProperties": false,
"description": "Reference to the session value to test",
"properties": {
"field": {
"description": "The field/key name within the source",
"type": "string"
},
"source": {
"description": "Where the value is read from: 'extractedData' (a field from extractionSchema, extracted on demand from the transcript) or 'providedData' (a key set at creation or written by a prior external_call)",
"enum": [
"extractedData",
"providedData"
],
"type": "string"
}
},
"required": [
"source",
"field"
],
"type": "object"
},
"operator": {
"description": "Comparison operator (omit 'right' for 'exists'/'notExists')",
"enum": [
"equals",
"notEquals",
"gt",
"lt",
"gte",
"lte",
"exists",
"notExists"
],
"type": "string"
},
"outcome": {
"description": "Node id to route to when this rule matches",
"type": "string"
},
"right": {
"anyOf": [
{
"$ref": "#/properties/flow/items/anyOf/2/properties/awaitExternal/properties/request/properties/vars/items/properties/value/anyOf/0"
},
{
"$ref": "#/properties/flow/items/anyOf/2/properties/awaitExternal/properties/request/properties/vars/items/properties/value/anyOf/1"
}
],
"description": "Literal or reference to compare against; omit for 'exists'/'notExists'"
}
},
"required": [
"left",
"operator",
"outcome"
],
"type": "object"
},
"minItems": 1,
"type": "array"
},
"type": {
"const": "field_condition",
"type": "string"
}
},
"required": [
"id",
"type",
"rules",
"default"
],
"type": "object"
}
]
},
"type": "array"
},
"flowVariants": {
"additionalProperties": {
"items": {
"$ref": "#/properties/flow/items"
},
"type": "array"
},
"description": "Optional additional flows declared at export time. The landing page selects one by calling window.__startSession({ flowId: 'variant-id' }); the browser never sends a graph.",
"propertyNames": {
"maxLength": 64,
"minLength": 1,
"pattern": "^[A-Za-z0-9_-]+$"
},
"type": "object"
},
"landingHtml": {
"description": "REQUIRED. Raw HTML/JS for the pre-session landing page. NO CDATA, NO markdown fences. Every visible string MUST come from `uiStrings` — reference them via {{KEY}} placeholders (interpolated at inject time) or via window.t('KEY') (dynamic at runtime). Contract: reads window.__FACESIGN_FLOW__ (the default flow, for display only). MUST call window.__startSession(input?) to launch the session. input may contain only a declared flowId and/or providedData collected by the form; never pass a flow graph from the browser.\n\nSTYLE GUIDE (Pre-Session Page):\nMatch this visual style for consistency with the FaceSign UI.\n\nBackground: Light gray #f5f7fa. Full-viewport centered layout (flexbox, min-height: 100vh; min-height: 100dvh — always use dvh with vh fallback for iOS compatibility).\n\nCard: White, border-radius 16px, box-shadow 0 4px 24px rgba(0,0,0,.08), padding 2.5-3rem, max-width 480-600px, centered.\n\nHeadings: Bold, 1.4-1.8rem, color #1a1a2e, centered in card.\n\nInputs: Full-width, padding 14px 16px, border 2px solid #d1d5db, border-radius 12px, font-size 1rem. Focus: border-color #5b7bab. Placeholder color #9ca3af.\n\nPrimary buttons: Background #7b8fb5 (steel-blue), color white, font-weight 600, font-size 1.05rem, border-radius 50px (pill shape), padding 14px, full-width in card. Hover: background #6a7fa5. Disabled: background #c5cdd8, cursor not-allowed.\n\nLinks: Color #4573b8, no underline, underline on hover. Back navigation: \"← Back\" at top-left of card, color #4573b8, font-size 0.95rem.\n\nProfile images: Circular (border-radius 50%), 120-150px diameter, centered, subtle box-shadow.\n\nSpacing: 2.5-3rem card padding, 1.5rem between form groups, 1rem between label and input, 2rem above primary button.\n\nFont: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif.\n\n\nCOMMON MISTAKES TO AVOID (UI/CSS):\n\n1. Never set padding or margin on body\nThe custom HTML is injected into a host page that controls its own layout. Setting padding, margin, or min-height on body will conflict with the host page styles and create unwanted spacing.\n\nWrong:\nbody { padding: 2rem; min-height: 100vh; }\n(Also wrong — using only 100vh without dvh fallback. Always add min-height: 100dvh after 100vh for iOS Safari.)\n\nRight — use a wrapper element instead:\nbody { margin: 0; padding: 0; }\n.wrap { max-width: 860px; margin: 0 auto; padding: 2rem; }\n\nThe same applies to background on body — avoid it unless you are certain the host page does not set its own background.\n\n2. Use HTML entities for emoji, not JS unicode escapes\nWhen building HTML strings in JavaScript (via innerHTML, string concatenation, etc.), JS unicode escapes like \\ud83c\\udfa5 will NOT render as emoji. They only work inside JS string literals that are directly displayed via textContent or similar APIs.\n\nWrong — renders as garbled text:\nh += '<div class=\"title\">\\ud83c\\udfa5 Video AI Analysis</div>';\n\nRight — use HTML numeric entities:\nh += '<div class=\"title\">🎥 Video AI Analysis</div>';\n\nAlso right — use emoji directly in static HTML (outside of JS):\n<div class=\"title\">🎥 Video AI Analysis</div>\n\nCommon emoji HTML entities reference:\n👤 👤 User/person\n🎂 🎂 Birthday cake\n📍 📍 Pin/location\n💻 💻 Computer\n✨ ✨ Sparkles\n🔍 🔍 Search\n🎥 🎥 Camera\n🎬 🎬 Clapper board\n📋 📋 Clipboard\n⚧ ⚧ Gender symbol\n✓ ✓ Checkmark\n\n3. CSS class name collision between landingHtml and recapHtml\nCRITICAL: landingHtml and recapHtml are injected into the SAME host document. If both fragments use the same class names (e.g., .card, .container, .header), styles from one will leak into the other, causing layout breakage.\n\nALWAYS use unique prefixed class names:\n- landingHtml (pre-session/landing page): prefix all classes with .lp- (e.g., .lp-card, .lp-header, .lp-btn)\n- recapHtml (results page): prefix all classes with .r- (e.g., .r-card, .r-header, .r-wrap)\n\nWrong — causes collisions:\n/* in landingHtml */ .card { max-width: 460px; }\n/* in recapHtml */ .card { max-width: 900px; }\n\nRight — namespaced:\n/* in landingHtml */ .lp-card { max-width: 460px; }\n/* in recapHtml */ .r-card { max-width: 900px; }\n\n\nCOMMON MISTAKES TO AVOID (Code/API):\n\n4. window.__startSession race condition\nNEVER call window.__startSession() synchronously in a click handler. The host page may not have injected the function yet. ALWAYS use polling:\n\nfunction waitAndStart(input) {\n var n = 0;\n var iv = setInterval(function() {\n n++;\n if (typeof window.__startSession === 'function') {\n clearInterval(iv);\n window.__startSession(input);\n } else if (n > 100) {\n clearInterval(iv);\n document.body.innerHTML = '<p style=\"text-align:center;padding:2rem;color:red;\">Failed to initialize session. Please refresh.</p>';\n }\n }, 100);\n}\n\nWrong:\nstartBtn.addEventListener('click', function() { window.__startSession(); });\n\nRight:\nstartBtn.addEventListener('click', function() { waitAndStart(); });\n\nFor exported apps, NEVER send window.__FACESIGN_FLOW__ or another graph back to\nthe server. To select a flow declared in export_app, pass its ID:\nwaitAndStart({ flowId: 'enhanced', providedData: { name: nameInput.value } });\n\n5. Permissions are automatic — do NOT add a permissions node by default\nFaceSign automatically requests camera and microphone permissions at session start. Most flows do NOT need a PERMISSIONS node. Only add one in these specific cases:\n(a) The user wants to request microphone and camera permissions separately at different times during the flow (instead of both at once at session start).\n(b) The user wants to move the permission request to the initial page (landingHtml), outside the FaceSign flow itself.\n\n6. nodeReport.type values are lowercase snake_case\nThe API returns nodeReport.type in lowercase snake_case: \"face_compare\", \"document_scan\", \"conversation\", \"permissions\", \"liveness_detection\", etc. — NOT uppercase like \"FACE_COMPARE\".\n\nALWAYS use case-insensitive comparison when looking up node reports:\n\nfunction getNode(r, type) {\n var rr = (r && r.nodeReports) || [];\n var tl = type.toLowerCase();\n for (var i = 0; i < rr.length; i++) {\n if (rr[i].type && rr[i].type.toLowerCase() === tl) return rr[i];\n }\n return null;\n}\n\n// Usage: getNode(report, 'face_compare'), getNode(report, 'document_scan')\n\n7. Microblink document report — nested field structure\nDOCUMENT_SCAN nodeReport.report fields are NOT plain strings. They use Microblink's nested structure:\n firstName: { latin: { value: \"JANICE\" } }\n dateOfBirth: { originalString: { latin: { value: \"04/30/1970\" } } }\n — OR —\n dateOfBirth: { day: 30, month: 4, year: 1970 }\n\nNEVER read fields directly as strings (e.g., dr.firstName will be an object, not \"JANICE\").\n\nALWAYS use this safe-value extractor:\n\nfunction sv(v) {\n if (v == null) return '';\n if (typeof v === 'string') return v;\n if (v.latin && v.latin.value != null) return String(v.latin.value);\n if (v.originalString) return sv(v.originalString);\n if (v.day != null && v.month != null && v.year != null)\n return v.month + '/' + v.day + '/' + v.year;\n return '';\n}\n\n// Usage: sv(dr.firstName) → \"JANICE\", sv(dr.dateOfBirth) → \"04/30/1970\"\n\n8. videoAIAnalysis criterion is camelCase — format for display\nThe criterion field (e.g., \"facialExpressionAndMovement\", \"useOfExternalDevices\") comes in camelCase. Convert to human-readable format:\n\nfunction toStartCase(s) {\n if (!s) return '';\n return s.replace(/([A-Z]+)/g, function(m) { return ' ' + m.toLowerCase(); })\n .trim().replace(/^./, function(c) { return c.toUpperCase(); });\n}\n// \"facialExpressionAndMovement\" → \"Facial expression and movement\"\n\n9. Conversation node: condition vs prompt\nIn CONVERSATION nodes:\n- \"condition\" (in outcomes) = ONLY describes the trigger event for transitioning to the next node (e.g., \"User explicitly agrees to proceed\")\n- \"prompt\" = ALL instructions for the avatar's behavior, including how to greet, how to respond to questions, how to handle objections, and how to persuade\n- User questions or objections during the conversation are handled within the SAME node (continued dialog), NOT via separate outcomes\n- Conversation nodes can have any number of outcomes depending on the use case:\n * Branching nodes (e.g., \"what color?\") → one outcome per branch + a fallback\n * Consent/agreement nodes → typically 2: agreement + fallback after N attempts\n- ALWAYS include a fallback outcome for when the conversation stalls (e.g., \"conversation exceeded N exchanges with no condition met\" or \"user does not want to reply\")\n\nWrong — putting behavior instructions in condition:\n\"condition\": \"User agrees. If they ask questions, answer warmly and ask again\"\n\nRight — behavior in prompt, condition is just the trigger:\n\"prompt\": \"Explain the process. If the user has questions, answer them warmly. Once they're ready, confirm.\"\n\"condition\": \"User explicitly agrees or says they are ready\"",
"type": "string"
},
"langs": {
"description": "Optional whitelist of BCP-47 language codes (from facesign://catalog) the exported session may use. DEFAULT BEHAVIOUR (recommended): OMIT this parameter entirely — the app then supports every language in the FaceSign catalog, and `uiStrings` must cover every catalog language. ONLY set `langs` when the user EXPLICITLY restricts the language set (e.g. 'Spanish-only demo', 'support English and Russian'). Do NOT narrow to `['en']` just because the user described the demo in English or did not mention languages. Example (explicit restriction): [\"en\", \"es\", \"ru\"]. The set of target languages for `uiStrings` is derived from this list (or the full catalog if omitted).",
"items": {
"type": "string"
},
"type": "array"
},
"metadata": {
"additionalProperties": {},
"description": "Arbitrary metadata to attach",
"type": "object"
},
"providedData": {
"additionalProperties": true,
"description": "Pre-known user data",
"properties": {
"email": {
"type": "string"
},
"name": {
"type": "string"
},
"phone": {
"type": "string"
}
},
"type": "object"
},
"recapHtml": {
"description": "REQUIRED. Raw HTML/JS for the results/recap page. NO CDATA, NO markdown fences. Every visible string MUST come from `uiStrings` (via {{KEY}} or window.t('KEY')). Contract: reads window.__FACESIGN_SESSION__, window.__FACESIGN_SESSION_ID__, window.__refetchSession() (returns Promise with updated session data). The COMMON MISTAKES sections from the `landingHtml` field description apply here too — read them once and follow for both fields. For the complete session data shape (Session, SessionReport, NodeReport types, delayed vs immediate fields), read the `facesign://session-data-types` MCP resource.\n\nSTYLE GUIDE (Results/Recap Page):\nMatch this visual style for consistency with the FaceSign UI.\n\nBackground: Light gray #f5f7fa with subtle gradient to light blue at top.\n\nLayout: Single-column, max-width 900px, centered (margin 0 auto), padding 2rem.\n\nPage header: \"Session summary\" bold 1.6rem. Subtitle with date/time and duration in muted color #888, font-size 0.9rem.\n\nUser info card: White card with rounded photo (80-100px), grid of icon+text pairs for age, gender, location, device. Icons in muted blue #6b7faa.\n\nSection cards: White background, border-radius 12px, box-shadow 0 2px 12px rgba(0,0,0,.05), padding 1.5rem, margin-bottom 1.5rem.\n\nSection headings: Bold 1.15rem, color #1a1a2e, with small emoji/icon prefix (e.g. ✨ AI Analysis, 🔍 Detected Signals, 📋 Transcript). Margin-bottom 1rem.\n\nStatus banners (full-width within card, border-radius 12px, padding 1rem 1.5rem, white text, bold):\n- Verified/success: background #4a9d6e, shield ✓ icon\n- High-risk/warning: background #d97b30, ⚠ warning icon\n\nSignal items: Left border 4px solid, padding-left 1rem, margin-bottom 1rem, background white or tinted.\n- Normal: border-color #059669, light green tint background #f0fdf4\n- Suspicious: border-color #f59e0b, light yellow tint background #fefce8\n- High-risk: border-color #ef4444, light red tint background #fef2f2\n\nStatus badges (inline, pill): border-radius 6px, padding 2px 10px, font-weight 700, font-size 0.8rem, uppercase.\n- RECOGNIZED/NORMAL: background #d1fae5, color #065f46\n- SUSPICIOUS: background #fef3c7, color #92400e\n\nConfidence scores: Right-aligned, font-size 0.85rem, color #aaa.\n\nKey-value grid: Two-column layout. Label: small text, color #888, font-size 0.8rem, uppercase. Value: font-size 0.95rem, color #1a1a2e, below label.\n\nTranscript: Dark background #1e2a3a, border-radius 10px, padding 1.5rem, monospace font. \"CONVERSATION LOG\" header uppercase, small, muted. Each line: timestamp (gray #777), speaker label (FACESIGN: in teal #4db8a4, USER: in green #6bc96f), message text white. Line-height 1.8.\n\nVideo: Centered, border-radius 8px, max-width 100%, dark container background.\n\nFont: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif.\n\n\nBEST PRACTICES FOR STABLE RENDERING (recapHtml):\n\nSession data arrives in two stages: some fields are available immediately when the session ends (user info, AI analysis, transcript), while others require polling. Video AI analysis may take up to 60 seconds; media usually arrives sooner. Naive implementations cause layout jumping (DOM rebuild on each poll) and duplicate network requests.\n\nARCHITECTURE: Split Static vs Dynamic Rendering.\n\nrenderStatic(data) — called ONCE:\nRenders all sections that won't change: user info (age, sex, location, device), aiAnalysis.overallSummary, aiAnalysis.analysis list, transcript, documentImages.\n\nupdateDynamic(data) — called on each poll:\nUpdates ONLY sections whose data arrives asynchronously: nodeReports, videoAIAnalysis, deepfakeDetection, and media (avatarVideo, screenshots).\nIMPORTANT: nodeReports is DELAYED — do NOT render node report data (face compare results, document scan results, etc.) in renderStatic().\nEach dynamic section has its own wrapper element (e.g. <div id=\"video-ai-content\">, <div id=\"node-reports-content\">) so updates replace only that element's innerHTML, leaving the rest untouched.\nTrack completion with boolean flags (videoAIDone, recordingDone, nodeReportsDone). Once a section is populated, skip further updates.\nSet videoAIDone when videoAIAnalysisStatus is either `succeeded` or `failed`. A failed analysis is terminal: replace the spinner with an unavailable message and do not interpret the missing result as clean evidence. If the status is absent, analysis was not requested, so omit the section.\n\nHTML STRUCTURE:\n<!-- Static sections: rendered once -->\n<div id=\"userinfo-card\"></div>\n<div id=\"summary-card\"></div>\n<div id=\"analysis-card\"></div>\n\n<!-- Dynamic sections: inner content updated by polling -->\n<div id=\"video-ai-card\">\n <div class=\"card\">\n <div class=\"card-title\">Video AI Analysis</div>\n <div id=\"video-ai-content\"><!-- spinner initially, replaced when data arrives --></div>\n </div>\n</div>\n\n<div id=\"recording-card\">\n <div class=\"card\">\n <div class=\"card-title\">Session Recording</div>\n <div id=\"recording-content\"><!-- spinner initially, replaced when data arrives --></div>\n </div>\n</div>\n\n<!-- Static section: rendered once -->\n<div id=\"transcript-card\"></div>\n\nPOLLING: Single Entry Point with Guard (IIFE pattern):\nIMPORTANT: Polling MUST be limited — max 30 attempts, max 2 minutes total. Stop polling when limits are reached even if some data hasn't arrived.\n(function() {\n var polling = false;\n var pollCount = 0;\n var MAX_POLLS = 30;\n var startTime = Date.now();\n var MAX_DURATION = 2 * 60 * 1000; // 2 minutes\n var videoAIDone = false;\n var recordingDone = false;\n var nodeReportsDone = false;\n\n function shouldStop() {\n return pollCount >= MAX_POLLS || (Date.now() - startTime) >= MAX_DURATION;\n }\n\n function poll() {\n if (polling) return;\n if (shouldStop()) return;\n polling = true;\n pollCount++;\n if (typeof window.__refetchSession !== 'function') {\n polling = false;\n setTimeout(poll, 2000);\n return;\n }\n window.__refetchSession()\n .then(function(data) {\n polling = false;\n updateDynamic(data);\n if ((!videoAIDone || !recordingDone || !nodeReportsDone) && !shouldStop()) setTimeout(poll, 4000);\n })\n .catch(function() {\n polling = false;\n if (!shouldStop()) setTimeout(poll, 4000);\n });\n }\n\n function init() {\n var data = window.__FACESIGN_SESSION__;\n if (!data) return;\n renderStatic(data);\n var needMore = updateDynamic(data);\n if (needMore) setTimeout(poll, 4000);\n }\n\n if (window.__FACESIGN_SESSION__) {\n init();\n } else {\n var chk = setInterval(function() {\n if (window.__FACESIGN_SESSION__) { clearInterval(chk); init(); }\n }, 500);\n }\n})();\n\nKEY RULES:\n- Call renderStatic() exactly once — avoids DOM rebuild and layout jumps\n- updateDynamic() only touches dedicated container elements — no reflow outside the updated section\n- Use a polling boolean guard — prevents concurrent __refetchSession() calls\n- ALWAYS limit polling — max 30 attempts AND max 2 minutes total. Never poll indefinitely.\n- Use videoAIDone / recordingDone / nodeReportsDone flags — stops updating a section once its data has been rendered\n- For video AI, derive videoAIDone from videoAIAnalysisStatus, not from the presence of videoAIAnalysis alone\n- nodeReports is DELAYED — always render node report data (face compare, document scan, etc.) in updateDynamic(), never in renderStatic()\n- Single init() entry point via IIFE — eliminates duplicate initialization paths\n- Check typeof __refetchSession === 'function' before calling — handles the case where the API isn't injected yet\n- Use setTimeout not setInterval for polling — ensures the next poll starts only after the previous one completes\n- nodeReport.type is lowercase snake_case (\"face_compare\", \"document_scan\") — use case-insensitive comparison\n- Document scan report fields are Microblink nested objects — use a safe-value extractor function (see COMMON MISTAKES)\n- videoAIAnalysis criterion is camelCase — convert to human-readable with toStartCase() (see COMMON MISTAKES)\n\nSUMMARY: Render once, patch selectively, poll safely with limits. Static content is written to the DOM a single time. Dynamic content (nodeReports, videoAIAnalysis, media) targets specific container elements. Polling is serialized with a guard flag and stops as soon as all async data has arrived or limits are reached (max 30 attempts / 2 minutes).",
"type": "string"
},
"uiStrings": {
"additionalProperties": {
"additionalProperties": {
"type": "string"
},
"type": "object"
},
"description": "REQUIRED. Per-language UI string dictionary: { langId: { key: translatedString } }. You invent the keys to match {{KEY}} / window.t('KEY') in landingHtml and recapHtml. Must contain `en` plus every language in `langs` (or every catalog language if `langs` is omitted). All per-language dicts must share the IDENTICAL key set.",
"type": "object"
},
"videoAIAnalysisEnabled": {
"description": "Enable video AI fraud analysis",
"type": "boolean"
},
"zone": {
"description": "Data processing zone",
"enum": [
"us",
"eu"
],
"type": "string"
}
},
"required": [
"flow",
"landingHtml",
"recapHtml",
"uiStrings"
],
"type": "object"
},
"name": "export_app",
"outputSchema": null
},
{
"description": "Retrieve detailed information about a FaceSign session including status, transcript, AI analysis, node reports, video AI analysis, and asynchronous analysis lifecycle states.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"sessionId": {
"description": "The session ID to retrieve",
"type": "string"
}
},
"required": [
"sessionId"
],
"type": "object"
},
"name": "get_session",
"outputSchema": null
},
{
"description": "REQUIRES set_api_key to be called first. Launch a local web page in the browser for a FaceSign session. You MUST supply `landingHtml` (the pre-session page shown before the iframe), `recapHtml` (the results page shown after the session finishes), and `uiStrings` (a per-language dictionary whose keys are invented by you to match placeholders in the HTML). The MCP server provides NO default chrome — every visible string comes from you. Each page load / refresh creates a fresh session.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"avatarId": {
"description": "Avatar ID from the facesign://catalog resource.",
"type": "string"
},
"clientReferenceId": {
"description": "Your own reference ID for this session",
"type": "string"
},
"customization": {
"additionalProperties": false,
"description": "UI customization",
"properties": {
"controls": {
"additionalProperties": false,
"properties": {
"autoHide": {
"description": "Whether controls may automatically fade out. Omitted or true preserves the default behavior; false keeps the selected buttons visible. Ignored when showUxControls is false",
"type": "boolean"
},
"buttons": {
"description": "Optional exact subset of in-session control buttons. Omit this field for the recommended default panel with all applicable controls. Set it only when the user requests a specialized flow or UI; for example, [\"captions\"] creates a captions-only panel, while an empty array hides the panel. Ignored when showUxControls is false",
"items": {
"enum": [
"microphone",
"camera",
"captions",
"language",
"close"
],
"type": "string"
},
"type": "array"
},
"captionsOpenedByDefault": {
"description": "Start the session with the closed-captions (transcript) panel open. Omitted or false keeps it closed. The user can toggle it with the CC button either way. Set it for accessibility, or when viewers may be reading along in a second language. Ignored when showUxControls is false",
"type": "boolean"
},
"showUxControls": {
"description": "Master switch for the in-session control panel. When false the panel is not rendered and every other controls.* setting has no effect",
"type": "boolean"
}
},
"type": "object"
},
"permissionsPage": {
"additionalProperties": false,
"properties": {
"backgroundColor": {
"type": "string"
},
"backgroundType": {
"enum": [
"AVATAR",
"COLOR"
],
"type": "string"
},
"buttonText": {
"type": "string"
},
"buttonTextTranslates": {
"additionalProperties": {
"type": "string"
},
"description": "Localized permission-button text, for example { es: 'Continuar' }",
"type": "object"
},
"mainHeading": {
"type": "string"
},
"mainHeadingTranslates": {
"$ref": "#/properties/customization/properties/permissionsPage/properties/buttonTextTranslates",
"description": "Localized permission-page headings"
},
"subheading": {
"type": "string"
},
"subheadingTranslates": {
"$ref": "#/properties/customization/properties/permissionsPage/properties/buttonTextTranslates",
"description": "Localized permission-page subheadings"
}
},
"type": "object"
}
},
"type": "object"
},
"defaultLang": {
"description": "Optional fallback BCP-47 language code used when the end-user's browser language is not in `langs`. DEFAULT BEHAVIOUR: OMIT this parameter. The MCP server falls back to 'en' internally only as a last-resort for UI string lookup; you should not hardcode a default language here unless the user explicitly asks for one. When set, must be one of the codes in `langs` (if `langs` is also set).",
"type": "string"
},
"extractionSchema": {
"description": "Optional schema describing fields the FaceSign backend should extract from the session transcript using an LLM. Results are populated post-session at `session.report.extractedData` as `{ [fieldName]: string | number | boolean | null }` (null when the transcript did not contain the data — every field is treated as optional). Use this for structured data collection that does NOT need its own dedicated node — e.g. follow-up questions in a single CONVERSATION node where you'd otherwise have to author multiple branching outcomes. The conversation flow itself is unaffected; extraction runs on the final transcript. Render the results in your recapHtml when present.",
"items": {
"additionalProperties": false,
"properties": {
"description": {
"description": "Natural-language hint for the LLM describing what to look for in the transcript. This is the primary extraction signal — be specific (e.g. \"the user's full legal name\" rather than \"name\").",
"type": "string"
},
"enum": {
"description": "Optional allowed string values. When set, the LLM normalizes free-form answers (e.g. \"yeah\", \"sure\") into one of the listed values. Recommended for yes/no or fixed-choice questions.",
"items": {
"type": "string"
},
"type": "array"
},
"fieldName": {
"description": "Key under which the extracted value appears in session.report.extractedData.",
"type": "string"
},
"type": {
"description": "Expected value type. `date` is returned as an ISO 8601 string.",
"enum": [
"string",
"number",
"boolean",
"date"
],
"type": "string"
}
},
"required": [
"fieldName",
"type",
"description"
],
"type": "object"
},
"type": "array"
},
"flow": {
"description": "Array of nodes forming the session's directed graph",
"items": {
"anyOf": [
{
"additionalProperties": false,
"properties": {
"id": {
"description": "Unique node identifier",
"type": "string"
},
"outcome": {
"description": "The id of the next node",
"type": "string"
},
"type": {
"const": "start",
"type": "string"
}
},
"required": [
"id",
"type",
"outcome"
],
"type": "object"
},
{
"additionalProperties": false,
"properties": {
"id": {
"description": "Unique node identifier",
"type": "string"
},
"type": {
"const": "end",
"type": "string"
}
},
"required": [
"id",
"type"
],
"type": "object"
},
{
"additionalProperties": false,
"properties": {
"awaitExternal": {
"additionalProperties": false,
"description": "Optional background poll: the avatar keeps talking (normal outcomes stay active) while an external result becomes ready; exits via onDataReady/onTimeout/onError",
"properties": {
"exits": {
"additionalProperties": false,
"description": "Where to route when the background poll resolves",
"properties": {
"onDataReady": {
"description": "Node id when the endpoint returns HTTP 200 (result ready)",
"type": "string"
},
"onError": {
"description": "Node id when the call errors",
"type": "string"
},
"onTimeout": {
"description": "Node id when poll.timeoutMs elapses first",
"type": "string"
}
},
"required": [
"onDataReady",
"onTimeout",
"onError"
],
"type": "object"
},
"poll": {
"additionalProperties": false,
"description": "Polling cadence and overall timeout",
"properties": {
"intervalMs": {
"description": "Delay between polls in milliseconds",
"type": "number"
},
"timeoutMs": {
"description": "Total time to keep polling before giving up",
"type": "number"
}
},
"required": [
"intervalMs",
"timeoutMs"
],
"type": "object"
},
"request": {
"additionalProperties": false,
"description": "The endpoint to poll in the background",
"properties": {
"method": {
"description": "HTTP method. Defaults to POST (JSON body); GET sends vars as query params",
"enum": [
"GET",
"POST"
],
"type": "string"
},
"url": {
"description": "Endpoint to call (must pass the server-side allowlist). The current sessionId is appended automatically as a query parameter",
"type": "string"
},
"vars": {
"description": "Payload variables, each resolving to a literal or a session value",
"items": {
"additionalProperties": false,
"properties": {
"name": {
"description": "Key under which the value is placed in the request payload",
"type": "string"
},
"value": {
"anyOf": [
{
"additionalProperties": false,
"properties": {
"type": {
"const": "literal",
"type": "string"
},
"value": {
"description": "The literal scalar value",
"type": [
"string",
"number",
"boolean"
]
}
},
"required": [
"type",
"value"
],
"type": "object"
},
{
"additionalProperties": false,
"properties": {
"field": {
"description": "The field/key name within the source",
"type": "string"
},
"source": {
"description": "Source to resolve the referenced value from",
"enum": [
"extractedData",
"providedData"
],
"type": "string"
},
"type": {
"const": "ref",
"type": "string"
}
},
"required": [
"type",
"source",
"field"
],
"type": "object"
}
],
"description": "Literal value or a reference resolved from session data"
}
},
"required": [
"name",
"value"
],
"type": "object"
},
"type": "array"
}
},
"required": [
"url"
],
"type": "object"
}
},
"required": [
"request",
"poll",
"exits"
],
"type": "object"
},
"doesNotRequireReply": {
"description": "If true, avatar speaks without waiting for user reply",
"type": "boolean"
},
"id": {
"description": "Unique node identifier",
"type": "string"
},
"outcomes": {
"description": "Array of {id, targetNodeId, condition} routing entries",
"items": {
"additionalProperties": false,
"properties": {
"condition": {
"description": "Condition description, e.g. \"default\", \"user says yes\"",
"type": "string"
},
"id": {
"type": "string"
},
"targetNodeId": {
"description": "The id of the node to route to",
"type": "string"
}
},
"required": [
"id",
"targetNodeId",
"condition"
],
"type": "object"
},
"minItems": 1,
"type": "array"
},
"prompt": {
"description": "What the avatar says to the user",
"type": "string"
},
"type": {
"const": "conversation",
"type": "string"
}
},
"required": [
"id",
"type",
"prompt",
"outcomes"
],
"type": "object"
},
{
"additionalProperties": false,
"properties": {
"id": {
"description": "Unique node identifier",
"type": "string"
},
"outcomes": {
"additionalProperties": false,
"description": "Record mapping each outcome to a target node id. All four are required",
"properties": {
"deepfakeDetected": {
"description": "Node id when deepfake is detected",
"type": "string"
},
"inconclusive": {
"description": "Node id when the check produced no verdict — the detector could not decide, did not run, or there was nothing to analyse. This is our failure, not the user's: route it to a retry, a step-up or a manual review, never to the happy path by default",
"type": "string"
},
"livenessDetected": {
"description": "Node id when liveness is confirmed",
"type": "string"
},
"noFace": {
"description": "Node id when frames were captured but no face is on them",
"type": "string"
}
},
"required": [
"livenessDetected",
"deepfakeDetected",
"noFace",
"inconclusive"
],
"type": "object"
},
"type": {
"const": "liveness_detection",
"type": "string"
}
},
"required": [
"id",
"type",
"outcomes"
],
"type": "object"
},
{
"additionalProperties": false,
"properties": {
"id": {
"description": "Unique node identifier",
"type": "string"
},
"outcomes": {
"additionalProperties": false,
"description": "Record mapping each outcome to a target node id",
"properties": {
"canceled": {
"description": "Node id when user cancels",
"type": "string"
},
"emailEntered": {
"description": "Node id when email is provided",
"type": "string"
}
},
"required": [
"emailEntered",
"canceled"
],
"type": "object"
},
"prompt": {
"description": "Optional phrase the avatar says at the moment the email input field appears (direct speech, e.g. \"Could you please enter your email address?\"). Keep it short — one sentence. Omit to let the avatar stay silent while the field is shown.",
"type": "string"
},
"type": {
"const": "enter_email",
"type": "string"
}
},
"required": [
"id",
"type",
"outcomes"
],
"type": "object"
},
{
"additionalProperties": false,
"properties": {
"id": {
"description": "Unique node identifier",
"type": "string"
},
"outcomes": {
"description": "Array of {id, targetNodeId, condition} routing entries",
"items": {
"$ref": "#/properties/flow/items/anyOf/2/properties/outcomes/items"
},
"minItems": 1,
"type": "array"
},
"type": {
"const": "data_validation",
"type": "string"
},
"validation": {
"additionalProperties": false,
"properties": {
"action": {
"type": "string"
},
"field": {
"type": "string"
},
"value": {
"type": "string"
}
},
"required": [
"field",
"action"
],
"type": "object"
}
},
"required": [
"id",
"type",
"validation",
"outcomes"
],
"type": "object"
},
{
"additionalProperties": false,
"properties": {
"allowedDocumentTypes": {
"description": "Allowed document types, e.g. [\"id\", \"passport\", \"dl\"]",
"items": {
"type": "string"
},
"minItems": 1,
"type": "array"
},
"id": {
"description": "Unique node identifier",
"type": "string"
},
"outcomes": {
"additionalProperties": false,
"description": "Record mapping each outcome to a target node id",
"properties": {
"scanSuccess": {
"description": "Node id on successful scan",
"type": "string"
},
"scanTimeout": {
"description": "Node id on scan timeout",
"type": "string"
},
"userCancelled": {
"description": "Node id when user cancels",
"type": "string"
}
},
"required": [
"scanSuccess",
"userCancelled",
"scanTimeout"
],
"type": "object"
},
"scanningMode": {
"description": "Scanning mode",
"enum": [
"single",
"automatic"
],
"type": "string"
},
"type": {
"const": "document_scan",
"type": "string"
}
},
"required": [
"id",
"type",
"scanningMode",
"allowedDocumentTypes",
"outcomes"
],
"type": "object"
},
{
"additionalProperties": false,
"properties": {
"id": {
"description": "Unique node identifier",
"type": "string"
},
"outcomes": {
"additionalProperties": false,
"description": "Record mapping each outcome to a target node id",
"properties": {
"newUser": {
"description": "Node id when user is new",
"type": "string"
},
"noFace": {
"description": "Node id when no face is found",
"type": "string"
},
"recognized": {
"description": "Node id when user is recognized",
"type": "string"
}
},
"required": [
"recognized",
"newUser",
"noFace"
],
"type": "object"
},
"type": {
"const": "recognition",
"type": "string"
}
},
"required": [
"id",
"type",
"outcomes"
],
"type": "object"
},
{
"additionalProperties": false,
"properties": {
"captureInstructions": {
"type": "string"
},
"id": {
"description": "Unique node identifier",
"type": "string"
},
"outcomes": {
"additionalProperties": false,
"description": "Record mapping each outcome to a target node id",
"properties": {
"cancelled": {
"description": "Node id when user cancels",
"type": "string"
},
"error": {
"description": "Node id on error",
"type": "string"
},
"notPassed": {
"description": "Node id when face scan fails",
"type": "string"
},
"passed": {
"description": "Node id when face scan passes",
"type": "string"
}
},
"required": [
"passed",
"notPassed",
"cancelled",
"error"
],
"type": "object"
},
"requireAILivenessCheck": {
"type": "boolean"
},
"requireLivenessChallenge": {
"type": "boolean"
},
"similarityThreshold": {
"description": "0-1, how similar faces must be",
"type": "number"
},
"type": {
"const": "face_scan",
"type": "string"
}
},
"required": [
"id",
"type",
"outcomes"
],
"type": "object"
},
{
"additionalProperties": false,
"properties": {
"emailTemplate": {
"type": "string"
},
"expirySeconds": {
"description": "OTP expiry in seconds, default 300",
"type": "number"
},
"id": {
"description": "Unique node identifier",
"type": "string"
},
"maxAttempts": {
"description": "Max verification attempts, default 3",
"type": "number"
},
"otpLength": {
"description": "OTP length 4-8, default 6",
"type": "number"
},
"outcomes": {
"additionalProperties": false,
"description": "Record mapping each outcome to a target node id",
"properties": {
"cancelled": {
"description": "Node id when user cancels",
"type": "string"
},
"delivery_failed": {
"description": "Node id when delivery fails",
"type": "string"
},
"error": {
"description": "Node id on error",
"type": "string"
},
"failed_unverified": {
"description": "Node id when max attempts exhausted",
"type": "string"
},
"verified": {
"description": "Node id when OTP verified",
"type": "string"
}
},
"required": [
"verified",
"delivery_failed",
"failed_unverified",
"cancelled",
"error"
],
"type": "object"
},
"resendAfterSeconds": {
"description": "Seconds before allowing OTP resend",
"type": "number"
},
"showUI": {
"description": "Whether to show the OTP input UI",
"type": "boolean"
},
"type": {
"const": "two_factor_email",
"type": "string"
}
},
"required": [
"id",
"type",
"outcomes"
],
"type": "object"
},
{
"additionalProperties": false,
"properties": {
"expirySeconds": {
"description": "OTP expiry in seconds, default 300",
"type": "number"
},
"id": {
"description": "Unique node identifier",
"type": "string"
},
"maxAttempts": {
"description": "Max verification attempts, default 3",
"type": "number"
},
"otpLength": {
"description": "OTP length 4-8, default 6",
"type": "number"
},
"outcomes": {
"additionalProperties": false,
"description": "Record mapping each outcome to a target node id",
"properties": {
"cancelled": {
"description": "Node id when user cancels",
"type": "string"
},
"delivery_failed": {
"description": "Node id when delivery fails",
"type": "string"
},
"error": {
"description": "Node id on error",
"type": "string"
},
"failed_unverified": {
"description": "Node id when max attempts exhausted",
"type": "string"
},
"verified": {
"description": "Node id when OTP verified",
"type": "string"
}
},
"required": [
"verified",
"delivery_failed",
"failed_unverified",
"cancelled",
"error"
],
"type": "object"
},
"resendAfterSeconds": {
"description": "Seconds before allowing OTP resend",
"type": "number"
},
"showUI": {
"description": "Whether to show the OTP input UI",
"type": "boolean"
},
"smsTemplate": {
"type": "string"
},
"type": {
"const": "two_factor_sms",
"type": "string"
}
},
"required": [
"id",
"type",
"outcomes"
],
"type": "object"
},
{
"additionalProperties": false,
"properties": {
"id": {
"description": "Unique node identifier",
"type": "string"
},
"outcomes": {
"additionalProperties": false,
"description": "Record mapping each outcome to a target node id",
"properties": {
"permissionsDenied": {
"description": "Node id when permissions are denied",
"type": "string"
},
"permissionsGranted": {
"description": "Node id when permissions are granted",
"type": "string"
}
},
"required": [
"permissionsGranted",
"permissionsDenied"
],
"type": "object"
},
"permissions": {
"additionalProperties": false,
"description": "Which permissions to request",
"properties": {
"camera": {
"description": "Request camera access",
"type": "boolean"
},
"microphone": {
"description": "Request microphone access",
"type": "boolean"
}
},
"type": "object"
},
"prompt": {
"description": "Optional message the avatar says (direct speech mode, e.g. 'Say: Please enable your camera.')",
"type": "string"
},
"type": {
"const": "permissions",
"type": "string"
}
},
"required": [
"id",
"type",
"permissions",
"outcomes"
],
"type": "object"
},
{
"additionalProperties": false,
"properties": {
"id": {
"description": "Unique node identifier",
"type": "string"
},
"outcomes": {
"additionalProperties": false,
"description": "Record mapping each outcome to a target node id",
"properties": {
"imageUnavailable": {
"description": "Node id when one or both images could not be obtained",
"type": "string"
},
"match": {
"description": "Node id when faces match",
"type": "string"
},
"noMatch": {
"description": "Node id when faces do not match",
"type": "string"
}
},
"required": [
"match",
"noMatch",
"imageUnavailable"
],
"type": "object"
},
"similarityThreshold": {
"description": "0-1, minimum similarity score required for a match",
"type": "number"
},
"sourceA": {
"additionalProperties": false,
"description": "First face image source",
"properties": {
"providedDataKey": {
"description": "Required when source is 'providedData' — the key in providedData that holds the image URL",
"type": "string"
},
"source": {
"description": "Source of the face image",
"enum": [
"sessionVideo",
"faceScan",
"providedData",
"documentPhoto"
],
"type": "string"
}
},
"required": [
"source"
],
"type": "object"
},
"sourceB": {
"additionalProperties": false,
"description": "Second face image source",
"properties": {
"providedDataKey": {
"$ref": "#/properties/flow/items/anyOf/12/properties/sourceA/properties/providedDataKey"
},
"source": {
"$ref": "#/properties/flow/items/anyOf/12/properties/sourceA/properties/source"
}
},
"required": [
"source"
],
"type": "object"
},
"type": {
"const": "face_compare",
"type": "string"
}
},
"required": [
"id",
"type",
"sourceA",
"sourceB",
"outcomes"
],
"type": "object"
},
{
"additionalProperties": false,
"properties": {
"id": {
"description": "Unique node identifier",
"type": "string"
},
"outcome": {
"description": "Node id to advance to after the response is merged into providedData (never branches)",
"type": "string"
},
"request": {
"additionalProperties": false,
"description": "The external endpoint to call and the payload to send",
"properties": {
"method": {
"$ref": "#/properties/flow/items/anyOf/2/properties/awaitExternal/properties/request/properties/method"
},
"url": {
"$ref": "#/properties/flow/items/anyOf/2/properties/awaitExternal/properties/request/properties/url"
},
"vars": {
"$ref": "#/properties/flow/items/anyOf/2/properties/awaitExternal/properties/request/properties/vars"
}
},
"required": [
"url"
],
"type": "object"
},
"type": {
"const": "external_call",
"type": "string"
}
},
"required": [
"id",
"type",
"request",
"outcome"
],
"type": "object"
},
{
"additionalProperties": false,
"properties": {
"default": {
"description": "Node id to route to when no rule matches",
"type": "string"
},
"id": {
"description": "Unique node identifier",
"type": "string"
},
"rules": {
"description": "Rules evaluated top-to-bottom; first match wins",
"items": {
"additionalProperties": false,
"properties": {
"left": {
"additionalProperties": false,
"description": "Reference to the session value to test",
"properties": {
"field": {
"description": "The field/key name within the source",
"type": "string"
},
"source": {
"description": "Where the value is read from: 'extractedData' (a field from extractionSchema, extracted on demand from the transcript) or 'providedData' (a key set at creation or written by a prior external_call)",
"enum": [
"extractedData",
"providedData"
],
"type": "string"
}
},
"required": [
"source",
"field"
],
"type": "object"
},
"operator": {
"description": "Comparison operator (omit 'right' for 'exists'/'notExists')",
"enum": [
"equals",
"notEquals",
"gt",
"lt",
"gte",
"lte",
"exists",
"notExists"
],
"type": "string"
},
"outcome": {
"description": "Node id to route to when this rule matches",
"type": "string"
},
"right": {
"anyOf": [
{
"$ref": "#/properties/flow/items/anyOf/2/properties/awaitExternal/properties/request/properties/vars/items/properties/value/anyOf/0"
},
{
"$ref": "#/properties/flow/items/anyOf/2/properties/awaitExternal/properties/request/properties/vars/items/properties/value/anyOf/1"
}
],
"description": "Literal or reference to compare against; omit for 'exists'/'notExists'"
}
},
"required": [
"left",
"operator",
"outcome"
],
"type": "object"
},
"minItems": 1,
"type": "array"
},
"type": {
"const": "field_condition",
"type": "string"
}
},
"required": [
"id",
"type",
"rules",
"default"
],
"type": "object"
}
]
},
"type": "array"
},
"landingHtml": {
"description": "REQUIRED. Raw HTML/JS for the pre-session landing page. NO CDATA, NO markdown fences. Every visible string MUST come from `uiStrings` — reference them via {{KEY}} placeholders (interpolated at inject time) or via window.t('KEY') (dynamic at runtime). Contract: reads window.__FACESIGN_FLOW__ (the flow array). MUST call window.__startSession(updatedFlow?) to launch the session. The session iframe has its own Start button for audio/video autoplay gesture — the landing page does NOT need one for that purpose.\n\nSTYLE GUIDE (Pre-Session Page):\nMatch this visual style for consistency with the FaceSign UI.\n\nBackground: Light gray #f5f7fa. Full-viewport centered layout (flexbox, min-height: 100vh; min-height: 100dvh — always use dvh with vh fallback for iOS compatibility).\n\nCard: White, border-radius 16px, box-shadow 0 4px 24px rgba(0,0,0,.08), padding 2.5-3rem, max-width 480-600px, centered.\n\nHeadings: Bold, 1.4-1.8rem, color #1a1a2e, centered in card.\n\nInputs: Full-width, padding 14px 16px, border 2px solid #d1d5db, border-radius 12px, font-size 1rem. Focus: border-color #5b7bab. Placeholder color #9ca3af.\n\nPrimary buttons: Background #7b8fb5 (steel-blue), color white, font-weight 600, font-size 1.05rem, border-radius 50px (pill shape), padding 14px, full-width in card. Hover: background #6a7fa5. Disabled: background #c5cdd8, cursor not-allowed.\n\nLinks: Color #4573b8, no underline, underline on hover. Back navigation: \"← Back\" at top-left of card, color #4573b8, font-size 0.95rem.\n\nProfile images: Circular (border-radius 50%), 120-150px diameter, centered, subtle box-shadow.\n\nSpacing: 2.5-3rem card padding, 1.5rem between form groups, 1rem between label and input, 2rem above primary button.\n\nFont: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif.\n\n\nCOMMON MISTAKES TO AVOID (UI/CSS):\n\n1. Never set padding or margin on body\nThe custom HTML is injected into a host page that controls its own layout. Setting padding, margin, or min-height on body will conflict with the host page styles and create unwanted spacing.\n\nWrong:\nbody { padding: 2rem; min-height: 100vh; }\n(Also wrong — using only 100vh without dvh fallback. Always add min-height: 100dvh after 100vh for iOS Safari.)\n\nRight — use a wrapper element instead:\nbody { margin: 0; padding: 0; }\n.wrap { max-width: 860px; margin: 0 auto; padding: 2rem; }\n\nThe same applies to background on body — avoid it unless you are certain the host page does not set its own background.\n\n2. Use HTML entities for emoji, not JS unicode escapes\nWhen building HTML strings in JavaScript (via innerHTML, string concatenation, etc.), JS unicode escapes like \\ud83c\\udfa5 will NOT render as emoji. They only work inside JS string literals that are directly displayed via textContent or similar APIs.\n\nWrong — renders as garbled text:\nh += '<div class=\"title\">\\ud83c\\udfa5 Video AI Analysis</div>';\n\nRight — use HTML numeric entities:\nh += '<div class=\"title\">🎥 Video AI Analysis</div>';\n\nAlso right — use emoji directly in static HTML (outside of JS):\n<div class=\"title\">🎥 Video AI Analysis</div>\n\nCommon emoji HTML entities reference:\n👤 👤 User/person\n🎂 🎂 Birthday cake\n📍 📍 Pin/location\n💻 💻 Computer\n✨ ✨ Sparkles\n🔍 🔍 Search\n🎥 🎥 Camera\n🎬 🎬 Clapper board\n📋 📋 Clipboard\n⚧ ⚧ Gender symbol\n✓ ✓ Checkmark\n\n3. CSS class name collision between landingHtml and recapHtml\nCRITICAL: landingHtml and recapHtml are injected into the SAME host document. If both fragments use the same class names (e.g., .card, .container, .header), styles from one will leak into the other, causing layout breakage.\n\nALWAYS use unique prefixed class names:\n- landingHtml (pre-session/landing page): prefix all classes with .lp- (e.g., .lp-card, .lp-header, .lp-btn)\n- recapHtml (results page): prefix all classes with .r- (e.g., .r-card, .r-header, .r-wrap)\n\nWrong — causes collisions:\n/* in landingHtml */ .card { max-width: 460px; }\n/* in recapHtml */ .card { max-width: 900px; }\n\nRight — namespaced:\n/* in landingHtml */ .lp-card { max-width: 460px; }\n/* in recapHtml */ .r-card { max-width: 900px; }\n\n\nCOMMON MISTAKES TO AVOID (Code/API):\n\n4. window.__startSession race condition\nNEVER call window.__startSession() synchronously in a click handler. The host page may not have injected the function yet. ALWAYS use polling:\n\nfunction waitAndStart(input) {\n var n = 0;\n var iv = setInterval(function() {\n n++;\n if (typeof window.__startSession === 'function') {\n clearInterval(iv);\n window.__startSession(input);\n } else if (n > 100) {\n clearInterval(iv);\n document.body.innerHTML = '<p style=\"text-align:center;padding:2rem;color:red;\">Failed to initialize session. Please refresh.</p>';\n }\n }, 100);\n}\n\nWrong:\nstartBtn.addEventListener('click', function() { window.__startSession(); });\n\nRight:\nstartBtn.addEventListener('click', function() { waitAndStart(); });\n\nFor exported apps, NEVER send window.__FACESIGN_FLOW__ or another graph back to\nthe server. To select a flow declared in export_app, pass its ID:\nwaitAndStart({ flowId: 'enhanced', providedData: { name: nameInput.value } });\n\n5. Permissions are automatic — do NOT add a permissions node by default\nFaceSign automatically requests camera and microphone permissions at session start. Most flows do NOT need a PERMISSIONS node. Only add one in these specific cases:\n(a) The user wants to request microphone and camera permissions separately at different times during the flow (instead of both at once at session start).\n(b) The user wants to move the permission request to the initial page (landingHtml), outside the FaceSign flow itself.\n\n6. nodeReport.type values are lowercase snake_case\nThe API returns nodeReport.type in lowercase snake_case: \"face_compare\", \"document_scan\", \"conversation\", \"permissions\", \"liveness_detection\", etc. — NOT uppercase like \"FACE_COMPARE\".\n\nALWAYS use case-insensitive comparison when looking up node reports:\n\nfunction getNode(r, type) {\n var rr = (r && r.nodeReports) || [];\n var tl = type.toLowerCase();\n for (var i = 0; i < rr.length; i++) {\n if (rr[i].type && rr[i].type.toLowerCase() === tl) return rr[i];\n }\n return null;\n}\n\n// Usage: getNode(report, 'face_compare'), getNode(report, 'document_scan')\n\n7. Microblink document report — nested field structure\nDOCUMENT_SCAN nodeReport.report fields are NOT plain strings. They use Microblink's nested structure:\n firstName: { latin: { value: \"JANICE\" } }\n dateOfBirth: { originalString: { latin: { value: \"04/30/1970\" } } }\n — OR —\n dateOfBirth: { day: 30, month: 4, year: 1970 }\n\nNEVER read fields directly as strings (e.g., dr.firstName will be an object, not \"JANICE\").\n\nALWAYS use this safe-value extractor:\n\nfunction sv(v) {\n if (v == null) return '';\n if (typeof v === 'string') return v;\n if (v.latin && v.latin.value != null) return String(v.latin.value);\n if (v.originalString) return sv(v.originalString);\n if (v.day != null && v.month != null && v.year != null)\n return v.month + '/' + v.day + '/' + v.year;\n return '';\n}\n\n// Usage: sv(dr.firstName) → \"JANICE\", sv(dr.dateOfBirth) → \"04/30/1970\"\n\n8. videoAIAnalysis criterion is camelCase — format for display\nThe criterion field (e.g., \"facialExpressionAndMovement\", \"useOfExternalDevices\") comes in camelCase. Convert to human-readable format:\n\nfunction toStartCase(s) {\n if (!s) return '';\n return s.replace(/([A-Z]+)/g, function(m) { return ' ' + m.toLowerCase(); })\n .trim().replace(/^./, function(c) { return c.toUpperCase(); });\n}\n// \"facialExpressionAndMovement\" → \"Facial expression and movement\"\n\n9. Conversation node: condition vs prompt\nIn CONVERSATION nodes:\n- \"condition\" (in outcomes) = ONLY describes the trigger event for transitioning to the next node (e.g., \"User explicitly agrees to proceed\")\n- \"prompt\" = ALL instructions for the avatar's behavior, including how to greet, how to respond to questions, how to handle objections, and how to persuade\n- User questions or objections during the conversation are handled within the SAME node (continued dialog), NOT via separate outcomes\n- Conversation nodes can have any number of outcomes depending on the use case:\n * Branching nodes (e.g., \"what color?\") → one outcome per branch + a fallback\n * Consent/agreement nodes → typically 2: agreement + fallback after N attempts\n- ALWAYS include a fallback outcome for when the conversation stalls (e.g., \"conversation exceeded N exchanges with no condition met\" or \"user does not want to reply\")\n\nWrong — putting behavior instructions in condition:\n\"condition\": \"User agrees. If they ask questions, answer warmly and ask again\"\n\nRight — behavior in prompt, condition is just the trigger:\n\"prompt\": \"Explain the process. If the user has questions, answer them warmly. Once they're ready, confirm.\"\n\"condition\": \"User explicitly agrees or says they are ready\"",
"type": "string"
},
"langs": {
"description": "Optional whitelist of BCP-47 language codes (from facesign://catalog) the session may use. DEFAULT BEHAVIOUR (recommended): OMIT this parameter entirely — the session then supports every language in the FaceSign catalog, and `uiStrings` must cover every catalog language. ONLY set `langs` when the user EXPLICITLY restricts the language set (e.g. 'Spanish-only demo', 'support English and Russian'). Do NOT narrow to `['en']` just because the user described the demo in English or did not mention languages — that would silently drop multilingual support. Example (explicit restriction): [\"en\", \"es\", \"ru\"]. The set of target languages for `uiStrings` is derived from this list (or the full catalog if omitted).",
"items": {
"type": "string"
},
"type": "array"
},
"metadata": {
"additionalProperties": {},
"description": "Arbitrary metadata to attach",
"type": "object"
},
"providedData": {
"additionalProperties": true,
"description": "Pre-known user data",
"properties": {
"email": {
"type": "string"
},
"name": {
"type": "string"
},
"phone": {
"type": "string"
}
},
"type": "object"
},
"recapHtml": {
"description": "REQUIRED. Raw HTML/JS for the results/recap page. NO CDATA, NO markdown fences. Every visible string MUST come from `uiStrings` (via {{KEY}} or window.t('KEY')). Contract: reads window.__FACESIGN_SESSION__ (session data, re-populated on each poll), window.__FACESIGN_SESSION_ID__, window.__refetchSession() (returns Promise with updated data). The COMMON MISTAKES sections from the `landingHtml` field description apply here too — read them once and follow for both fields. For the complete session data shape (Session, SessionReport, NodeReport types, delayed vs immediate fields), read the `facesign://session-data-types` MCP resource.\n\nSTYLE GUIDE (Results/Recap Page):\nMatch this visual style for consistency with the FaceSign UI.\n\nBackground: Light gray #f5f7fa with subtle gradient to light blue at top.\n\nLayout: Single-column, max-width 900px, centered (margin 0 auto), padding 2rem.\n\nPage header: \"Session summary\" bold 1.6rem. Subtitle with date/time and duration in muted color #888, font-size 0.9rem.\n\nUser info card: White card with rounded photo (80-100px), grid of icon+text pairs for age, gender, location, device. Icons in muted blue #6b7faa.\n\nSection cards: White background, border-radius 12px, box-shadow 0 2px 12px rgba(0,0,0,.05), padding 1.5rem, margin-bottom 1.5rem.\n\nSection headings: Bold 1.15rem, color #1a1a2e, with small emoji/icon prefix (e.g. ✨ AI Analysis, 🔍 Detected Signals, 📋 Transcript). Margin-bottom 1rem.\n\nStatus banners (full-width within card, border-radius 12px, padding 1rem 1.5rem, white text, bold):\n- Verified/success: background #4a9d6e, shield ✓ icon\n- High-risk/warning: background #d97b30, ⚠ warning icon\n\nSignal items: Left border 4px solid, padding-left 1rem, margin-bottom 1rem, background white or tinted.\n- Normal: border-color #059669, light green tint background #f0fdf4\n- Suspicious: border-color #f59e0b, light yellow tint background #fefce8\n- High-risk: border-color #ef4444, light red tint background #fef2f2\n\nStatus badges (inline, pill): border-radius 6px, padding 2px 10px, font-weight 700, font-size 0.8rem, uppercase.\n- RECOGNIZED/NORMAL: background #d1fae5, color #065f46\n- SUSPICIOUS: background #fef3c7, color #92400e\n\nConfidence scores: Right-aligned, font-size 0.85rem, color #aaa.\n\nKey-value grid: Two-column layout. Label: small text, color #888, font-size 0.8rem, uppercase. Value: font-size 0.95rem, color #1a1a2e, below label.\n\nTranscript: Dark background #1e2a3a, border-radius 10px, padding 1.5rem, monospace font. \"CONVERSATION LOG\" header uppercase, small, muted. Each line: timestamp (gray #777), speaker label (FACESIGN: in teal #4db8a4, USER: in green #6bc96f), message text white. Line-height 1.8.\n\nVideo: Centered, border-radius 8px, max-width 100%, dark container background.\n\nFont: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif.\n\n\nBEST PRACTICES FOR STABLE RENDERING (recapHtml):\n\nSession data arrives in two stages: some fields are available immediately when the session ends (user info, AI analysis, transcript), while others require polling. Video AI analysis may take up to 60 seconds; media usually arrives sooner. Naive implementations cause layout jumping (DOM rebuild on each poll) and duplicate network requests.\n\nARCHITECTURE: Split Static vs Dynamic Rendering.\n\nrenderStatic(data) — called ONCE:\nRenders all sections that won't change: user info (age, sex, location, device), aiAnalysis.overallSummary, aiAnalysis.analysis list, transcript, documentImages.\n\nupdateDynamic(data) — called on each poll:\nUpdates ONLY sections whose data arrives asynchronously: nodeReports, videoAIAnalysis, deepfakeDetection, and media (avatarVideo, screenshots).\nIMPORTANT: nodeReports is DELAYED — do NOT render node report data (face compare results, document scan results, etc.) in renderStatic().\nEach dynamic section has its own wrapper element (e.g. <div id=\"video-ai-content\">, <div id=\"node-reports-content\">) so updates replace only that element's innerHTML, leaving the rest untouched.\nTrack completion with boolean flags (videoAIDone, recordingDone, nodeReportsDone). Once a section is populated, skip further updates.\nSet videoAIDone when videoAIAnalysisStatus is either `succeeded` or `failed`. A failed analysis is terminal: replace the spinner with an unavailable message and do not interpret the missing result as clean evidence. If the status is absent, analysis was not requested, so omit the section.\n\nHTML STRUCTURE:\n<!-- Static sections: rendered once -->\n<div id=\"userinfo-card\"></div>\n<div id=\"summary-card\"></div>\n<div id=\"analysis-card\"></div>\n\n<!-- Dynamic sections: inner content updated by polling -->\n<div id=\"video-ai-card\">\n <div class=\"card\">\n <div class=\"card-title\">Video AI Analysis</div>\n <div id=\"video-ai-content\"><!-- spinner initially, replaced when data arrives --></div>\n </div>\n</div>\n\n<div id=\"recording-card\">\n <div class=\"card\">\n <div class=\"card-title\">Session Recording</div>\n <div id=\"recording-content\"><!-- spinner initially, replaced when data arrives --></div>\n </div>\n</div>\n\n<!-- Static section: rendered once -->\n<div id=\"transcript-card\"></div>\n\nPOLLING: Single Entry Point with Guard (IIFE pattern):\nIMPORTANT: Polling MUST be limited — max 30 attempts, max 2 minutes total. Stop polling when limits are reached even if some data hasn't arrived.\n(function() {\n var polling = false;\n var pollCount = 0;\n var MAX_POLLS = 30;\n var startTime = Date.now();\n var MAX_DURATION = 2 * 60 * 1000; // 2 minutes\n var videoAIDone = false;\n var recordingDone = false;\n var nodeReportsDone = false;\n\n function shouldStop() {\n return pollCount >= MAX_POLLS || (Date.now() - startTime) >= MAX_DURATION;\n }\n\n function poll() {\n if (polling) return;\n if (shouldStop()) return;\n polling = true;\n pollCount++;\n if (typeof window.__refetchSession !== 'function') {\n polling = false;\n setTimeout(poll, 2000);\n return;\n }\n window.__refetchSession()\n .then(function(data) {\n polling = false;\n updateDynamic(data);\n if ((!videoAIDone || !recordingDone || !nodeReportsDone) && !shouldStop()) setTimeout(poll, 4000);\n })\n .catch(function() {\n polling = false;\n if (!shouldStop()) setTimeout(poll, 4000);\n });\n }\n\n function init() {\n var data = window.__FACESIGN_SESSION__;\n if (!data) return;\n renderStatic(data);\n var needMore = updateDynamic(data);\n if (needMore) setTimeout(poll, 4000);\n }\n\n if (window.__FACESIGN_SESSION__) {\n init();\n } else {\n var chk = setInterval(function() {\n if (window.__FACESIGN_SESSION__) { clearInterval(chk); init(); }\n }, 500);\n }\n})();\n\nKEY RULES:\n- Call renderStatic() exactly once — avoids DOM rebuild and layout jumps\n- updateDynamic() only touches dedicated container elements — no reflow outside the updated section\n- Use a polling boolean guard — prevents concurrent __refetchSession() calls\n- ALWAYS limit polling — max 30 attempts AND max 2 minutes total. Never poll indefinitely.\n- Use videoAIDone / recordingDone / nodeReportsDone flags — stops updating a section once its data has been rendered\n- For video AI, derive videoAIDone from videoAIAnalysisStatus, not from the presence of videoAIAnalysis alone\n- nodeReports is DELAYED — always render node report data (face compare, document scan, etc.) in updateDynamic(), never in renderStatic()\n- Single init() entry point via IIFE — eliminates duplicate initialization paths\n- Check typeof __refetchSession === 'function' before calling — handles the case where the API isn't injected yet\n- Use setTimeout not setInterval for polling — ensures the next poll starts only after the previous one completes\n- nodeReport.type is lowercase snake_case (\"face_compare\", \"document_scan\") — use case-insensitive comparison\n- Document scan report fields are Microblink nested objects — use a safe-value extractor function (see COMMON MISTAKES)\n- videoAIAnalysis criterion is camelCase — convert to human-readable with toStartCase() (see COMMON MISTAKES)\n\nSUMMARY: Render once, patch selectively, poll safely with limits. Static content is written to the DOM a single time. Dynamic content (nodeReports, videoAIAnalysis, media) targets specific container elements. Polling is serialized with a guard flag and stops as soon as all async data has arrived or limits are reached (max 30 attempts / 2 minutes).",
"type": "string"
},
"uiStrings": {
"additionalProperties": {
"additionalProperties": {
"type": "string"
},
"type": "object"
},
"description": "REQUIRED. Per-language UI string dictionary: { langId: { key: translatedString } }. You invent the keys to match the {{KEY}} placeholders / window.t('KEY') calls in your landingHtml and recapHtml. Must contain an entry for `en` (ultimate runtime fallback) and for every language in `langs` (or every catalog language if `langs` is omitted). Every per-language dict must share the IDENTICAL set of keys. Example: { \"en\": { \"START\": \"Start\", \"LOADING\": \"Loading...\" }, \"fr\": { \"START\": \"Commencer\", \"LOADING\": \"Chargement...\" } }.",
"type": "object"
},
"videoAIAnalysisEnabled": {
"description": "Enable video AI fraud analysis",
"type": "boolean"
},
"zone": {
"description": "Data processing zone",
"enum": [
"us",
"eu"
],
"type": "string"
}
},
"required": [
"flow",
"landingHtml",
"recapHtml",
"uiStrings"
],
"type": "object"
},
"name": "launch_session_ui",
"outputSchema": null
},
{
"description": "List FaceSign sessions with optional filtering by status, date range, and search term. Supports pagination via cursor.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"clientReferenceId": {
"description": "Filter by client reference ID",
"type": "string"
},
"cursor": {
"description": "Pagination cursor from a previous response",
"type": "string"
},
"flowId": {
"description": "Filter sessions by flow ID",
"type": "string"
},
"fromDate": {
"description": "Start of date range (Unix timestamp in ms)",
"type": "number"
},
"limit": {
"description": "Max sessions to return (1-100, default 10)",
"type": "number"
},
"search": {
"description": "Search term to filter sessions",
"type": "string"
},
"sortBy": {
"description": "Field to sort by",
"enum": [
"createdAt",
"status",
"finishedAt"
],
"type": "string"
},
"sortOrder": {
"description": "Sort direction",
"enum": [
"asc",
"desc"
],
"type": "string"
},
"status": {
"anyOf": [
{
"enum": [
"created",
"inProgress",
"incomplete",
"complete"
],
"type": "string"
},
{
"items": {
"$ref": "#/properties/status/anyOf/0"
},
"type": "array"
}
],
"description": "Filter by session status"
},
"toDate": {
"description": "End of date range (Unix timestamp in ms)",
"type": "number"
}
},
"type": "object"
},
"name": "list_sessions",
"outputSchema": null
},
{
"description": "Set your FaceSign API key for this session. This must be called before any other FaceSign tools. Get your API key from the FaceSign dashboard. The key starts with sk_live_... or sk_test_... Optionally override the API server URL via `serverUrl` — intended for FaceSign developers pointing the SDK at a non-production backend (e.g. a dev server). Leave unset to use the default production API.",
"inputSchema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"apiKey": {
"description": "Your FaceSign API key (sk_live_... or sk_test_...)",
"type": "string"
},
"serverUrl": {
"description": "Optional FaceSign API server URL override. Only set this if the user explicitly asks to target a non-default server (e.g. a dev backend). Omit otherwise to use the production API.",
"format": "uri",
"type": "string"
}
},
"required": [
"apiKey"
],
"type": "object"
},
"name": "set_api_key",
"outputSchema": null
}
]
}Verify it yourself
curl -s https://api.teppi.xyz/v1/evidence/sha256:c6dd86dcf75ad644deb0518e6d443345d83800bcfc3ab263d21096516f3d34f5 | sha256sum