> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sahlfinancial.com/llms.txt
> Use this file to discover all available pages before exploring further.

# eID check

> Remote identity verification of a Canadian client: start, poll, read the result, download the report.

An eID check proves that a client who is not in front of you is the person on their ID. The client receives an email with a PIN and a link, scans an ID and takes a selfie in the eID provider's app. Three endpoints cover it. Scope for all three: `kyc:eid`.

In this version the engine accepts Canadian clients only for eID. The samples on this page therefore use country `CA`, unlike the rest of the guides, which use `MA`.

| Endpoint | Does |
| - | - |
| `POST /v1/kyc/eid` | Starts the check. The provider emails the client right away. Returns HTTP 201 and a `key`. |
| `GET /v1/kyc/eid/{key}` | Where the check stands, what it proved, and its checks. |
| `GET /v1/kyc/eid/{key}/report` | The provider's PDF report of the check. |

## Before you start

| Condition | If not met |
| - | - |
| The client is Canadian: `country` is `CA` or `CAN`. | 422 `identity verification is available for Canadian clients only` |
| Your workspace has its own account with the eID provider. Sahl sets this up. | 404 `identity verification is not set up for this tenant` |
| The key has the `kyc:eid` scope and the workspace is enabled for the partner API. | 403 `insufficient_scope` (key) or `kyc_scope_not_allowed` (workspace). To get partner access, use the [contact form](https://sahlfinancial.com/contact?type=demo). |

<Warning>
  `environment` does not change the provider. A request made with `environment: "sandbox"` still emails the client and still calls the provider. `environment` only chooses which case the request is filed on. Use an email address you control when you test.
</Warning>

## Start a check

```json theme={null}
{
  "reference": "client-0001",
  "first_name": "Test",
  "last_name": "Client",
  "email": "test.client@example.com",
  "country": "CA",
  "language": "en",
  "documents": 1,
  "environment": "sandbox"
}
```

| Field | Type | Default | Rule |
| - | - | - | - |
| `reference` | string | required | 1 to 64 characters of `A-Z a-z 0-9 _ . : -`. Your id for the client. |
| `first_name` | string | required | 1 to 100 characters. |
| `last_name` | string | required | 1 to 100 characters. |
| `email` | string | required | A valid email. The PIN and link go here. |
| `country` | string | required | 2 or 3 characters. Only `CA` or `CAN` is accepted. |
| `language` | string | `en` | `en` or `fr`. |
| `documents` | integer | `1` | `1` or `2`: how many pieces of ID the client must scan. |
| `environment` | string | `sandbox` | `sandbox` or `production`. Picks the case. |

The answer:

```json theme={null}
{ "key": 123456, "reference": "client-0001" }
```

`key` is the id you poll with. The PIN is sent to the client only and is never returned to you.

The request is filed as `pending` on the case for (workspace, environment, `reference`). A new request for the same reference and environment replaces the record, because you started the client's verification over. The provider stores the request under a client id made of your workspace and your reference, which is what keeps another workspace from reading it.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://app.sahlfinancial.com/api/v1/kyc/eid \
    -H "Authorization: Bearer $SAHL_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"reference":"client-0001","first_name":"Test","last_name":"Client","email":"test.client@example.com","country":"CA","language":"en","documents":1,"environment":"sandbox"}'
  ```

  ```javascript JavaScript theme={null}
  const res = await fetch("https://app.sahlfinancial.com/api/v1/kyc/eid", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.SAHL_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      reference: "client-0001",
      first_name: "Test",
      last_name: "Client",
      email: "test.client@example.com",
      country: "CA",
      language: "en",
      documents: 1,
      environment: "sandbox",
    }),
  });
  if (res.status !== 201) throw new Error(`${res.status} ${await res.text()}`);
  const { key } = await res.json();
  ```

  ```python Python theme={null}
  import os, requests

  res = requests.post(
      "https://app.sahlfinancial.com/api/v1/kyc/eid",
      headers={"Authorization": f"Bearer {os.environ['SAHL_API_KEY']}"},
      json={
          "reference": "client-0001",
          "first_name": "Test",
          "last_name": "Client",
          "email": "test.client@example.com",
          "country": "CA",
          "language": "en",
          "documents": 1,
          "environment": "sandbox",
      },
      timeout=60,
  )
  assert res.status_code == 201, res.text
  key = res.json()["key"]
  ```
</CodeGroup>

## States

There is no callback from the provider. You poll `GET /v1/kyc/eid/{key}`. Sahl derives the state from the poll.

```mermaid theme={null}
stateDiagram-v2
    [*] --> pending: POST /v1/kyc/eid (client emailed)
    pending --> complete: client scanned a document
    pending --> archived: request archived, never completed
    complete --> passed: no critical check failed
    complete --> failed: a critical check failed
    passed --> [*]
    failed --> [*]
    archived --> [*]
