Quickstart: hosted flow
Create a verification session on your server, send the person to the hosted flow and read the verdict.
Sekyro AI (by Ibtikar Technologies) verifies your end users' identities through a
hosted flow: your server creates a verification session over the API, your
site opens the returned hosted_url (link or popup), Sekyro AI guides the user
through the checks (document capture → liveness → …; consent to data
processing is recorded implicitly when the user completes the first step), and
you read the verdict back server-side.
This guide is copy-pasteable end-to-end against the staging sandbox.
Two ways to run the checks. This page covers the hosted flow (the user leaves for
verify.sekyro.ai). To run the capture inside your own page, with your branding —@sekyro/nodeon the server,@sekyro/webor@sekyro/reactin the browser — read the Web SDK guide. Session creation (step 1) and the result read (step 4) are identical on both paths.
Pricing and free tier: per-check prices, phone OTP bands and the 500 free verifications per month are on the pricing page.
| Aspect | Staging (this guide) | Production |
|---|---|---|
| API base | https://api-staging.sekyro.ai | https://api.sekyro.ai |
| Hosted flow | https://verify-staging.sekyro.ai | https://verify.sekyro.ai |
| API keys | sk_test_… (sandbox) | sk_live_… (live) |
Prerequisites (provided during onboarding by your Sekyro AI contact):
- An app in the dashboard (Apps → Create app). The app's eKYC profile (a preset such as "Mauritania: document", plus options like "verify phone first") decides which checks every session runs.
- A sandbox secret API key (
sk_test_…) of that app (app page → API keys → Mint key) with thesessions:createandsessions:readscopes. Keep it server-side only — never ship it to a browser or mobile app.
Org-level keys no longer work. A key that isn't bound to an app answers
401 key_not_bound("This API key is not bound to an app. Create an app and use its key."). Create an app and use its key.
1. Create a session
From your server, POST /api/v2/sessions. Authenticate with your secret key
as a bearer token (the X-Api-Key: sk_test_… header is accepted as an
alternative), and send an X-Idempotency-Key so a network retry can never
create two sessions (see Idempotency). With @sekyro/node
this is sekyro.sessions.create({ locale, input }, { idempotencyKey }) — see
the SDK guide.
There is no recipe field: the API key belongs to an app, and the app's eKYC
profile decides the steps. Raw HTTP:
curl -sS https://api-staging.sekyro.ai/api/v2/sessions \
-H "Authorization: Bearer sk_test_…" \
-H "Content-Type: application/json" \
-H "X-Idempotency-Key: $(uuidgen)" \
-d '{
"locale": "fr"
}'An empty body ({}) is valid too.
Request body:
| Field | Type | Notes |
|---|---|---|
ttlMinutes | integer, optional | Per-link session lifetime. Must fall within the app profile's link range (minTtlMinutes…maxTtlMinutes) — otherwise 400 ttl_out_of_policy. When omitted, the profile's defaultTtlMinutes applies. |
locale | "ar" | "fr" | "en", optional | Explicitly pins the hosted-flow language (the link becomes …/{locale}/{token}). Leave it out to let the hosted page detect the user's device language. |
input | object, optional | Step inputs consumed server-side. Its identity-anchor keys (nni, or fullName + birthDate, and phone) also seed the session's prefill — the trusted source for a prefilled phone_otp number and the client-provided data source below. |
flowandtemplateIdwere removed (apps Release 2, 2026-10-18). Sending either answers400 validation_errorwith a field error naming it. Pick the steps in the app's eKYC profile instead.
Session-input fields go under input, never at the top level. A body such
as {"nni": "…"} is rejected with 400 input_field_misplaced
(it would otherwise mint a session with no bound NNI). The message names each
misplaced field and where it belongs — field names only, never the values:
{
"status": "error",
"error": {
"code": "input_field_misplaced",
"message": "`nni` must be sent as `input.nni` — session input fields belong under `input`",
"request_id": "01907f4e-3b1a-7c2d-9e8f-0a1b2c3d4e5f"
}
}The checked names are the identity anchors nni, phone, fullName and
birthDate. Any other unknown top-level field (other than the removed flow /
templateId) is ignored, as before.
The verification flow (2026-10-23)
Availability
The phone-or-email choice, email codes, the identity summary, national eID, the document chip and the ICAO portrait described here are not available in production yet. See Availability.
Every surface (hosted page, web SDK, Android, iOS) shows the same three sections, then the result. The eKYC profile decides what each contains.
- Contact — phone code (WhatsApp or SMS), email code, both, or one of
them, chosen by the person (
contact.mode). - Identity — one method: national eID (Houwiyeti in Mauritania), national
number (NNI) lookup, document photo, or the document chip (native apps with
NFC). After a document or a chip read, or a lookup of an NNI the person
typed, the person can see the details read and confirm them or retake
(
identity.confirmSummary). There is no summary when the identity came from you or a provider: an NNI bound at create (input.nni/subject.nni) without a document step, supplied identity data, or a national eID. - Live photo — one camera session: liveness and face match, plus the ICAO portrait when the profile asks for one (the instructions then add the ICAO tips: glasses off, no hat, neutral expression, plain background). If only the ICAO portrait fails, the person retakes just the photo, with the camera still open.
Consent is one line on the first screen ("By continuing, you agree to the terms", with a link to the terms). It is recorded when the person presses the first button; there is no separate consent screen.
Profile settings (all optional; leaving them out keeps the behaviour from before 2026-10-23):
"contact": { "mode": "one_of" },
"identity": { "confirmSummary": true },
"nationalEid": { "enabled": true, "mode": "alternative", "providers": ["MR.houwiyeti"], "afterPass": "continue" }| Setting | Values | Notes |
|---|---|---|
contact.mode | all (default) · one_of | one_of: the person chooses phone or email; the other step is skipped. Needs phone and email both on (400 contact_one_of_needs_both). Not overridable per session. |
identity.confirmSummary | false (default for existing profiles) · true | Shows the identity details before the live photo, only when they came from the person. After a document or chip read: name, date of birth, nationality, document type, masked document number and NNI, expiry, photo. After a lookup of an NNI the person typed: a shortened view only (first name and last initial, birth year, masked NNI, no photo). Not shown when you bound the NNI at create, supplied the identity data (input.suppliedIdentity) or after a national eID: the session goes straight to the live photo. With a bound NNI and a document step, the document's details are shown. New profiles from presets have it on. It takes effect once Sekyro enables it on the platform (after the SDK releases that show the screen); until then sessions run without it. Not overridable per session. |
nationalEid.afterPass | end (default) · continue | See National eID. |
The identity summary is shown only to the person in the verification flow. It is never in the session API response, webhooks or the dashboard, and confirming it does not change what you receive: the steps and their states are the same as before. A retake re-runs the document (or NNI) step and uses one of its attempts; partner-supplied identity data has no retake.
Identity data source
Every session needs a civil-data source to anchor the identity. The request is rejected if any country enabled in the app profile has no source:
- Managed registry — for countries where we operate a registry (Mauritania /
ANRPTS) and your organisation is entitled (
registryAccess: managed). - Client-provided prefill — pass identity data in
input(nni, orfullName+birthDate). Works for any country. - Document-derived — the profile has a document step (
doc_ocr); the scan itself is the data source.
If none applies, you get 400 flow_needs_data_source with the offending
countries listed:
{
"status": "error",
"error": {
"code": "flow_needs_data_source",
"message": "no identity data source for MR — supply prefill (nni, or fullName + birthDate) or use a flow with a document step",
"countries": ["MR"]
}
}A document-less profile (registry "always", document "off") for a non-entitled country is therefore mintable only when you supply prefill.
National eID (nationalEid)
National eID (Houwiyeti) is not available in production yet. You can try it in the staging sandbox (https://api-staging.sekyro.ai, sk_test_… keys). See Availability.
The eKYC profile can let people prove who they are with their country's national eID instead of the camera. In Mauritania the provider is Houwiyeti: the person types their NNI, opens the Houwiyeti app, and types the 6-digit code the app shows. Other countries' eIDs will be added as new providers, with no change on your side.
Profile setting (dashboard: app page → eKYC profile → National eID; absent = off):
"nationalEid": { "enabled": true, "mode": "alternative", "providers": ["MR.houwiyeti"] }| Field | Values | Notes |
|---|---|---|
enabled | boolean | Off by default. |
mode | alternative (default) · required | alternative: the person picks "Verify with Houwiyeti" or the camera path. required: the eID is the only identity path. |
providers | ["MR.houwiyeti"] | Provider ids are {country}.{provider}. Today only MR.houwiyeti. |
afterPass | end (default) · continue | end: a passed eID completes the session. continue: the person then takes the live photo, matched against the photo the eID returned. Needs liveness on (400 eid_continue_needs_liveness). |
- The eID is offered only when the provider's country is an accepted nationality for the person's location, and only in an environment where your organisation has managed registry access.
- A passed eID check ends the identity part of the session: the document,
chip and registry steps are
skipped. WithafterPass: "end"(the default) liveness, the ICAO portrait and face match areskippedtoo and the session completes; with"continue"they run, and face match compares the live photo with the eID photo (no eID photo and none cached for that person → face match has no reference,no_face_reference:reviewin review mode,failedin automatic mode). A session may overrideafterPass(end→continuenarrows;continue→endneedsoverrides.allowWidening). OTP steps (phone, email) still run first. - In
alternativemode, if the eID cannot be used (eid_not_enrolled,eid_unavailable,eid_locked), the person can switch to the camera path. Inrequiredmode these end the session. - Saving the profile can fail with
400:national_eid_not_available(not enabled on the platform yet),national_eid_not_entitled(no managed registry access),national_eid_needs_provider(no provider chosen),national_eid_country_not_accepted(no provider country is an accepted nationality). The profile response saysfeatures.nationalEidEnabled, andwarnings.nationalEidUnavailableInlists the environments (sandbox,live) where it cannot run. - Where it runs: the hosted page, the headless
@sekyro/webrunner,<SekyroVerification>in@sekyro/react, and the Android and iOS drop-in flows. They all show the same screens: "Verify with Houwiyeti", the NNI, the code with a resend after 60 s (3 new codes at most), and the camera path whenalternativemode allows it. - In the session, the check is
eid_authand the identity method isnational_eid. Sandbox uses a test provider: no real Houwiyeti call. The sandbox fixtures are in the table below. - Price: 6.00 MRU per code sent (pricing): the first send and each successful resend. A failed send (no Houwiyeti account, provider outage, rate limit) and entering the code are not billed.
Sandbox national eID fixtures
| NNI | Code | Result |
|---|---|---|
1000000161 | any | eid_not_enrolled (no Houwiyeti app) |
1000000258 | any | eid_unavailable (simulated outage; alternative mode offers the camera path) |
| any other valid NNI | 000000 | passes with a synthetic test person and a sandbox portrait |
| any other valid NNI | 111111 | eid_code_expired |
| any other valid NNI | anything else | eid_code_invalid (the fifth wrong code locks the eID path) |
The sandbox portrait is an illustration marked "SANDBOX". It is not a photo of a real
person. With afterPass: 'continue' it is the face_match reference, so you can
test the whole path in sandbox: eID, then live photo, then face match. Liveness
runs as usual on your live photo. The face match against the sandbox portrait does
not run the matcher. It passes (score 0.99), or
fails with below_threshold when the face_match submission sets forceOutcome: "fail"
(sandbox only). Live sessions always
run the real matcher against the registry portrait.
The step-by-step flow (method choice, eid step, reasons) is in the
Web SDK guide.
Every success response from the API is wrapped in a {success, data}
envelope. 201 Created:
{
"success": true,
"data": {
"id": "665f1c2ab8d3a94a4c6c2f10",
"status": "created",
"steps": [
{ "checkType": "doc_ocr", "state": "pending" },
{ "checkType": "liveness", "state": "pending" },
{ "checkType": "face_match", "state": "pending" }
],
"hosted_url": "https://verify-staging.sekyro.ai/abc-defg-hjk",
"client_token": "abc-defg-hjk",
"expiresAt": "2026-07-06T13:45:00.000Z"
}
}Display-once:
client_token(and thehosted_urlthat embeds it) is the end user's flow credential and is returned here only — it is never readable again. Hand it to the verified user's browser and nothing else. Do not log it.GETroutes never return it.
Errors use the partner envelope
{"status": "error", "error": {"code", "message", "request_id"}} — quote
request_id when contacting support.
Sandbox test NNIs (population_registry)
In sandbox, the registry never resolves a real citizen — it answers only for
a fixed set of test records. Out of the box that's the built-in test NNI
(passes — your Sekyro AI contact shares it at onboarding; it is deliberately not
printed in public documentation) and any other valid NNI (nni_not_found).
To test more personas, seed your own in the dashboard — sign in, switch the environment toggle to sandbox, and open Settings → Test NNIs (adding requires an admin role):
- Mauritania: enter an NNI — the civil data and portrait resolve from the
registry mirror automatically. The portrait becomes the
face_matchreference, so you can test the biometric step too. - For an NNI the mirror doesn't know, add a first/last name to seed a fake persona. (Non-Mauritanian test personas — civil fields + a mandatory photo — arrive with per-country support.)
- Seeded records are private to your organisation and never overwrite the built-in fixture; remove them from the same screen.
Guard: creating a sandbox session prefilled with an NNI you haven't seeded (and that isn't the built-in test NNI) is rejected at create time:
{ "status": "error", "error": { "code": "sandbox_nni_not_seeded" } }Seed it first, or omit input.nni and let the end user enter it in the flow.
2. Open the hosted flow
Option A — link / redirect
Send the user to hosted_url (full-page redirect, an email/SMS link, or a QR
code for a phone hand-off). When the flow finishes, confirm the outcome
server-side (step 4 / webhooks).
Option B — popup via the web SDK
The @sekyro/web package wraps the popup + event plumbing
(openHosted(hostedUrl, { mode, onEvent }); the lower-level
open({ hostedUrl, onEvent }) on @sekyro/web/popup is the same embed).
Pilot install note: the SDK is not yet published to npm (publish is deferred). For the pilot it is consumed via a
file:dependency or a tarball — your Sekyro AI contact shares thepackages/sekyro-webpackage; install it withnpm install file:../path/to/sekyro-weband import from the same specifiers shown below.
import { openHosted } from "@sekyro/web";
import type { PopupHandle, SekyroEvent } from "@sekyro/web";
let popup: PopupHandle | null = null;
// hostedUrl: the `hosted_url` your server got from POST /api/v2/sessions.
const result = openHosted(hostedUrl, {
mode: "popup",
onEvent(event: SekyroEvent) {
// Treat events as HINTS — confirm server-side before acting (see below).
console.log(event.event, event.sessionId);
if (event.event !== "session.cancelled") popup?.close();
},
});
if ("error" in result) {
// result.error === "popup_blocked": the browser refused the popup.
// Fall back to a full redirect (never done implicitly — leaving your page
// is your call):
openHosted(hostedUrl, { mode: "redirect" });
} else {
popup = result;
}
// Also call popup.close() when your page is done with the flow (e.g. on
// component unmount) — otherwise the message listener lives for the rest of
// the page's lifetime after a genuine abandon.openHosted() returns a PopupHandle ({ close() }) or
{ error: "popup_blocked" }; in redirect mode it navigates this tab and
returns a no-op handle.
Option C — embedded capture in your own page
Run liveness + face match inside your page with <LivenessCapture>
(@sekyro/react) or the headless createVerification (@sekyro/web). This
needs your origins registered, a CSP and self-hosted model assets — all in the
Web SDK guide.
One active popup per page. The popup window name is shared (
sekyro-verify), so alwaysclose()the previous handle before callingopen()again — a reused named window would otherwise feed stale listeners.
Origin allowlist. The hosted page posts events only to your app's registered web origin. Add the exact origin your integration page runs on (e.g.
https://app.example.com) to the app's Web platform in the dashboard; with no registered origin the flow still works but no events are posted. Org-level origins are no longer used. A change to your registered origins is effective on every API instance within 60 seconds (the allowlist is cached per instance for at most that long). Direct media uploads need no registration of their own: the media bucket's CORS rule is platform-wide, and each presigned upload URL is the access control.
3. Receive the popup events
The hosted page posts window.postMessage events that the SDK validates
(sender window + origin + shape) and forwards to your onEvent callback. The
payload is frozen:
{
"source": "sekyro",
"version": 1,
"event": "session.completed",
"sessionId": "665f1c2ab8d3a94a4c6c2f10"
}event is one of (mirroring the webhook event names):
| Event | Meaning |
|---|---|
session.completed | The flow finished and the session has a verdict. |
session.failed | The session failed terminally. |
session.review_pending | The run finished everything it could execute and at least one check needs human review — verdict comes later. |
session.expired | The session's TTL lapsed before completion. |
session.cancelled | Popup-only: the user closed/left the flow mid-way. |
The payload carries the event name and session id only — never scores, personal data, or outcome detail. Fetch the verdict server-side.
Events are at-least-once hints, not a system of record. Two caveats you must design for:
- A popup reload mid-flow fires
session.cancelledeven though the user may resume and finish the flow moments later.- A reload after a terminal state re-posts the terminal event — you can receive
session.completed(etc.) more than once.Always confirm outcomes server-side — via webhooks or
GET /api/v2/sessions/:id— before granting anything to the user.
4. Read the result server-side
Poll the session from your server (sessions:read scope):
curl -sS https://api-staging.sekyro.ai/api/v2/sessions/<SESSION_ID> \
-H "Authorization: Bearer sk_test_…"200 OK:
{
"success": true,
"data": {
"id": "665f1c2ab8d3a94a4c6c2f10",
"status": "completed",
"outcome": "approved",
"steps": [
{ "checkType": "doc_ocr", "state": "passed" },
{ "checkType": "liveness", "state": "passed", "calibratedScore": 0.97 }
],
"expiresAt": "2026-07-06T13:45:00.000Z",
"createdAt": "2026-07-06T12:45:00.000Z"
}
}Two orthogonal fields carry the lifecycle:
status— where the session is:created | in_progress | review_pending | completed | expired | failed.outcome— the verdict,nulluntil the session is terminal:approved | review | rejected.
Each step is {checkType, state, reason?, calibratedScore?}; state is one of
pending | input_required | processing | passed | failed | review | skipped.
reason is a stable machine-readable code explaining a failed/review
state, and calibratedScore (partner API only — never shown to the end user)
is the calibrated confidence for scored ML checks such as liveness and face
match.
Client context (record-only): once recorded, the session also carries
network — { start: { at, country?, region?, city?, asn?, operator?, connectionType, anonymizer: { vpn, tor, hosting }, browserTimezone?, timezoneMismatch? }, countryChanged, ip? } (the end user's IP; never in
webhooks) — and device — { platform, sdk?, os?, osVersion?, browser?, browserVersion?, deviceClass, model?, deviceKeys, signals }, where
deviceKeys are per-organisation hashes (raw device ids are never stored). Both
are informational and do not change outcome. IP geolocation by
DB-IP (CC BY 4.0).
Handling review_pending: the session is parked for human review — this
is not a rejection. Show the user a "verification under review" state and
wait for the session.review_decided webhook (or keep polling until status
leaves review_pending); the final verdict then lands in outcome.
When session.review_pending fires (review-defer, 2026-07-07): a check
landing in the review band no longer halts the run on the spot. The session
stays in_progress, the user keeps going through the remaining checks, and
session.review_pending fires only once the run has exhausted its
executable checks — i.e. every remaining step is either done or blocked on
the pending review. Two consequences to design for:
session.failedcan carry areview-state step. If a later check fails non-retryably while an earlier check is deferred in review, the session fail-fasts tofailed/rejectedand the deferred step keepsstate: "review"insteps. You will getsession.failedand nosession.review_pendingfor that session. Never infer "needs review" from a step'sstatealone — gate on the sessionstatus(review_pending).- A referred clip is not superseded by later submissions. When liveness defers to review and the user then retries a downstream check (e.g. face_match), the eventual human review still evaluates the original liveness clip that entered the review band — the retry does not replace it.
Sessions already parked in review keep their existing behavior; the new timing applies to sessions that enter review after this change.
Webhooks (recommended over polling): subscribe an HTTPS endpoint to the
session.* lifecycle events (session.created, session.review_pending,
session.review_decided, session.completed, session.failed,
session.expired, …). Endpoint registration and the signed-delivery contract
are documented in the API reference and, with
verifyWebhook from @sekyro/node plus Next.js / Express receivers, in the
Web SDK guide.
Verified contacts. When a phone or email check passes, the session and its
session.completed / session.failed webhooks include verifiedContacts with
the full number (E.164) and/or address the person verified, the channel used,
and when:
"verifiedContacts": {
"phone": { "number": "+22236123456", "channel": "whatsapp", "verifiedAt": "2026-10-21T10:00:00.000Z" },
"email": { "address": "[email protected]", "verifiedAt": "2026-10-21T10:01:00.000Z" }
}- It is present only for an OTP check that passed in this session — never
for a failed or skipped check, and never for a number or address you
prefilled that was not verified. The
session.step.completedevent of a passedphone_otp/email_otpstep carries that single entry. - It is sent only to you (this API and your webhooks), never to the person's browser; the hosted page and the dashboard show masked values only.
- It is removed when the session's personal data is purged under your retention settings (the field is then absent).
- You are the controller of these values: store and use them under your own privacy notice and retention rules.
National eID result. When an eid_auth check passed, the session and its
session.completed / session.failed webhooks include the provider that
verified the person, with generic keys (never provider-specific ones):
"steps": [{ "checkType": "eid_auth", "state": "passed" }, { "checkType": "liveness", "state": "skipped" }],
"eid": { "provider": "MR.houwiyeti", "country": "MR", "verifiedAt": "2026-10-22T10:02:11.000Z" }The national number and the code are never returned.
With @sekyro/node: await sekyro.sessions.get(id) returns the same
{ id, status, outcome, steps, expiresAt, createdAt } view, typed.
Deliveries carry the signature in x-sekyro-signature (and, for the transition, the identical
legacy x-ibtikar-signature); @sekyro/node's verifyWebhook +
signatureFromHeaders handle both.
Subjects
Every session is linked to a subject — the same person across all of your apps — once their national number or document is read. The session response and session.* webhooks carry subjectId (and your externalRef, if you sent one in subject.externalRef at creation).
Read a subject and all of its sessions with GET /api/v2/subjects/:id, or find it by your own id with GET /api/v2/subjects?externalRef=…. These endpoints return the person's verified name and birth date, so they need the subjects:read scope. Keys minted from the dashboard after 2026-10-20 include it; mint a new key if an older one gets 403 insufficient_scope.
Scopes. Reading subjects needs subjects:read; revoking or deleting them needs subjects:write. Keys minted from the dashboard after 2026-10-20 carry both. An older key answers 403 insufficient_scope: mint a new one.
What a subject keeps. When a session linked to a subject is approved, Sekyro AI stores the subject's own reference photo (from the national registry, the document's chip or the document, the best source wins), its latest face-match photos and face templates. They are kept until you delete the subject; there is no expiry unless your app sets a maximum age (below). GET /api/v2/subjects/:id adds:
| field | meaning |
|---|---|
status | active or revoked (revokedAt is set when revoked) |
reference | { source: "registry" | "doc_registry" | "chip" | "document", capturedAt }, or null before a photo is stored. The photo itself is never returned |
assurance | registry_biometric, chip_biometric (a document chip read plus a face match), document_biometric, registry_only or document_only, or null |
lastVerifiedAt | last approved full or returning-user session, or null |
returningReady | true when a returning-user session can be created now: active, a reference photo is stored, and the last full verification is within your app's maximum age (when set). Returning-user sessions do not reset that age |
The subject.enrolled webhook ({ subjectId, sessionId, assurance }) tells you when a subject's first reference photo is stored.
Returning users. To verify a known person again with only liveness and a face match against their stored photo, create the session with subject.reuse naming exactly one of subjectId, externalRef or nni:
curl -sS https://api-staging.sekyro.ai/api/v2/sessions \
-H "Authorization: Bearer sk_test_…" \
-H "Content-Type: application/json" \
-H "X-Idempotency-Key: $(uuidgen)" \
-d '{ "subject": { "reuse": { "externalRef": "user-42" } } }'The session view carries returning: true. Once the session is approved, GET /api/v2/sessions/:id also returns identity — the subject's stored name, birthDate and verifiedAt — only when your key also has subjects:read. With sessions:read alone the field is left out: the rest of the session view carries no civil data (steps, reasons, network, device and subject ids only), and the name and birth date stay behind the same scope as GET /api/v2/subjects/:id. Liveness and face match are billed as usual; no registry or document check runs. Create errors: 404 subject_not_found, 409 subject_revoked, 409 subject_no_reference (no approved full verification yet), 409 subject_stale (the last full verification is older than the app's Returning users: maximum age, set in the dashboard's eKYC settings; returning-user sessions do not reset it; no limit by default) and 400 / 409 reuse_target_ambiguous (none or several targets, or an externalRef shared by several subjects).
Revoke. POST /api/v2/subjects/:id/revoke with an optional { "reason": "…" } (kept on your audit trail only, 500 characters at most; do not put personal data in it). The subject can no longer be used for returning-user sessions, including ones already created (see below). Its history is kept. Revoking again returns the same subject. A subject.updated webhook with { subjectId, status: "revoked" } is sent once.
Revoking is permanent: a revoked subject is never re-enrolled. Later full sessions for the same person still link to it (by national number, document or externalRef) but do not store a new reference photo, so it stays unusable for returning-user sessions. To verify the person again as a returning user, delete the subject (below), then run a full session: it creates a new subject, and its approval stores a new reference photo.
A returning-user session already created when its subject is revoked or deleted fails its face_match with reason subject_revoked. The reason is not retryable: create a full session instead.
Delete. DELETE /api/v2/subjects/:id deletes the subject's stored photos, face templates and identifiers, and unlinks its sessions (their results stay). It answers { "id": "…", "deleted": true }, sends a subject.deleted webhook ({ subjectId }) and cannot be undone: a second call answers 404 subject_not_found. A subject of another organization or environment also answers 404.
With @sekyro/node: sekyro.subjects.get(id), .list({ externalRef }), .revoke(id, { reason }) and .delete(id); sekyro.sessions.create({ subject: { reuse: { subjectId } } }) for a returning user.
Idempotency
Mutating endpoints accept an X-Idempotency-Key header (any unique string —
a UUID is ideal). Semantics, scoped to your org + environment + endpoint:
- Replay — same key + same request body within the cache window returns the original response verbatim (the original status code too, and error responses are cached the same as successes). Safe to blind-retry on timeouts.
- TTL — cached for 24 hours (regardless of outcome).
- Collision — same key + different body →
409witherror.code = "idempotency_key_collision". Use a fresh key for a new logical request. - Concurrent duplicate — a second request while the first is still in
flight →
409witherror.code = "idempotency_in_progress"; retry shortly.
API reference
- Browsable reference (Scalar): https://api-staging.sekyro.ai/v1/partner/docs
- OpenAPI document: https://api-staging.sekyro.ai/openapi.json
Questions or onboarding requests: your Sekyro AI contact at Ibtikar Technologies.