API reference
The identity verification API reference.
Every endpoint you can call, every field you'll see back, and what to do with a webhook once it arrives. For exact field types, treat the live OpenAPI document at /docs on your own API origin as the source of truth — this page is the plain-language version.
Base URL and environments
One base URL. Two environments.
Every request goes to the same host. Which environment you're in is decided entirely by which API key you send.
| Base URL (this documentation) | https://api.shieldid.ca — illustrative; your production API origin is published together with your API keys. |
|---|---|
| Auth header | X-Api-Key: <your key>, on every request except the two public endpoints noted below. |
| Sandbox key prefix | vk_sand_… — sessions created with this key are stamped environment: "sandbox" and are never billed. |
| Production key prefix | vk_live_… — the same requests, charged per your plan. |
| Content type | application/json on every request body and response. |
Endpoints
The full v1 surface.
Six JSON endpoints, plus a hosted page and a public credential check that aren't JSON at all — you just link to them.
| Endpoint | Auth | What it does |
|---|---|---|
| POST /v1/sessions | X-Api-Key | Create a verification session. Requires a return_url and a webhook_url. Returns session_id, verification_url and status: "PENDING". |
| GET /v1/sessions/{id} | X-Api-Key | Status and top-line fields for one session: status, confidence, environment. |
| GET /v1/sessions/{id}/result | X-Api-Key | The full result once a session reaches VERIFIED or FAILED: person, per-check verdicts, confidence, verified_until. |
| DELETE /v1/sessions/{id} | X-Api-Key | Cancel a session that is still PENDING or IN_PROGRESS. A session already at VERIFIED, FAILED or EXPIRED can't be canceled. |
| GET /v1/credentials/{token} | None — public | Check whether a verified credential is currently valid. No API key required, so it's callable from a browser. Note: today's response also includes the verified person's name and document type/country, not just a valid/invalid flag — do not hand this link to a third party you don't want to see that. |
| GET /v1/pricing/{country} | X-Api-Key | Per-country pricing for a two-letter ISO 3166-1 country code. |
| /v/{session_id} | The hosted verification page — the verification_url a session returns. Desktop shows a QR code; scanning it hands the applicant's phone the same link to capture their document and selfie. |
|---|---|
| /c/{session_id} | A public, unauthenticated credential-check page for one verified session. Note: today's response also includes the verified person's name and document type/country, not just a valid/invalid flag — do not hand this link to a third party you don't want to see that. |
| /docs | The live, interactive OpenAPI reference for your account's API origin. |
Creating a session
POST /v1/sessions, end to end.
The request, and exactly what comes back.
curl -X POST https://api.shieldid.ca/v1/sessions \
-H "X-Api-Key: vk_sand_your_key" \
-H "Content-Type: application/json" \
-d '{
"return_url": "https://yourapp.example/verified",
"webhook_url": "https://yourapp.example/hooks/shieldid",
"expected_name": "Jordan Smith",
"locale": "en",
"metadata": { "application_id": "app_9182" }
}'| return_url | string, required — Where the applicant is sent back after they finish. Must be http or https. |
|---|---|
| webhook_url | string, required — Where the signed result is delivered. See Webhooks below. |
| expected_name | string, required — The name you expect on the document, used to cross-check what the ID actually says. |
| expected_dob / expected_address | string, optional — Cross-checked against the document when supplied; not required to run a verification. |
| locale | string, optional — "en" or "fr". Sets the language of the hosted page the applicant sees. |
| metadata | object, optional — Your own reference data — an application id, a tenant id, anything you want echoed back unchanged in the result and the webhook. |
The result
GET /v1/sessions/{id}/result — response shape.
The same shape is delivered in the webhook payload, so you rarely need to poll this endpoint at all.
| session_id | The id you created. |
|---|---|
| status | PENDING, IN_PROGRESS, VERIFIED, FAILED or EXPIRED. |
| environment | "sandbox" or "production", matching the key that created the session. |
| person | The fields read from the document: name, date of birth, document type, document country and jurisdiction, address. |
| checks | One verdict per check — see below. Never one opaque score. |
| confidence | A single overall figure for the verification. Individual checks, including face match, are still reported on their own against a fixed decision line — this field doesn't replace them. |
| verified_until | How long the result is considered current, on a VERIFIED session. |
| failure_reason | Set on a FAILED session — for example FACE_MATCH_BELOW_THRESHOLD or CANCELLED_BY_API. null otherwise. |
| metadata | Whatever you sent when you created the session, unchanged. |
{
"session_id": "ses_9f2c1a",
"status": "VERIFIED",
"environment": "sandbox",
"person": {
"name": "Jordan Smith",
"dob": "1994-03-11",
"document_type": "drivers_licence",
"document_country": "CA",
"document_jurisdiction": "ON"
},
"checks": {
"document": "PASS",
"liveness": "PASS",
"antispoof": "PASS",
"face_match": "PASS"
},
"confidence": 88,
"verified_until": "2027-03-11T00:00:00Z",
"failure_reason": null,
"metadata": { "application_id": "app_9182" }
}Add-onchecks.document, checks.liveness, checks.antispoof and checks.face_match each resolve to PASS, FAIL or SKIPPED. Watchlist screening (checks.sanctions — CLEAR, MATCH or SKIPPED) is an add-on and only appears for accounts that turn it on.
Session lifecycle
Five states, one path.
A session moves forward on its own; you don't drive the state machine, you just read it.
| PENDING | Created. Nobody has opened the verification_url yet. |
|---|---|
| IN_PROGRESS | The applicant opened the link and is somewhere in the capture flow. |
| VERIFIED | Every check ran and the result is ready. Terminal — call the result endpoint or read the webhook. |
| FAILED | At least one check didn't pass. failure_reason says which. Coming soon: a retry that resets the session to PENDING — until then, treat FAILED as terminal and create a new session if the applicant wants to try again. |
| EXPIRED | Nobody finished in time. Sessions expire automatically after a default window (30 minutes); create a new one to try again. |
DELETE /v1/sessions/{id} cancels a PENDING or IN_PROGRESS session early. A session that already reached VERIFIED, FAILED or EXPIRED can't be canceled — it's already done.
Errors
What to plan for.
This is the shortlist worth coding against today. The full, current set of error codes and payloads is documented in the live OpenAPI at /docs.
| Status | Code | Meaning |
|---|---|---|
| 401 | UNAUTHORIZED | The X-Api-Key header is missing. |
| 401 | INVALID_KEY_FORMAT | The key doesn't start with vk_sand_ or vk_live_, or doesn't match a key on the account. |
| 403 | FORBIDDEN | The key is valid but doesn't have access to the session or resource you asked for. |
| 404 | NOT_FOUND | No session, credential or resource exists with that id. |
| 409 | CONFLICT | The session isn't in a state that allows the action — for example, canceling one that's already VERIFIED. |
| 422 | VALIDATION_ERROR | A required field is missing or malformed — for example, return_url isn't a valid http or https URL. |
Webhooks
Signed, delivered, retried.
When a session reaches VERIFIED or FAILED, ShieldID POSTs the shape documented above to your webhook_url.
- Delivered as a POST with a JSON body matching the result shape above.
- Signed with HMAC-SHA256 over the raw request body, sent in an X-Bio-Signature header. Verify it against your webhook secret before you trust the payload.
- Retried up to three times with a linear backoff if your endpoint doesn't answer with a 2xx status.
- Coming soon: a FAILED-session retry that delivers a second webhook once it resolves. Today, treat one webhook per session as the rule.
- Field names here match this reference. If what actually arrives looks different, trust the live payload over this page and tell us — the field list is kept in step with the API, not the other way around.
Verify the signature
import crypto from "node:crypto";
function isValidShieldIdSignature(rawBody, signatureHeader, secret) {
const expected = crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
const provided = signatureHeader || "";
const a = Buffer.from(expected, "hex");
const b = Buffer.from(provided, "hex");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// rawBody must be the exact, unparsed request body
app.post("/hooks/shieldid", express.raw({ type: "application/json" }), (req, res) => {
const ok = isValidShieldIdSignature(req.body, req.headers["x-bio-signature"], process.env.SHIELDID_WEBHOOK_SECRET);
if (!ok) return res.status(401).send("invalid signature");
const result = JSON.parse(req.body);
// result.session_id, result.status, result.checks, …
res.status(200).send("ok");
});Idempotency and retries
Build handlers that are safe to run twice.
There's no dedicated idempotency key yet, on session creation or on webhook delivery.
- A retried webhook delivery can arrive more than once for the same session. Key your handler on session_id + status and make writing the result idempotent.
- Don't retry POST /v1/sessions blindly on a timeout — check whether the session was created first, or you may end up with two sessions for one applicant.
- A dedicated Idempotency-Key header for session creation is planned.
Coming soonIdempotency-Key support on session creation
Keeping up
This reference changes. Watch the changelog.
Field additions, new error codes and endpoint changes are announced there before they land here.
Questions
Reference FAQ.
Next
Put this reference to work.
Follow the quickstart to go from a sandbox key to a verified session and a signed webhook, in order.