> For the complete documentation index, see [llms.txt](https://docs.fill-easy.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.fill-easy.com/facial-and-id-scan.md).

# Facial & ID Scan

Identity verification that bundles **ID document verification**, **face match**, and **passive liveness** into a single hosted flow. The user completes capture on a Fill Easy-hosted page; you receive the decision via webhook or polling.

***

#### Flow

1. Call `POST /ocr/auth` with a `redirect` URL → receive a `token` and a hosted `sessionUrl`
2. Redirect the user to `sessionUrl` to complete ID capture, liveness, and face match
3. Receive the result via **webhook** or **poll** `/ocr/poll`

***

#### Result Delivery

**Webhook (recommended)** — Pass a `callbackUrl` in the `/ocr/auth` body. When verification completes, a `POST` is sent to that URL with the decision wrapped in a `WebhookPayload` envelope. Polling remains available as a fallback.

**Polling** — Call `/ocr/poll` with the returned `token` until a `200` response is returned.

***

#### Direct API (no redirect)

Prefer to run capture inside your own app? Call `POST /ocr/verify` with base64 images to get the same `OcrResult` decision synchronously — no hosted page, webhook, or polling. The checks that run are driven by what you send: `frontImage` for ID Verification, `selfie` for Passive Liveness, and both together add Face Match.

## Start session

> Creates an OCR verification session bundling ID Verification, Face Match,\
> and Passive Liveness. Returns a hosted \`sessionUrl\` to redirect the user to,\
> plus a JWT \`token\` used later with \`/ocr/poll\` to retrieve the decision.\
> \
> \*\*Webhook Delivery:\*\* Provide \`callbackUrl\` to receive a \`POST\` when verification\
> completes. The webhook body is a \[\`WebhookPayload\`]\(#/components/schemas/WebhookPayload)\
> envelope whose \`data\` is an \[\`OcrResult\`]\(#/components/schemas/OcrResult) — the\
> same shape returned by \`/ocr/poll\` on \`200\`. The request also carries an\
> \`X-Webhook-Event: ocr.verification.completed\` header plus any custom headers\
> supplied via \`callbackHeaders\`. Polling remains available regardless of whether\
> a \`callbackUrl\` is provided.

```json
{"openapi":"3.1.0","info":{"title":"Fill Easy Services","version":"1.0.0"},"tags":[{"name":"Facial & ID Scan","description":"Identity verification that bundles **ID document verification**, **face match**, and\n**passive liveness** into a single hosted flow. The user completes capture on a\nFill Easy-hosted page; you receive the decision via webhook or polling.\n\n---\n\n### Flow\n\n1. Call [`POST /ocr/auth`](#tag/OCR/operation/ocrAuth) with a `redirect` URL → receive a\n   `token` and a hosted `sessionUrl`\n\n2. Redirect the user to `sessionUrl` to complete ID capture, liveness, and face match\n\n3. Receive the result via **webhook** or **poll** [`/ocr/poll`](#tag/OCR/operation/ocrPoll)\n\n---\n\n### Result Delivery\n\n**Webhook (recommended)** — Pass a `callbackUrl` in the `/ocr/auth` body. When verification\ncompletes, a `POST` is sent to that URL with the decision wrapped in a\n[`WebhookPayload`](#/components/schemas/WebhookPayload) envelope. Polling remains available\nas a fallback.\n\n**Polling** — Call `/ocr/poll` with the returned `token` until a `200` response is returned.\n\n---\n\n### Direct API (no redirect)\n\nPrefer to run capture inside your own app? Call\n[`POST /ocr/verify`](#tag/OCR/operation/ocrVerify) with base64 images to get the same\n[`OcrResult`](#/components/schemas/OcrResult) decision synchronously — no hosted page,\nwebhook, or polling. The checks that run are driven by what you send: `frontImage` for\nID Verification, `selfie` for Passive Liveness, and both together add Face Match.\n"}],"servers":[{"url":"sandbox.staging-api.fill-easy.com"}],"security":[{"ClientID":[],"ClientSecret":[]}],"components":{"securitySchemes":{"ClientID":{"type":"apiKey","description":"Client ID in x-client-id header.","name":"x-client-id","in":"header"}},"schemas":{"RedirectUri":{"type":"string","description":"Redirect URI after user authorization.\n\nBrowser: HTTPS URL to your website.\n\niOS: HTTPS Universal link\n\nAndroid: Package name (com.filleasy.app)\n"},"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"Error message"}}}},"responses":{"BadRequest":{"description":"Bad Request - Invalid input parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"InternalServerError":{"description":"Internal Server Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"paths":{"/ocr/auth":{"post":{"tags":["Facial & ID Scan"],"summary":"Start session","description":"Creates an OCR verification session bundling ID Verification, Face Match,\nand Passive Liveness. Returns a hosted `sessionUrl` to redirect the user to,\nplus a JWT `token` used later with `/ocr/poll` to retrieve the decision.\n\n**Webhook Delivery:** Provide `callbackUrl` to receive a `POST` when verification\ncompletes. The webhook body is a [`WebhookPayload`](#/components/schemas/WebhookPayload)\nenvelope whose `data` is an [`OcrResult`](#/components/schemas/OcrResult) — the\nsame shape returned by `/ocr/poll` on `200`. The request also carries an\n`X-Webhook-Event: ocr.verification.completed` header plus any custom headers\nsupplied via `callbackHeaders`. Polling remains available regardless of whether\na `callbackUrl` is provided.","operationId":"ocrAuth","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["redirect"],"properties":{"redirect":{"$ref":"#/components/schemas/RedirectUri"},"vendorData":{"type":"string","description":"Optional identifier of your own (e.g. user id) stored with the session and returned in the webhook payload."},"metadata":{"type":"object","description":"Arbitrary JSON stored with the session, returned in the webhook payload.","additionalProperties":true},"callbackUrl":{"type":"string","format":"uri","pattern":"^https://.+","description":"HTTPS URL to receive a webhook POST when verification completes.\nWhen provided, the webhook `data` is the same payload as a `200` response\nfrom `/ocr/poll`. Polling remains available as a fallback.\n"},"callbackHeaders":{"type":"object","additionalProperties":{"type":"string"},"description":"Custom HTTP headers to include in the webhook request.\nUse this to pass authentication or any other headers your endpoint requires\n(e.g. `{\"Authorization\": \"Bearer <token>\", \"X-Api-Key\": \"...\"}`).\n"}}}}}},"responses":{"200":{"description":"Session created","content":{"application/json":{"schema":{"type":"object","required":["token","sessionUrl"],"properties":{"token":{"type":"string","description":"JWT to pass to /ocr/poll"},"sessionUrl":{"type":"string","format":"uri","description":"Hosted OCR verification URL to redirect the user to"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"500":{"$ref":"#/components/responses/InternalServerError"},"503":{"description":"Upstream verification provider unavailable - retry later"}}}}}}
```

## Poll result

> Poll with the \`token\` returned from \`/ocr/auth\` to get the verification decision.\
> Returns 202 while still pending, 200 once the upstream webhook has delivered the\
> result. The \`status\` field doubles as the pending flag and the terminal verdict.

```json
{"openapi":"3.1.0","info":{"title":"Fill Easy Services","version":"1.0.0"},"tags":[{"name":"Facial & ID Scan","description":"Identity verification that bundles **ID document verification**, **face match**, and\n**passive liveness** into a single hosted flow. The user completes capture on a\nFill Easy-hosted page; you receive the decision via webhook or polling.\n\n---\n\n### Flow\n\n1. Call [`POST /ocr/auth`](#tag/OCR/operation/ocrAuth) with a `redirect` URL → receive a\n   `token` and a hosted `sessionUrl`\n\n2. Redirect the user to `sessionUrl` to complete ID capture, liveness, and face match\n\n3. Receive the result via **webhook** or **poll** [`/ocr/poll`](#tag/OCR/operation/ocrPoll)\n\n---\n\n### Result Delivery\n\n**Webhook (recommended)** — Pass a `callbackUrl` in the `/ocr/auth` body. When verification\ncompletes, a `POST` is sent to that URL with the decision wrapped in a\n[`WebhookPayload`](#/components/schemas/WebhookPayload) envelope. Polling remains available\nas a fallback.\n\n**Polling** — Call `/ocr/poll` with the returned `token` until a `200` response is returned.\n\n---\n\n### Direct API (no redirect)\n\nPrefer to run capture inside your own app? Call\n[`POST /ocr/verify`](#tag/OCR/operation/ocrVerify) with base64 images to get the same\n[`OcrResult`](#/components/schemas/OcrResult) decision synchronously — no hosted page,\nwebhook, or polling. The checks that run are driven by what you send: `frontImage` for\nID Verification, `selfie` for Passive Liveness, and both together add Face Match.\n"}],"servers":[{"url":"sandbox.staging-api.fill-easy.com"}],"security":[{"ClientID":[],"ClientSecret":[]}],"components":{"securitySchemes":{"ClientID":{"type":"apiKey","description":"Client ID in x-client-id header.","name":"x-client-id","in":"header"}},"schemas":{"Token":{"type":"string","pattern":"^[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]*$","description":"JWT token"},"OcrResult":{"type":"object","description":"Verification decision returned by `/ocr/poll` (200) and as the `data` field of\nthe `ocr.verification.completed` webhook. `status` carries the terminal verdict.\n`identity`, `faceMatch`, and `liveness` are present only when the upstream\ncheck produced data — for example, a `Declined` outcome with no captured\ndocument may return only `status`.\n","required":["status"],"properties":{"status":{"type":"string","enum":["Approved","Declined","In Review","Kyc Expired"],"description":"Terminal verdict from the verification provider."},"identity":{"type":"object","description":"Extracted ID document fields.","properties":{"firstName":{"type":"string"},"lastName":{"type":"string"},"fullName":{"type":"string"},"dateOfBirth":{"type":"string"},"documentNumber":{"type":"string"},"documentType":{"type":"string"},"issuingCountry":{"type":"string","description":"ISO 3166-1 alpha-3 code."},"gender":{"type":"string"},"dateOfIssue":{"type":"string"},"expirationDate":{"type":"string"}}},"faceMatch":{"type":"object","description":"1:1 face match between the selfie capture and the ID portrait.","properties":{"score":{"type":"number","description":"Similarity 0-100 (higher is a closer match)."}}},"liveness":{"type":"object","description":"Passive liveness check on the selfie capture.","properties":{"score":{"type":"number","description":"Confidence 0-100 that the subject is a live person."},"method":{"type":"string"}}}}},"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"Error message"}}}},"responses":{"BadRequest":{"description":"Bad Request - Invalid input parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Unauthorized":{"description":"Unauthorized - Token is missing, invalid, or expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"paths":{"/ocr/poll":{"post":{"tags":["Facial & ID Scan"],"summary":"Poll result","description":"Poll with the `token` returned from `/ocr/auth` to get the verification decision.\nReturns 202 while still pending, 200 once the upstream webhook has delivered the\nresult. The `status` field doubles as the pending flag and the terminal verdict.","operationId":"ocrPoll","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["token"],"properties":{"token":{"$ref":"#/components/schemas/Token"}}}}}},"responses":{"200":{"description":"Verification complete - decision returned","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OcrResult"}}}},"202":{"description":"Pending - user has not completed verification yet","content":{"application/json":{"schema":{"type":"object","required":["status"],"properties":{"status":{"type":"string","enum":["pending"]}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"410":{"description":"Token is valid but the session result has expired (TTL) or is unknown"}}}}}}
```

## Verify (direct)

> Synchronous, no-redirect alternative to \`/ocr/auth\`. Submit the images you have as\
> base64 strings and receive the decision directly in the response — the same\
> \[\`OcrResult\`]\(#/components/schemas/OcrResult) shape \`/ocr/poll\` returns.\
> \
> \*\*You choose which checks run by what you send\*\* (send at least one of \`frontImage\`\
> or \`selfie\`):\
> \
> \| Inputs sent | Checks run |\
> \|---|---|\
> \| \`frontImage\` (+ optional \`backImage\`) | ID Verification |\
> \| \`selfie\` | Passive Liveness |\
> \| \`frontImage\` + \`selfie\` | ID Verification + Face Match + Passive Liveness |\
> \
> No hosted page, webhook, or polling is involved: your application captures the images\
> and this call returns the verdict in one round-trip. \`status\` is \`Approved\` only when\
> every check that ran passed; \`identity\`, \`faceMatch\`, and \`liveness\` are present only\
> for the checks that ran.\
> \
> Images are base64-encoded — a raw base64 string or a \`data:\` URL. Keep the combined\
> request body under \~6 MB; compress phone captures before sending.

```json
{"openapi":"3.1.0","info":{"title":"Fill Easy Services","version":"1.0.0"},"tags":[{"name":"Facial & ID Scan","description":"Identity verification that bundles **ID document verification**, **face match**, and\n**passive liveness** into a single hosted flow. The user completes capture on a\nFill Easy-hosted page; you receive the decision via webhook or polling.\n\n---\n\n### Flow\n\n1. Call [`POST /ocr/auth`](#tag/OCR/operation/ocrAuth) with a `redirect` URL → receive a\n   `token` and a hosted `sessionUrl`\n\n2. Redirect the user to `sessionUrl` to complete ID capture, liveness, and face match\n\n3. Receive the result via **webhook** or **poll** [`/ocr/poll`](#tag/OCR/operation/ocrPoll)\n\n---\n\n### Result Delivery\n\n**Webhook (recommended)** — Pass a `callbackUrl` in the `/ocr/auth` body. When verification\ncompletes, a `POST` is sent to that URL with the decision wrapped in a\n[`WebhookPayload`](#/components/schemas/WebhookPayload) envelope. Polling remains available\nas a fallback.\n\n**Polling** — Call `/ocr/poll` with the returned `token` until a `200` response is returned.\n\n---\n\n### Direct API (no redirect)\n\nPrefer to run capture inside your own app? Call\n[`POST /ocr/verify`](#tag/OCR/operation/ocrVerify) with base64 images to get the same\n[`OcrResult`](#/components/schemas/OcrResult) decision synchronously — no hosted page,\nwebhook, or polling. The checks that run are driven by what you send: `frontImage` for\nID Verification, `selfie` for Passive Liveness, and both together add Face Match.\n"}],"servers":[{"url":"sandbox.staging-api.fill-easy.com"}],"security":[{"ClientID":[],"ClientSecret":[]}],"components":{"securitySchemes":{"ClientID":{"type":"apiKey","description":"Client ID in x-client-id header.","name":"x-client-id","in":"header"}},"schemas":{"OcrResult":{"type":"object","description":"Verification decision returned by `/ocr/poll` (200) and as the `data` field of\nthe `ocr.verification.completed` webhook. `status` carries the terminal verdict.\n`identity`, `faceMatch`, and `liveness` are present only when the upstream\ncheck produced data — for example, a `Declined` outcome with no captured\ndocument may return only `status`.\n","required":["status"],"properties":{"status":{"type":"string","enum":["Approved","Declined","In Review","Kyc Expired"],"description":"Terminal verdict from the verification provider."},"identity":{"type":"object","description":"Extracted ID document fields.","properties":{"firstName":{"type":"string"},"lastName":{"type":"string"},"fullName":{"type":"string"},"dateOfBirth":{"type":"string"},"documentNumber":{"type":"string"},"documentType":{"type":"string"},"issuingCountry":{"type":"string","description":"ISO 3166-1 alpha-3 code."},"gender":{"type":"string"},"dateOfIssue":{"type":"string"},"expirationDate":{"type":"string"}}},"faceMatch":{"type":"object","description":"1:1 face match between the selfie capture and the ID portrait.","properties":{"score":{"type":"number","description":"Similarity 0-100 (higher is a closer match)."}}},"liveness":{"type":"object","description":"Passive liveness check on the selfie capture.","properties":{"score":{"type":"number","description":"Confidence 0-100 that the subject is a live person."},"method":{"type":"string"}}}}},"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"Error message"}}}},"responses":{"BadRequest":{"description":"Bad Request - Invalid input parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"InternalServerError":{"description":"Internal Server Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"paths":{"/ocr/verify":{"post":{"tags":["Facial & ID Scan"],"summary":"Verify (direct)","description":"Synchronous, no-redirect alternative to `/ocr/auth`. Submit the images you have as\nbase64 strings and receive the decision directly in the response — the same\n[`OcrResult`](#/components/schemas/OcrResult) shape `/ocr/poll` returns.\n\n**You choose which checks run by what you send** (send at least one of `frontImage`\nor `selfie`):\n\n| Inputs sent | Checks run |\n|---|---|\n| `frontImage` (+ optional `backImage`) | ID Verification |\n| `selfie` | Passive Liveness |\n| `frontImage` + `selfie` | ID Verification + Face Match + Passive Liveness |\n\nNo hosted page, webhook, or polling is involved: your application captures the images\nand this call returns the verdict in one round-trip. `status` is `Approved` only when\nevery check that ran passed; `identity`, `faceMatch`, and `liveness` are present only\nfor the checks that ran.\n\nImages are base64-encoded — a raw base64 string or a `data:` URL. Keep the combined\nrequest body under ~6 MB; compress phone captures before sending.","operationId":"ocrVerify","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","description":"Send at least one of `frontImage` or `selfie`.","properties":{"frontImage":{"type":"string","description":"Base64-encoded image of the ID document front. Triggers ID Verification."},"backImage":{"type":"string","description":"Base64-encoded image of the ID document back, when the document has one."},"selfie":{"type":"string","description":"Base64-encoded selfie. Triggers Passive Liveness, and Face Match when `frontImage` is also sent."}}}}}},"responses":{"200":{"description":"Verification complete - decision returned","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OcrResult"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"500":{"$ref":"#/components/responses/InternalServerError"},"503":{"description":"Upstream verification provider unavailable - retry later"}}}}}}
```