```

| State | `complete` | `passed` | What you do |
| - | - | - | - |
| `pending` | false | false | Keep polling. The check `eid:completed` says `the client has not completed the verification yet`. |
| `passed` | true | true | Use it. Download the report. |
| `failed` | true | false | See which `eid:` check failed. Start a new request to retry. |
| `archived` | false | false | The request was archived without a completed check. `eid:completed` says `the request was archived without a completed verification`. Start a new request. |

`pending`, `passed`, `failed` and `archived` are also the status Sahl records on the case. Only `passed` meets a policy's eID requirement.

## Poll

`GET /v1/kyc/eid/{key}` has one optional query parameter, `environment` (default `sandbox`). It only places a request that was started before Sahl kept the eID record. A request made through `POST /v1/kyc/eid` keeps its own environment.

Sahl sets no polling interval. The only cap is the 100 requests a minute per IP. A reasonable schedule is every 30 seconds for the first 10 minutes, then every 5 minutes. The client has to open the email, so minutes to hours are normal.

A finished check (complete, or archived) is the moment Sahl records the outcome on the case, and the first poll that sees it sends the `kyc.eid_completed` [webhook](/guides/webhooks). Later polls stay quiet.

### Answer when complete

```json theme={null}
{
  "key": 123456,
  "complete": true,
  "passed": true,
  "identity": {
    "documentType": "PASSPORT",
    "documentNumber": "P1234567",
    "expiryDate": "2030-05-01",
    "birthDate": "1988-04-12",
    "firstName": "Test",
    "lastName": "Client"
  },
  "checks": [
    { "id": "eid:liveness", "label": "Selfie passed the liveness check", "severity": "critical", "passed": true, "detail": "" },
    { "id": "eid:face_match", "label": "Face on the ID matches the selfie (score 3 or more)", "severity": "critical", "passed": true, "detail": "score 4 of 4, confidence 97%" },
    { "id": "eid:name_match", "label": "Name on the ID matches the name on the request", "severity": "critical", "passed": true, "detail": "" }
  ],
  "completed_date": "2026-10-07T12:00:00Z"
}
```

The values are fake, produced by the same function the API uses.

| Key | Meaning |
| - | - |
| `key` | The request id. |
| `complete` | True once the client has scanned a document. |
| `passed` | True when `complete` and no check with severity `critical` failed. |
| `identity` | What the check proved. Possible keys: `documentType`, `documentNumber`, `expiryDate`, `birthDate`, `firstName`, `lastName`, `address`. Only present for values the provider holds. Empty until complete. |
| `checks` | The eID checks, in the same shape as the verification checks. |
| `completed_date` | Completion time as the provider reports it, or null. |

### The checks

| Id | Severity | Passes when |
| - | - | - |
| `eid:completed` | critical | Only present until the client completes. Always failed. |
| `eid:liveness` | critical | The selfie passed the liveness check. |
| `eid:face_match` | critical | The face match score is 3 or more (the detail shows `score N of 4` and the confidence). |
| `eid:name_match` | critical | The name on the ID matches the name on your request. This is why the first and last name you send must be accurate. |
| `eid:message:N` | critical or warning | The provider returned a message. Always failed. See below. |

Provider messages and their severity:

| Message | Severity |
| - | - |
| `The machine readable values of one or more fields do not match.` | critical |
| `Document is past expiry date.` | critical |
| `Name entered on request does not match name on document.` | critical |
| `Low face match score.` | critical |
| `No machine readable data found on document.` | warning |
| any other message | warning |

## Report

`GET /v1/kyc/eid/{key}/report` returns the provider's report as `application/pdf` with `Content-Disposition: attachment; filename="eid-<key>.pdf"`. It is for the client's file.

```bash theme={null}
curl -o eid-123456.pdf https://app.sahlfinancial.com/api/v1/kyc/eid/123456/report \
  -H "Authorization: Bearer $SAHL_API_KEY"
```

## Keep the result

Fetch the result and the PDF within about seven days of the check. After that the provider deletes the personal details. Sahl records the outcome (status, completion time) on your case when a poll first sees the end state, but the `identity` block and the PDF come from the provider, so store what you need.

## Meeting a policy eID requirement

A workspace policy can require a remote eID check for a person not met face to face (`eid_required_non_face_to_face`). `/verify` and `/assess` then add a critical check `policy:eid_non_face_to_face`:

| Situation | Check |
| - | - |
| A request with this `reference` and `environment` was made through `POST /v1/kyc/eid` and its recorded status is `passed` | passes: `eID request N passed` |
| No request on file | fails: `the client was not met in person and no eID request made through Sahl (POST /v1/kyc/eid) is on file for this reference and environment` |
| Request pending | fails: `eID request N has not been seen completed; poll GET /v1/kyc/eid/{key} once the client has finished` |
| Request failed or archived | fails: `eID request N ended failed` (or `archived`) |
| `values.verified_in_person` is true | not applied. An info check `policy:met_in_person_declared` records that it is your declaration and that Sahl did not verify it. |
| `purpose` is `periodic_review` | not applied |

Only Sahl's own record meets the requirement. A check you send in `extra_checks` with an id starting `eid:` is renamed `partner:eid:...` and never counts. Use the same `reference` and `environment` for the eID request and for the `/verify` call. Poll the check to completion before you call `/verify`, because the outcome is recorded by the poll.

## Errors

| Status | Body | Cause |
| - | - | - |
| 401, 403 | See [errors](/errors) | Key problems. |
| 404 | `identity verification is not set up for this tenant` | No provider account on the workspace. |
| 404 | `Not Found` | Unknown `key`, or a key started by another workspace. |
| 422 | `identity verification is available for Canadian clients only` | `country` is not `CA` or `CAN`. |
| 422 | `detail` list | A field does not match the schema, for example `documents` is not 1 or 2. |
| 502 | `the identity verification service did not answer` | The provider is down or refused. Retry later. |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.