# Valyd — Full Documentation (single-file corpus for AI agents) This file concatenates every Valyd documentation page. Each page is delimited by a '=== FILE: ===' marker. The index below (from llms.txt) lists all pages. ================================================================================ === INDEX (llms.txt) === ================================================================================ # Valyd — Identity & Verify API Documentation > Valyd offers two products in one SDK. **Unique Human API** — API-key-only, no user account: > create a session for a workflow containing the liveness and/or face-uniqueness checks > (`verify.sessions.create({ workflowId, redirectUrl })`, NO user token), redirect the person to > Valyd's verification page, and read the verdict from the decision; nothing is saved to an > account. **Reusable Verification** — the user connects with Valyd (standard > OIDC), you read their verified data through `valyd.auth`, run a configured **workflow** on a > verification session (`verify.sessions.create({ workflowId, valydAccessToken, redirectUrl })`) > where every check is available (ID/KYC, liveness, face match, age, professional license, face > uniqueness, location), and passed proofs save to the user's Valyd ID for reuse. > Verification runs through the SDK on Valyd's verification page — there are no raw verification > endpoints to call. Everything runs through the Valyd IdP host. This file is the machine-readable > index of the docs. Every page below is available as clean Markdown at the linked `.md` URL — > fetch those directly; do not try to scrape the HTML site (it is a client-rendered SPA and > returns no static content). ## How to integrate (decision flow) Ask the developer these questions, in order; each answer pins one product + credential + one `.md` page to fetch and follow. Credentials must be supplied by a human (see "Human-only steps") — never guess them. - Q0 (context only): Existing app or starting fresh? Tells you where the connect button and keys go. - Q1 — Team/workforce you onboard AND control (add members, gate who can log in, assign roles)? - YES → **Organizations**. Members join by face-activation, then sign in with Connect with Valyd; roles owner/admin/developer/member; the `valyd_org_member_id` claim (a `vmem_…` id) tells you who logged in; unlimited verifications on a per-active-member monthly plan (14-day trial; current price → https://docs.valyd.vip/docs/organizations/billing.md). Credential: `client_id` + `client_secret` (connect) + Members API (`X-Client-Id`/`X-Client-Secret`). Fetch: https://docs.valyd.vip/docs/organizations.md - NO → continue. - Q2 — Just need to know a person is LIVE and/or UNIQUE, with only an API key (no login, no account, result returned to you, nothing saved)? → **Unique Human API**. Create a session with NO user token for a workflow containing the liveness (anti-spoof) and/or face-uniqueness checks — `verify.sessions.create({ workflowId, redirectUrl })` — redirect the person to `session.url` (Valyd's verification page captures a live burst), then read `verify.sessions.decision(sessionId)`: antispoof data carries `human_score`; face_uniqueness data carries a stable `valyd_uuid` + `registered: "new" | "existing"` (one-person-one-account dedup). Credential: App key (via the SDK) + `workflow_id`. Fetch: https://docs.valyd.vip/verify/unique-human.md (Liveness: https://docs.valyd.vip/verify/unique-human/antispoof.md · Uniqueness: https://docs.valyd.vip/verify/unique-human/face-uniqueness.md) - Q3 — Need reusable verified identity — KYC, ID, face match, age, professional license, location — or want to read/write proofs on the user's own Valyd account? → **Reusable Verification**. The journey is: **Connect with Valyd** (standard OIDC) → read the account's verified data (proofs like `id_verified`, age bands, licenses — free, instant) → run a configured **workflow** on a verification session with the user's token — `verify.sessions.create({ workflowId, valydAccessToken, redirectUrl })` — → get the decision via `verify.sessions.decision()` or a signed webhook. Every check is available (ID/KYC, liveness, face match, age, professional license, face uniqueness, location); passed proofs save to the user's Valyd ID (reusable across apps); PII stays encrypted with Valyd — you get the decision + proofs, never raw PII. Credential: `client_id` + `client_secret` (connect) + App key (via the SDK) + `workflow_id` + the user's `valyd_access_token`. Fetch: https://docs.valyd.vip/verify/intro.md then https://docs.valyd.vip/verify/quickstart.md NOTE: ID/KYC, face match, age, professional license, and location are NOT direct public APIs — they run ONLY as workflow checks inside a Reusable Verification session. - Q4 — Sign-in only (connect the user, run no check)? → **Connect with Valyd** (OAuth2/OIDC — part of Reusable Verification; it can also serve as your app's sign-in). Credential: `client_id` + `client_secret`. Fetch: https://docs.valyd.vip/docs/authentication.md Then clarify (as relevant), fetching the named `.md`: - Which workflow checks? ID/KYC, liveness, face match, age (age *bands* like `is_18_plus`, never the DOB), professional license, location/EVV presence, face uniqueness. → https://docs.valyd.vip/verify/types.md (Checks reference) - Reusable identity (KYC-reuse)? Save the passed check as a proof — that's **Reusable Verification** (needs the user's `valyd_access_token`). A Unique Human API call saves nothing. → https://docs.valyd.vip/docs/user-token.md - Result delivery? Async signed webhook or `verify.sessions.decision()` (Reusable Verification session) vs the synchronous return of a Unique Human API SDK call. → https://docs.valyd.vip/verify/webhooks.md and https://docs.valyd.vip/verify/statuses.md - Need RAW identity data (DOB, document number, address)? Requires the consent flow (the user approves on their device); otherwise you get proofs / verified-status only. → https://docs.valyd.vip/docs/request-data.md - Connect scopes? profile (legal name, country, verified status), verifications (proofs/badges), email, doctor_license. → https://docs.valyd.vip/docs/scopes.md - Professional license check? Runs as a workflow check in a **Reusable Verification** session (a connected user's verification session); the name comes from the account and the verified badge saves to their Valyd ID. → https://docs.valyd.vip/verify/intro.md - Drop-in button or your own OIDC library? "Connect with Valyd" button (https://docs.valyd.vip/docs/overview.md) vs any OIDC library via discovery (https://docs.valyd.vip/docs/oidc.md). - Credentials source (IMPORTANT): a human creates the app + keys in the Developer Portal (dev.valyd.vip); an agent CANNOT mint `client_id`/`client_secret`/API key/`workflow_id` — pause and ask, never guess. → https://docs.valyd.vip/docs/create-project.md - Billing? Organization = unlimited verifications on a per-active-member monthly plan (billing starts the moment a member face-scans); non-org = pay per check. Current price + trial → https://docs.valyd.vip/docs/organizations/billing.md Full narrative version of this tree (with example calls and the two golden rules) is the Agent Integration Guide, rendered at https://docs.valyd.vip/ai/agent-guide. ## Instructions for AI agents - Fetch pages with `curl -sL `; each `.md` is self-contained (prerequisites, steps, verification, and common errors). - **Base URLs (one canonical host per product — use these exactly):** - Valyd ID / OAuth / OIDC (API host): `https://idp.valyd.vip` - Verification API (same host): `https://idp.valyd.vip` - Developer Portal (human sign-in, projects, keys): `https://dev.valyd.vip` - Documentation (this site, `.md` + OpenAPI): `https://docs.valyd.vip` - **There is NO password login.** Human sign-in to the Developer Portal is passwordless — an emailed magic link OR face ("Connect with Valyd"). Never look for, store, or send a password. Portal sign-in, linking a face, and switching accounts are human-only steps an agent cannot perform. - **Credentials cannot be created via API.** A human must sign up and generate them in the portal (see "Human-only steps" at the bottom). Pause and request these values rather than guessing: `client_id`, `client_secret` (Valyd ID); App `API key`, `workflow_id`, webhook signing secret (Verification APIs). - Auth is not interchangeable: connect/account endpoints use an OIDC Bearer access token; verification runs through the SDK with your App key (server-side). A Reusable Verification session also carries the connected user's `valyd_access_token`. - Token exchange and webhook signature verification MUST run server-side (never expose the client secret or webhook secret to a browser). - **Machine-readable API specs (OpenAPI 3.1):** for endpoint discovery, schemas, and codegen, fetch `https://docs.valyd.vip/openapi/valyd-id.json` and `https://docs.valyd.vip/openapi/valyd-verify.json`. - **Capability matrix (machine-readable):** to pick a path deterministically, fetch `https://docs.valyd.vip/agent/capabilities.json` — a JSON array mapping each real goal to its `path`, `product`, `auth`, `credential`, `billable`, `pii_mode`, `saves_proof`, and the `.md` `doc` to follow (connect, account-read, reusable-verification session, Unique Human API, organizations). ## Valyd ID — Getting started - [Overview](https://docs.valyd.vip/docs/overview.md): When to use OIDC connect, what it reads from an account, and when to run a verification session instead. - [Create a Project & Get Credentials](https://docs.valyd.vip/docs/create-project.md): Dev-portal setup — sign up, create a project, set redirect/scopes, copy client_id + client_secret. (Human-only.) - [Quickstarts — Connect with Valyd](https://docs.valyd.vip/docs/quickstarts.md): Stack-by-stack complete flows — Node.js, Next.js, Python, PHP (Laravel), cURL, or any OIDC library — each a full working OIDC flow (state comparison, code exchange, userinfo). (The old /docs/quick-start.md is a compatibility redirect to this page.) - [OIDC session security](https://docs.valyd.vip/docs/tokens.md): Store and validate state, nonce, PKCE, and redirect URI as one server-side transaction. ## Valyd ID — Authentication & integration - [Authentication (OAuth2 / OIDC flow)](https://docs.valyd.vip/docs/authentication.md): Connect with Valyd — authorization URL, state/nonce CSRF, callback handling, code→token exchange, refresh rotation. - [OpenID Connect (OIDC) Integration](https://docs.valyd.vip/docs/oidc.md): Discovery endpoint, manual config, Mendix setup, claim/user mapping, testing, troubleshooting, security. ## Valyd ID — Reference - [API Reference (endpoints)](https://docs.valyd.vip/docs/endpoints.md): OIDC discovery, authorize, token, JWKS, UserInfo, licenses, and account verification proofs under `/api/auth/oidc`. - [OAuth2 Scopes](https://docs.valyd.vip/docs/scopes.md): profile, verifications, doctor_license, zkp, mcp — what data each unlocks and how to request them. - [Errors & troubleshooting](https://docs.valyd.vip/docs/errors.md): Every error code with cause and fix, plus the app-level "state mismatch" CSRF rejection. - [Changelog](https://docs.valyd.vip/docs/changelog.md): @valyd/sdk release history, including the v1.10.1 secure OIDC transaction helpers. - [Request user data (consent)](https://docs.valyd.vip/docs/request-data.md): Ask a Valyd user for raw attributes (legal_name, dob, country). The user approves in their Valyd app; values return end-to-end encrypted (X25519 sealed box) via valyd.auth.requestAttributes + getAttributeResult. Needs libsodium-wrappers for self-custody. ## Verification APIs — Guides - [Reusable Verification](https://docs.valyd.vip/verify/intro.md): The journey — Connect with Valyd, read verified data, run a workflow on a verification session with the user's token, get the decision; every check available, passed proofs save to the account. - [Run a verification (quickstart)](https://docs.valyd.vip/verify/quickstart.md): Get an App key, connect the user, and run `verify.sessions.create({ workflowId, valydAccessToken, redirectUrl })`. - [Create a workflow (setup)](https://docs.valyd.vip/verify/setup.md): Developer Portal setup — the App API key (shown once), a `workflow_id`, and the webhook secret. (Human-only UI steps.) - [Workflows](https://docs.valyd.vip/verify/workflows.md): Configure which checks a verification session runs, in the Developer Portal, and get the `workflow_id`. - [Unique Human API](https://docs.valyd.vip/verify/unique-human.md): API-key-only, no user account — a workflow session (no user token) on Valyd's verification page running the Liveness and/or Uniqueness checks; verdict returned to you, nothing saved to an account. See [Liveness](https://docs.valyd.vip/verify/unique-human/antispoof.md), [Uniqueness](https://docs.valyd.vip/verify/unique-human/face-uniqueness.md), and [Common errors](https://docs.valyd.vip/verify/unique-human/errors.md). (ID/KYC, face match, age, professional license, and location run only as workflow checks in a Reusable Verification session.) - [Developer Portal](https://docs.valyd.vip/verify/console.md): Apps, API keys, workflows, webhooks, SSO. (Human-only UI steps.) - [Node SDK](https://docs.valyd.vip/verify/sdk.md): Install/init @valyd/sdk, resources, types, error handling, Express webhook handler. - [Webhooks](https://docs.valyd.vip/verify/webhooks.md): Register a callback URL and verify HMAC-SHA256 signatures over the raw request body. - [Statuses & decisioning](https://docs.valyd.vip/verify/statuses.md): Every session/check status, what it means, and how to act on it — plus `verify.sessions.decision()`. - [Checks reference](https://docs.valyd.vip/verify/types.md): Every workflow check — ID/KYC, liveness, face match, age bands, professional license, face uniqueness, location. - [Consent & data access](https://docs.valyd.vip/verify/data-sharing.md): What the user consents to share, what your app receives (proofs, never raw PII), and what stays with Valyd. ## Verification APIs — API Reference - [API Reference](https://docs.valyd.vip/verify/api-reference.md): Sessions, Workflows, the Unique Human API checks, Decision, and Errors & rate limits — all driven through the SDK with your App key. ## API specs (OpenAPI 3.1) - [Valyd ID OpenAPI](https://docs.valyd.vip/openapi/valyd-id.json): OIDC connect and account-resource endpoints, schemas, auth, and errors. - [Verification APIs OpenAPI](https://docs.valyd.vip/openapi/valyd-verify.json): Sessions, the Unique Human API checks, credentials, and the verification webhook (HMAC-SHA256). Use for SDK/codegen. ## Downloads & tooling - [Postman collection](https://docs.valyd.vip/valyd-postman-collection.json): Importable collection of the Valyd ID API requests. - [Node SDK starter](https://docs.valyd.vip/downloads/valyd-sdk-starter.zip): Starter scaffold for integrating Valyd. ## Developer portal — accounts, organizations & teams - [Developer accounts & sign-in](https://docs.valyd.vip/docs/developer-accounts.md): Passwordless sign-in (emailed magic link or face / Connect with Valyd — there is NO password), connecting a Valyd face identity to an email-only account, and how one identity can own several console accounts (one per company) with an account switcher. Human-only portal actions. - [Organizations & teams](https://docs.valyd.vip/docs/organizations.md): Individual vs organization accounts, the owner/developer/member roles, shared apps, public vs private apps, adding a workforce by face, and one-account billing. ## Human-only steps (an agent must pause and request these) - Sign up for a Valyd account and log in to the Developer Portal: https://dev.valyd.vip (no KYC required). - Create a Valyd ID project; copy `client_id` and `client_secret` (secret shown once — regenerate if lost). - Register the exact redirect/callback URL on the project (must match what you send; no trailing slash). - Sign in to the Developer Portal (https://dev.valyd.vip) with Valyd SSO; copy the App API key (shown once). - Create a Verify workflow and copy its `workflow_id`; set the webhook URL and signing secret. ================================================================================ === FILE: https://docs.valyd.vip/docs/overview.md === ================================================================================ > Source: https://docs.valyd.vip/docs/overview --- product: valyd-id api_version: oidc auth: client-credentials billable: false pii_mode: proofs human_setup_required: true source_of_truth: openapi --- # Connect with Valyd > 🔑 **Auth:** `client_id` + `client_secret` (server-side) · 👤 Standard OpenID Connect · 📖 **After connecting:** read the user's account with a Bearer access token **Connect with Valyd** establishes the user's reusable Valyd identity in your app — the first step of [Reusable Verification](/verifications). It is built on **OpenID Connect (OIDC)**, so it can also serve as your app's sign-in. The user connects, and you get their profile, licenses, and verification proofs. Add the button, done: ```html ``` That's the whole front end. > 🌐 **Your OIDC provider is `https://idp.valyd.vip`** — discovery at > [`/api/.well-known/openid-configuration`](https://idp.valyd.vip/api/.well-known/openid-configuration). > The button and SDK point at it for you; using your own library? See > [Use your own OIDC library](/docs/oidc). Standard OpenID Connect, end to end — the button and SDK handle every step below: ```mermaid sequenceDiagram participant U as User participant App as Your app participant V as Valyd (idp.valyd.vip) U->>App: Click "Connect with Valyd" App->>V: authorize V->>U: Face login + consent V-->>App: Callback with code App->>V: Exchange code → tokens App->>V: GET userinfo (Bearer) V-->>App: Profile + proofs ``` ## 1. Get your credentials In the [Developer Portal](https://dev.valyd.vip) create an app, enable the [scopes](/docs/scopes) you need (`profile` is on by default), and register your exact callback URL. Copy the `client_id` and one-time `client_secret`. New accounts start with a **$100 welcome credit** for [testing](/docs/testing). ![The app's Quick setup page: setup checklist, client credentials, and OIDC endpoints](/images/screenshots/portal-app-quicksetup.png) ## 2. Handle the callback One backend route at the `data-redirect-uri` you set on the button (Express shown — any framework works; `req` is the incoming callback request): ```typescript import express from "express"; import cookieParser from "cookie-parser"; import { ValydClient } from "@valyd/sdk"; // npm i @valyd/sdk cookie-parser const app = express(); app.use(cookieParser()); const valyd = new ValydClient({ clientId, clientSecret, redirectUri }); app.get("/auth/valyd/callback", async (req, res) => { const { user } = await valyd.handleCallback(req.url, { expectedState: req.cookies.valyd_oidc_state, // the button set this cookie nonce: req.cookies.valyd_oidc_nonce, }); // user.valyd_id — stable ID; user.id_verified — identity proof res.redirect("/dashboard"); }); ``` Done. See the [complete example](/docs/quickstart/node), or plug in [your own OIDC library](/docs/oidc) instead. ## The user is connected — see what they already have | Endpoint | What it reads from the user's account | | --- | --- | | [`GET /api/auth/oidc/userinfo`](/docs/endpoints#get-userinfo--get-user-profile) | Profile: legal name, username, `id_verified` | | [`GET /api/auth/oidc/licenses`](/docs/endpoints#get-licenses--get-professional-licenses) | Professional licenses already verified on the account | | [`GET /api/auth/oidc/verifications`](/docs/endpoints#get-verifications--get-identity-verifications) | Saved verification proofs and badges | These read what the account already holds, gated by the scopes the user approved. Raw identity attributes (DOB, document data) go through the explicit [consent flow](/docs/request-data). KYC not done yet? License missing? Run your [workflow](/verifications/workflows) **for the user** — [create a session](/verifications/quickstart) with their `valyd_access_token` — and the passed proof lands on their Valyd ID. Next time you just read it here. ## Security rules - Keep `client_secret`, tokens, and the OIDC transaction on your backend. - Register an exact HTTPS redirect URI. - Never build your own state, PKCE, nonce, or JWT validation when the SDK can do it. - A verification API key is not an OIDC token and must not be placed in a browser. ================================================================================ === FILE: https://docs.valyd.vip/docs/api-key-lifecycle.md === ================================================================================ > Source: https://docs.valyd.vip/docs/api-key-lifecycle # API key lifecycle How Valyd App API keys are issued, rotated, and revoked — and the access model they imply. The facts below are confirmed from the [Developer Portal](/verifications/console) docs, the published OpenAPI spec, and the [Verify API reference](/verifications/api-reference). Anything not stated in these docs is available from support — contact **support@valyd.id**. ## Issuance - One app in the Developer Portal carries both identities: an OAuth `client_id` / `client_secret` for **Connect with Valyd**, and a Verify **App API key** (prefix `vrf_…`) for the verification APIs. - **API keys are created by a human in the Developer Portal — never via API.** - The key is **shown once at creation**. Copy it immediately and store it server-side. - You can create **multiple apps** (for example Test and Production), each with its own key. ## Authentication - Verify requests authenticate with `X-API-Key: `. A `Bearer` token carrying the same key is also accepted. - A missing or wrong key returns `401 invalid_api_key`; an inactive or misconfigured app returns `401 invalid_client` / `403 unauthorized_client` (see the [error catalog](/docs/errors#3-complete-code-catalog)). ## Rotation - **If a key is lost or compromised, rotate it in the Console** to generate a new one. - **Webhook signing secrets are separately rotatable** per app — see [Webhooks](/verifications/webhooks). - **Zero-downtime overlap** — whether a rotated key leaves the previous key valid for a grace period (dual-key overlap) is not documented here. If your rollout needs a coordinated cutover, contact **support@valyd.id**. ## Revocation & visibility | Item | Status | | --- | --- | | Explicit revoke (independent of rotation) | Not documented — contact **support@valyd.id** | | Last-used timestamp / usage visibility | Not documented — contact **support@valyd.id** | | Per-key scopes / least-privilege | **All-or-nothing** — an App API key is a full-authority credential for its app; there is no per-key scoping (see [Access model](#access-model-the-key-is-the-authority) below) | | Number of active keys per app | Not documented — contact **support@valyd.id** | ## Access model: the key IS the authority An App API key is a **full-authority credential for that app** — there is no separate reviewer or admin role layered on top of it. The clearest consequence is manual decision override: > The manual-decision endpoint (`PATCH /api/v2/session/{id}/status`) is authenticated by your > app's API key, so **any holder of that key can force a session's terminal decision** > (`APPROVED` or `DECLINED`). There is no separate reviewer role. This is documented in the [Verify API reference — manual override](/verifications/api-reference#patch-apiv2sessionidstatus--manual-override). Two guardrails still apply on the backend: - Only an `IN_REVIEW` session can be manually decided — any other state returns `409 review_not_pending`. - A manual `APPROVED` still requires the session's ID, liveness, and face-match checks to have passed, or it returns `409 required_face_checks_incomplete`. A manual `DECLINED` has no such gate. Because the key is this powerful, treat it as a high-value secret: store it server-side only, never ship it to a browser or mobile client, and rotate on any suspected exposure. ## See also - [The Developer Portal](/verifications/console) - [Verify API reference](/verifications/api-reference) - [Trust Center](/docs/security-trust) ================================================================================ === FILE: https://docs.valyd.vip/docs/api-reference.mdx.md === ================================================================================ > Source: https://docs.valyd.vip/docs/api-reference.mdx --- title: Full OpenAPI spec — Valyd ID API description: Machine-readable OpenAPI 3.1 specification for the Valyd ID (OIDC) API. --- # Full OpenAPI spec — Valyd ID API The complete, machine-readable OpenAPI 3.1 specification for the Valyd ID API — OIDC discovery, authorize/token/JWKS, the account resource endpoints, schemas, auth, error envelopes, and response examples. Use it for SDK code generation, API tooling, and endpoint discovery. - **Download / fetch:** [`/openapi/valyd-id.json`](/openapi/valyd-id.json) - **Postman collection:** [`/valyd-postman-collection.json`](/valyd-postman-collection.json) ```bash curl -sL https://docs.valyd.vip/openapi/valyd-id.json ``` For a human-readable walkthrough of every endpoint — request formats, parameters, and example responses — see the [API reference](/docs/endpoints) page. Authentication and token flows are covered in [OAuth / OIDC flow](/docs/authentication). ================================================================================ === FILE: https://docs.valyd.vip/docs/audit-logging.md === ================================================================================ > Source: https://docs.valyd.vip/docs/audit-logging # Audit logging What activity a Valyd integration can observe, and where a developer or org admin reads it. Four things are verifiable today from Valyd's own docs — the `X-Request-Id` on every response, the verification event records, the webhook delivery log in the Developer Portal, and org login attribution via the `valyd_org_member_id` claim. Everything beyond those observable signals (log retention, export/SIEM, tamper-evidence, an account-level admin audit trail) is not documented in these docs; contact **support@valyd.id** for the current details. Nothing here asserts an audit capability Valyd has not documented. ## Request correlation: `X-Request-Id` Every Valyd response carries an **`X-Request-Id`** header — the correlation id for that single request. It is the one identifier you quote to support to have a specific call traced, and it is the value support asks for first. Log it on your side against your own request records so a later investigation can line up your logs with Valyd's. See [Errors & troubleshooting](/docs/errors#contacting-support) for the exact support contract, and never quote API keys or tokens alongside it. ## Verification events Each verification session produces durable, machine-readable event records you can act on: - **Terminal outcomes** — `verification.approved` / `verification.declined`. - **Non-terminal states** — `verification.in_review` (a manual/agent review is pending), `verification.abandoned` (the user left Valyd's verification page), `verification.expired` (the session TTL elapsed). Each event carries a stable **`event_id`** (mirrored in the `X-Valyd-Event-Id` header) and the `session_id` that produced it. The decision API (`GET /api/v2/session/{id}/decision`) is the **authoritative** record of a session's result — poll or fetch it any time, independent of whether a webhook was delivered. Full event shape: [Webhooks](/verifications/webhooks). ## Webhook delivery log Every webhook attempt — successful or failed — is recorded in the **Developer Portal**, on your application's **Verification** page under **Recent webhook deliveries**. For each attempt the log shows the destination URL, the exact payload and headers Valyd sent, the receiver's HTTP status and response body, and any transport error. A delivery is retried automatically (up to 10 attempts across roughly 2.5 hours), and the **Resend** button re-queues any delivery on demand — a resend carries the **same** `X-Valyd-Event-Id`, so an idempotent handler treats it as the same event. This is the built-in place to confirm what Valyd told your endpoint and when. Details: [Webhooks — delivery log and manual resend](/verifications/webhooks). ## Who signed in: `valyd_org_member_id` For an [organization](/docs/organizations), you always know exactly which of your people just logged in. When an org member signs in with Connect with Valyd, the **`valyd_org_member_id`** claim returns on the OIDC [userinfo response](/docs/endpoints#get-userinfo--get-user-profile) and in the ID token (scoped to your org's client). Its value is that member's `vmem_…` id — your join key between your own roster and the login — so you can attribute and track logins against your own records. See [Organizations & teams](/docs/organizations#how-organizations-work). ## Not yet published These are standard audit questions an enterprise review asks. They are not documented in these docs today — contact **support@valyd.id** for the current answers. Retention windows for the delivery log and request logs are tracked separately on [Data retention](/docs/data-retention). | Question | Answer | | --- | --- | | History depth in the Portal | Not documented — contact **support@valyd.id** | | Export / SIEM | Not documented — contact **support@valyd.id** | | Tamper-evidence | Not documented — contact **support@valyd.id** | | Admin audit trail | Not documented — contact **support@valyd.id** | | Who can read the logs | Not documented — contact **support@valyd.id** | ## See also - [Errors & troubleshooting](/docs/errors) — the `X-Request-Id` support contract - [Webhooks](/verifications/webhooks) — event shape, retries, and the delivery log - [Data retention](/docs/data-retention) — how long logs and records are kept - [Support & escalation](/docs/support-escalation) — what to send when you contact support - [Trust Center](/docs/security-trust) ================================================================================ === FILE: https://docs.valyd.vip/docs/authentication.mdx.md === ================================================================================ > Source: https://docs.valyd.vip/docs/authentication.mdx # Add Connect with Valyd > 🔑 **Auth:** `client_id` + `client_secret` (server-side) · 👤 **Standard OpenID Connect** — Connect can also serve as your sign-in · 📖 **After connecting:** read the account with a Bearer access token > **Prefer the concept pages?** See [Flows](/docs/flows/authorization-code) for the picture-first > walkthrough and [Tokens](/docs/tokens) for what each returned token is for. > **These raw-HTTP examples demonstrate the protocol.** For production, use a maintained OIDC > library or `@valyd/sdk` — they validate issuer, audience, signature, expiry, `state`, `nonce`, > and PKCE for you. ### Prerequisites - Client ID and Client Secret (get these from the Developer Portal → your app → Credentials: https://dev.valyd.vip — the portal lists apps as "projects"; same object). - A registered redirect/callback URL matching what you send as `redirect_uri`. - Environment variables set on your server: - `VALYD_CLIENT_ID` (get from Developer Portal: https://dev.valyd.vip) - `VALYD_CLIENT_SECRET` (get from Developer Portal: https://dev.valyd.vip) - `VALYD_REDIRECT_URI` (the exact callback URL you registered, e.g. `https://yourapp.com/callback`) ### Steps 1. **Construct the authorization URL.** Redirect users to the OIDC authorization endpoint with your client id, redirect URI, scopes, and a freshly generated random `state` (and `nonce`): ```text https://idp.valyd.vip/api/auth/oidc/authorize?client_id={client_id}&redirect_uri={redirect_uri}&response_type=code&scope={scopes}&state={state}&nonce={nonce} ``` Parameters: | Parameter | Required | Description | | --- | --- | --- | | `client_id` | Yes | Your application's Client ID from the Developer Portal (https://dev.valyd.vip). | | `redirect_uri` | Yes | The URL to redirect to after authentication. Must match the URL registered on your application. | | `response_type` | Yes | Must be `code`. | | `scope` | Yes | Space-separated list of scopes, URL-encoded. **MUST include `openid`.** Example: `openid%20profile%20verifications`. | | `state` | Yes | A random value you generate and store. Echoed back unchanged on the callback — compare it there (CSRF protection). | | `nonce` | Recommended | A random value bound into the `id_token`. Verify the `nonce` claim after the exchange (replay protection). | **Expected output:** A fully-formed URL string. Example with encoded scopes `openid profile verifications`: ```text https://idp.valyd.vip/api/auth/oidc/authorize?client_id=YOUR_CLIENT_ID&redirect_uri=https://yourapp.com/callback&response_type=code&scope=openid%20profile%20verifications&state=RANDOM_STATE&nonce=RANDOM_NONCE ``` 2. **Create and store one secure transaction (recommended: SDK).** The SDK generates strong `state`, `nonce`, and S256 PKCE values together. Store the returned object in a server-side session: ```javascript import { ValydClient } from "@valyd/sdk"; const valyd = new ValydClient({ clientId: process.env.VALYD_CLIENT_ID!, clientSecret: process.env.VALYD_CLIENT_SECRET!, redirectUri: "https://yourapp.com/callback", }); const transaction = valyd.createAuthorizationRequest({ scope: ["profile", "verifications", "zkp"], // "openid" is added automatically }); req.session.valydOidc = transaction; // server-side only; includes the PKCE verifier res.redirect(transaction.url); ``` **Expected output:** HTTP 302 to the OIDC authorize endpoint with `state`, `nonce`, and `code_challenge`. The full transaction stays server-side for step 6. 3. **User consents on the Valyd consent screen.** Valyd shows the consent screen with the requested scopes and, on approval, issues a one-time authorization `code`. (Scopes must be enabled on your app in the Developer Portal before they can be requested — the same model as Google's consent-screen scopes.) **Expected output:** Valyd redirects the user's browser to your callback URL with the code attached. Codes are single-use and short-lived — exchange immediately. 4. **Receive the callback on your server.** The user is redirected to your registered callback URL with the authorization code and your original state: ```text https://yourapp.com/callback?code=AUTH_CODE_HERE&state=RANDOM_STATE ``` Callback query parameters: | Parameter | Description | | --- | --- | | `code` | The one-time authorization code. Single-use; exchange immediately. | | `state` | The **exact `state` value you sent** on `/authorize`, echoed back unchanged. Compare it to your stored value. | **Expected output:** Your `/callback` route is invoked with `code` and `state` (and possibly `error`) present in the query string. 5. **CSRF check — compare the `state`.** The callback `state` must strictly equal the value you stored in step 2. Reject the request on any mismatch, before touching the code. ```javascript const stored = readStoredOAuthValues(req); // your cookie/session read if (!stored?.state || req.query.state !== stored.state) { return res.status(400).send("state mismatch"); } ``` **Expected output:** On a legitimate flow the values are identical and processing continues. On a mismatch (missing cookie, forged callback), respond HTTP 400 and stop. 6. **Exchange the code for tokens, then fetch the user (server-side).** Exchange the `code` immediately (it is single-use). The callback handler in your stack — pick your language once and every code block on the page follows: ```javascript // Recommended: @valyd/sdk import { ValydClient } from "@valyd/sdk"; const valyd = new ValydClient({ clientId: process.env.VALYD_CLIENT_ID, clientSecret: process.env.VALYD_CLIENT_SECRET, redirectUri: process.env.VALYD_REDIRECT_URI, }); app.get("/callback", async (req, res) => { const transaction = req.session.valydOidc; if (!transaction) return res.status(400).send("login transaction missing"); delete req.session.valydOidc; const callbackUrl = new URL(req.originalUrl, process.env.VALYD_REDIRECT_URI).toString(); const { tokens, user } = await valyd.handleCallback(callbackUrl, { transaction }); // ...set your own app session, then redirect to /dashboard }); ``` `handleCallback()` compares state, sends the PKCE verifier, exchanges the code, verifies RS256/JWKS plus issuer/audience/expiry/nonce, and fetches UserInfo. ```python from flask import Flask, request, redirect, session, abort import requests app = Flask(__name__) @app.route("/callback") def callback(): code = request.args.get("code") state = request.args.get("state") if not code: return "missing code", 400 if not state or state != session.pop("valyd_state", None): return "state mismatch", 400 response = requests.post( "https://idp.valyd.vip/api/auth/oidc/token", json={ "grant_type": "authorization_code", "client_id": "YOUR_CLIENT_ID", "client_secret": "YOUR_CLIENT_SECRET", "code": code, "redirect_uri": "https://yourapp.com/callback", }, ) tokens = response.json() # top-level: access_token, refresh_token, id_token, ... session["access_token"] = tokens["access_token"] return redirect("/dashboard") ``` ```php 'https://idp.valyd.vip/api/auth/oidc/token', CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode([ 'grant_type' => 'authorization_code', 'client_id' => 'YOUR_CLIENT_ID', 'client_secret' => 'YOUR_CLIENT_SECRET', 'code' => $code, 'redirect_uri' => 'https://yourapp.com/callback' ]), CURLOPT_HTTPHEADER => ['Content-Type: application/json'], CURLOPT_RETURNTRANSFER => true, ]); $tokens = json_decode(curl_exec($ch), true); // top-level token JSON $_SESSION['access_token'] = $tokens['access_token']; header('Location: /dashboard'); ``` ```java @GetMapping("/callback") public ResponseEntity callback(@RequestParam String code, @RequestParam String state, HttpSession session) { String expected = (String) session.getAttribute("valyd_state"); if (expected == null || !expected.equals(state)) { return ResponseEntity.badRequest().body("state mismatch"); } session.removeAttribute("valyd_state"); RestTemplate rt = new RestTemplate(); Map body = Map.of( "grant_type", "authorization_code", "client_id", "YOUR_CLIENT_ID", "client_secret", "YOUR_CLIENT_SECRET", "code", code, "redirect_uri", "https://yourapp.com/callback" ); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); rt.postForEntity("https://idp.valyd.vip/api/auth/oidc/token", new HttpEntity<>(body, headers), String.class); return ResponseEntity.status(302).header("Location", "/dashboard").build(); } ``` **Expected output:** `exchangeCode(code)` returns tokens (`accessToken`, `refreshToken`, `idToken`, `expiresIn`, `scope`); `getUserInfo(accessToken)` returns the user's profile data. Then set your own app session and redirect (e.g. to `/dashboard`). #### Token exchange without the SDK (raw HTTP) The token endpoint is `POST https://idp.valyd.vip/api/auth/oidc/token` with a JSON body. The response is a **standard top-level token JSON** — `access_token`, `refresh_token`, `id_token`, `expires_in`, `scope`, `token_type` at the root (no `data` wrapper). Request shape: ```http POST /api/auth/oidc/token HTTP/1.1 Host: idp.valyd.vip Content-Type: application/json { "grant_type": "authorization_code", "client_id": "YOUR_CLIENT_ID", "client_secret": "YOUR_CLIENT_SECRET", "code": "AUTH_CODE_HERE", "redirect_uri": "https://yourapp.com/callback" } ``` Send the **same** `redirect_uri` you used at `/authorize` — Valyd validates it against the code. Authorization codes are bound to the client they were issued to and are **single-use** — exchange them as soon as your callback fires. **Expected output:** HTTP 200 with a top-level JSON body: ```json { "access_token": "eyJhbGciOi...", "refresh_token": "rfrsh_abc123...", "id_token": "eyJhbGciOiJSUzI1NiIs...", "token_type": "Bearer", "expires_in": 900, "scope": "openid profile verifications" } ``` The `id_token` is an RS256-signed JWT — validate its signature against the JWKS at `https://idp.valyd.vip/api/auth/oidc/jwks.json` and check that its `nonce` claim equals the nonce you sent on `/authorize`. #### Renewing an access token Access tokens are short-lived (`expires_in` ≈ 900 seconds). Exchange the `refresh_token` at the same token endpoint — `POST https://idp.valyd.vip/api/auth/oidc/token` — from your **backend**, with your client credentials: ```json { "grant_type": "refresh_token", "refresh_token": "rfrsh_abc123...", "client_id": "YOUR_CLIENT_ID", "client_secret": "YOUR_CLIENT_SECRET" } ``` The refresh token is validated against the client it was issued to, so a token leaked from one app cannot be used by another. **Rotation is on:** every refresh returns a new `refresh_token` and revokes the one you sent, so always persist the returned value. Replaying a rotated-away token is treated as theft and revokes every refresh token for that user and app. With the SDK this is one call — `const next = await valyd.auth.refreshToken(stored)` — then persist both `next.accessToken` and `next.refreshToken`. In all raw-HTTP examples, replace `YOUR_CLIENT_ID` and `YOUR_CLIENT_SECRET` with your real values (get these from the Developer Portal → your app → Credentials: https://dev.valyd.vip). Never expose `YOUR_CLIENT_SECRET` in client-side code — the token exchange must run on your server. ### Auth-flow decision tree ```text IF the request just hit your login/start route: → generate random state + nonce (crypto.randomBytes), store them (httpOnly cookie / server session), build the URL with valyd.auth.getAuthorizationUrl({ state, nonce, scope }), and res.redirect(url). IF the request hit your /callback route: → read code, state, error from the query string IF error is set OR code is missing: → return HTTP 400 (error ?? "missing code"). STOP. IF code is present: → continue to the CSRF check below. CSRF check (do this on every callback): → compare req.query.state to the state you stored before the redirect IF they differ (or the stored value is missing): → return HTTP 400 "state mismatch". STOP. IF they match: → proceed to token exchange. Token exchange: → const tokens = await valyd.auth.exchangeCode(code) // must run server-side; codes are single-use IF the exchange returns invalid_grant: → the code expired or was already used; restart from the login route. → verify tokens.idToken's `nonce` claim equals the nonce you stored → const user = await valyd.auth.getUserInfo(tokens.accessToken) → clear the stored state/nonce, set your own app session, redirect to /dashboard. IF you are not using Node / the SDK: → POST https://idp.valyd.vip/api/auth/oidc/token with JSON { grant_type: "authorization_code", client_id, client_secret, code, redirect_uri } and read the token from the TOP-LEVEL response field access_token. ``` ### Verification - Confirm the redirect: opening your login route returns HTTP 302 with a `Location` header beginning `https://idp.valyd.vip/api/auth/oidc/authorize?client_id=...&redirect_uri=...&response_type=code&scope=openid...&state=...`. - After consenting, confirm your `/callback` route receives `code` and `state` query parameters, and that `state` equals the value you sent. - Confirm the token exchange succeeds: ```bash curl -i -X POST https://idp.valyd.vip/api/auth/oidc/token \ -H "Content-Type: application/json" \ -d '{"grant_type":"authorization_code","client_id":"YOUR_CLIENT_ID","client_secret":"YOUR_CLIENT_SECRET","code":"AUTH_CODE_HERE","redirect_uri":"https://yourapp.com/callback"}' ``` Expected: HTTP 200 and a JSON body containing top-level `access_token`, `refresh_token`, and `id_token`. ### Common errors 1. **`state mismatch` on the callback (legitimate logins rejected).** - Cause: The stored `state` was lost before the callback — cookie not set, blocked by the browser, expired, or overwritten by a second parallel login attempt. - Fix: Store `state` in an `httpOnly`, `sameSite: "lax"` cookie (or server session) on the login route and compare strictly on the callback: `req.query.state === storedState`. This comparison IS the CSRF protection — do not remove it. 2. **"missing code" / `invalid_grant` (HTTP 400 on callback or 4xx from /token).** - Cause: No `code` in the callback (user denied or an `error` was returned), the code was already exchanged (single-use), or the `redirect_uri` in the token request differs from the authorize request. - Fix: When the callback carries `error` or no `code`, return HTTP 400 and restart the flow. Exchange the code immediately, exactly once, with the same `redirect_uri`. 3. **`redirect_uri` mismatch (authorization rejected by Valyd).** - Cause: The `redirect_uri` sent on the authorization request does not exactly match the URL registered for your application. - Fix: Set `VALYD_REDIRECT_URI` (and the SDK `redirectUri`) to the exact callback URL registered in the Developer Portal (https://dev.valyd.vip), matching scheme, host, and path. 4. **`invalid_scope` / missing `openid`.** - Cause: The `scope` parameter omitted `openid`, or a requested scope is not enabled on your app in the Developer Portal. - Fix: Always include `openid` (the SDK adds it automatically) and enable every requested scope on the app before requesting it. ================================================================================ === FILE: https://docs.valyd.vip/docs/changelog.md === ================================================================================ > Source: https://docs.valyd.vip/docs/changelog # Changelog ## Platform update — recovery email, liveness reliability & auth (2026-09-16) - **Changed (Account Recovery):** `startAccountRecovery` now **always emails** the recovery link to the member's on-file address — email is the primary channel for a locked-out user, so it no longer depends on an opt-in. The `deliverEmail` flag is deprecated and ignored; the hosted `recoveryUrl` is still returned so you can additionally deliver it via your own channel (SMS / in-app). - **Improved (Liveness):** more reliable liveness capture — head-turn challenges are now verified by yaw *movement* (offset-invariant across cameras), the spoof threshold is calibrated per deployment, and the capture screen coaches distance and centering ("move closer", "center your face in the oval") before starting, cutting false "spoof detected" / "action not detected" rejections. - **Changed (Auth):** login refresh tokens now last **24 hours**. - **Improved (Auth):** when face login or registration is temporarily locked after too many attempts, the screen shows a **countdown timer** until you can try again, instead of a retry button that would only re-lock. ## v1.10.5 — Hosted flow only: standalone direct checks hidden (2026-08-27) - **Changed (SDK):** the public surface is now the hosted flow only — `valyd.auth` (Connect with Valyd / OIDC), `verify.sessions.*` (hosted verification sessions), and the Unique Human API anti-spoof (`verify.standalone.antispoof` / `antispoofIdentity`). - **Hidden (SDK):** the remaining standalone direct checks (`idVerification`, `faceMatch`, `locationMatch`, `ageVerification`, `credential`, `kycCredential`) and the `kyc.redirectUrl` helper are no longer exposed. Run these through a hosted workflow session instead; they return if/when standalone direct calls ship as a confirmed public API. - **Docs:** install commands are now unversioned — `npm install @valyd/sdk` always pulls the latest published release. ## v1.10.4 — Workflow CRUD & evvPresence removed from the SDK (2026-08-21) - **Removed (SDK):** `verify.workflows.*` CRUD — workflows are composed in the [Developer Portal](https://dev.valyd.vip); the SDK no longer exposes create/list/update/remove. Pass the resulting `workflowId` to `verify.sessions.create({ workflowId, ... })`. Returns if/when the server contract is a confirmed public API. - **Removed (SDK):** `verify.standalone.evvPresence` — the `/evv-presence` endpoint does not exist server-side (it always 404'd). Compose presence from `faceMatch` + `locationMatch` instead. ## v1.10.3 — Credential-type discovery (2026-08-20) - **Added (SDK):** `verify.credentials.types(state?, provider?)` — list credential/license types (whole catalog, per-state, or per-provider-in-a-state), routed through the Valyd API (never `vc.*` directly). ## v1.10.2 — Anti-spoof in the SDK + idempotency (2026-08-19) - **Added (SDK):** `verify.standalone.antispoof()` and `verify.standalone.antispoofIdentity()` — the `/api/v2/antispoof` endpoints are now first-class SDK methods (single `image` or 3–8 burst `frames`; `/identity` resolves the proven-live face to a stable `valyd_` uuid). - **Added (SDK):** `verify.standalone.antispoofChallenge()` — single-use, 60s gesture challenge; echo `challengeId` back on antispoof / face-uniqueness runs (required by strict projects, which also accept `challengeId` on `faceUniqueness()`). - **Added (SDK):** optional `idempotencyKey` on every billable standalone check — sent as the `Idempotency-Key` header so a network retry can never double-charge or double-run a check. - **Docs:** [Standalone checks](/verifications/unique-human) split into per-check pages, SDK call first. ## v1.10.1 — Secure OIDC transaction (2026-08-18) - **Added:** Login with Valyd is now standard OpenID Connect end to end. `valyd.auth.getAuthorizationUrl()` targets `GET /api/auth/oidc/authorize`, takes `state` + `nonce`, and adds the required `openid` scope automatically. `exchangeCode()` / `refreshToken()` use `POST /api/auth/oidc/token` and return the standard top-level token JSON (`access_token`, `refresh_token`, `id_token`, `expires_in`, `scope`). - **Added:** `createAuthorizationRequest()` + `handleCallback(url, { transaction })` keep state, nonce, and S256 PKCE together and validate the RS256 ID token against discovery/JWKS. - **Breaking (docs):** the IdP now **echoes your `state` back on the callback** — the standard OAuth `state` comparison is the correct, required CSRF check. The login-session "marker" pattern is deprecated; `createLoginSession()` / `verifyLoginSession()` are now deprecated no-ops kept only for backward compatibility. - **Docs:** Login with Valyd and the Verification API are documented as separate integration paths. ## Docs — Anti-spoof, face uniqueness & developer accounts - **Added (API docs):** `POST /api/v2/antispoof` (single image or live burst → `human_score`), `POST /api/v2/antispoof/identity` (liveness + stable `valyd_` uuid for duplicate detection), `POST /api/v2/face-uniqueness` (+ unlink), and `POST /api/v2/location` are now in the [Standalone checks reference](/verifications/unique-human). - **Added (page):** [Developer accounts & sign-in](/docs/developer-accounts) — passwordless sign-in (magic link or face), connecting a Valyd ID to an email-only account, and one identity owning several console accounts with account switching. - **Docs:** every relying party now receives the user's **real legal name** (not the pseudonym). ## v1.8.0 — Member resolve + reactivate; login-only consent - **Added:** `resolveMember({ valydId })` / `{ email }` — look up ONE person's membership in your org at ANY role (returns the `Member` with `role` + `status`, or `null`). Lets you tell a workforce member apart from a developer/admin, or from someone not in your org. (`POST /api/sdk/members/resolve`) - **Added:** `reactivateMember(memberId)` — undo a `removeMember`; restores `active` (or `invited` if never activated). (`PATCH /api/sdk/members/{memberId}/reactivate`) - **Docs:** the member table now documents `removeMember` (deactivate) and `reactivateMember` — the older "no deactivate over the API" note was stale. - **Breaking (behavior):** the **at-login attribute release** on the consent screen (`attr_code`, remembered consent) is **currently disabled** — the consent screen is **login-only**. Request raw data with the **after-login** `requestAttributes` flow (user approves in their Valyd app). See `/docs/request-data`. ## v1.5.1 — Unified SDK + Workforce Members API - **Added:** Workforce Members API on `ValydClient` — `addMembers()` (single or bulk ≤ 500, `notify` flag), `getMembers()` (roster with `status` + `valyd_id`), `getBilling()` (seats, price, trial, balance, invoices). - **Added:** One unified package `@valyd/sdk` — `valyd.auth` (Login with Valyd) + `valyd.verify` (verification) + workforce members; one credential, one host. - **Docs:** The Organizations page lists every member operation. --- ## v0.2.0 — Legacy login-session helpers (superseded by v1.10.1) - **Added:** `createLoginSession()` and `verifyLoginSession()` helpers. - **Docs:** Clarified that the callback `state` is Valyd's session id, not your authorize state. - **Breaking (docs):** Removed the state-equality CSRF pattern — use `verifyLoginSession` instead. --- ## v0.1.0 — Initial release - **Added:** `ValydClient` with `getAuthorizationUrl`, `parseCallback`, `exchangeCode`, `refreshToken`. - **Added:** Resource helpers: `getUserInfo`, `getLicenses`, `getCprLicense`, `getDoctorLicense`, `getVerifications`. ================================================================================ === FILE: https://docs.valyd.vip/docs/create-account.md === ================================================================================ > Source: https://docs.valyd.vip/docs/create-account # Create your account Everything starts at [dev.valyd.vip](https://dev.valyd.vip). You **sign in first**, then a short guided setup creates your first app and ends with **ready-to-run credentials on screen**. About two minutes. ## 1. Sign in One sign-in creates your developer account — no password to set up: ![The sign-in screen: Connect with Valyd or a one-time email link](/images/screenshots/portal-onboarding-signin.png) - **Connect with Valyd** — face sign-in with the Valyd app; it also lets you log into your own apps and run verifications with your identity. - **Login with email** — a one-time magic link, no password. You can connect a Valyd ID later from the dashboard. > 💰 **New accounts start with a $100 welcome credit** — run real logins and verifications while > [testing](/docs/testing) without adding a card. ## 2. Create your first project (or skip) Right after sign-in the setup asks if you want to create your first project. A project gives you the OAuth credentials your app logs users in with. Choose **Skip for now** to go straight to the dashboard and set one up later: ![Create your first project, or skip to the dashboard](/images/screenshots/portal-onboarding-step3.png) ## 3. Name your app Give the project a name and add your **domain** and **redirect URL** — these identify your app to Valyd. You can change all three later in the project's settings: ![Naming your first app: project name, domain, and redirect URL](/images/screenshots/portal-onboarding-step2.png) ## 4. Add verifications Turn on the identity checks you want to run — government ID, liveness, face match, face uniqueness, age, professional license, or location. Pick any, or none. Every answer is delivered as **zero-knowledge-style proofs** — the fact (`id_verified: true`, `is_18_plus`), never the underlying data: ![Pick the verification checks to run in your app](/images/screenshots/portal-onboarding-verifications.png) ## 5. You're in — with your credentials Setup finishes with your **Client ID and secret, Verification API key, and webhook secret** on screen. Copy them now (the secrets are shown only once), then head to the dashboard: ![Your project is ready: client ID and secret, API key, and webhook secret](/images/screenshots/portal-ready-code.png) ## Where things live from here - **Finish app setup** — register your production redirect URI and scopes: [Set up Sign in with Valyd](/docs/quick-start) (this page's steps 2–3 are its step 1 done). - **Apps** ([Sign in with Valyd](/docs)) — `client_id`/`client_secret`, scopes, redirect URIs: [Dev portal setup](/docs/create-project), then a [quickstart](/docs/quickstarts) for your stack. - **Verification** — account-connected verification lives **inside your app**; standalone projects live **on the dashboard**: [Verification setup](/verifications/setup). ================================================================================ === FILE: https://docs.valyd.vip/docs/create-project.md === ================================================================================ > Source: https://docs.valyd.vip/docs/create-project # Apps & API keys > **Terminology:** the Developer Portal UI labels this object a **"project"**; these docs call it > your **app**. It is the same thing — one OAuth client plus its **Verification** tab (API key + > workflows). Where an > API path literally says `project` (e.g. `/verify/projects/{id}`), keep the literal path. ## Prerequisites - A Valyd account. Sign up at https://dev.valyd.vip if you do not have one. No KYC verification is required for portal access. - The list of domains your application sends requests from (used for Allowed Web Origins). - Your redirect/callback URL, without a trailing slash. - A decision on which data scopes your application needs (`profile`, `verifications`, `zkp` — detailed below). ## Steps This is a portal-driven setup. All steps below are performed by a human in the web UI at https://dev.valyd.vip — there is no API to automate project creation. ### Step 1 — Visit the Developer Portal and log in Open the Developer Portal in a browser and log in with your Valyd account. ```text https://dev.valyd.vip ``` **Expected output:** You are signed in and see the Developer Portal dashboard with an option to create a new project. ### Step 2 — Create the project and fill in project details Click "Create Project" and fill in the following fields. Required/Optional status is as shown in the portal. **Project Name** (Required) - Your application's name that users will see on the consent screen when logging in. - Example: `My Awesome App` **Description** (Optional) - A brief description of your application. Helps users understand what they're authorizing. **Allowed Web Origins** (Required) - The domains from which your application will send requests. This is a security feature to prevent unauthorized domains from using your credentials. - Example: `https://myapp.com, https://staging.myapp.com` **Redirect URL** (Required) - The URL where users will be redirected after authentication. This is where you'll receive the authorization code. - Example: `https://myapp.com/callback` - IMPORTANT: The redirect URL must NOT end with a trailing slash (`/`). ```text IF your callback is https://myapp.com/callback → register exactly this (no trailing slash) IF your callback is https://myapp.com/callback/ → remove the trailing slash before registering ``` **Allowed Scopes** (Required) Select the data permissions your application needs. Users will see these scopes on the consent screen. Available scopes and their fields: - `profile` — **Required.** Core biometric identity. Face vector is required for most Valyd features. KYC fields are optional. - Core Biometrics (always enabled — cannot be turned off): - `Face Vector` — Biometric face data, foundational to Valyd - `Face Match` — Facial recognition matching - KYC Data (optional — select verification method): - `Name` — User's self-reported name - `Age` — User's age estimate - `Portrait` — the photo extracted from the user's submitted ID document during KYC (consent-gated; Valyd never exposes a stored face image — accounts hold only irreversible face vectors) - `verifications` — Document-based identity verification. - `ID Verification` — Government ID document verification - `Licenses` — Professional or driver's licenses - `zkp` — Zero-Knowledge Proof verification: prove facts without revealing data. - `Age Verification` — Prove age without revealing birthdate - `Country Verification` — Prove residency without revealing address **Expected output:** The project form is submitted successfully. ![The app's Settings tab: redirect URIs, web origins, and scopes](/images/screenshots/portal-app-settings.png) ### Step 3 — Save only the credentials your path needs After creating your project, you'll immediately see a modal with your credentials. **Client ID** — your unique application identifier. You can view this anytime in your project settings. ```text Example Client ID format: 9357c59bc1794b4c9efe8823e5878147 ``` **Client Secret** — shown only once. ```text Example Client Secret format: sk_live_a1b2c3d4e5f6g7h8i9j0... ``` IMPORTANT: The Client Secret is shown only once. Copy and store it securely immediately. If you lose it, you'll need to regenerate it. For Verification API integrations, also copy the App API key shown by the portal. Store it as `VALYD_API_KEY` on your backend and send it as `X-API-Key`. Do not send `client_secret` to Verification API endpoints, and never expose either secret in frontend code. Store only the credentials your chosen path needs, for example: ```bash # .env (server-side only — never commit or expose in frontend code) VALYD_CLIENT_ID=9357c59bc1794b4c9efe8823e5878147 VALYD_CLIENT_SECRET=sk_live_a1b2c3d4e5f6g7h8i9j0... # Verification API only (does not require the two OIDC values above) VALYD_API_KEY=vrf_... ``` **Expected output:** Connect with Valyd integrations have `client_id` + `client_secret`. Unique Human API calls need only `VALYD_API_KEY`. Reusable Verification uses both, plus a workflow ID and webhook secret. ## Choose your next step For Reusable Verification (Connect with Valyd, then verify what's missing): 1. Store your `client_secret` securely in your backend environment variables. 2. Follow the [Connect with Valyd quickstart](/docs/quick-start); the SDK builds the OIDC URL. 3. Handle the callback and exchange the one-time code for tokens using your `client_secret` (backend only). 4. Use the `access_token` to fetch scoped account data, then [run a verification](/verifications/quickstart) with a configured workflow for anything missing. For the Unique Human API (liveness and face uniqueness, API key only): 1. Store `VALYD_API_KEY` on your backend. 2. Do not build an OIDC login route — these calls involve no user account. 3. Follow the [Unique Human API](/verifications/unique-human) guide. ## Verification There is no CLI/API check for project creation; verify manually in the portal: ```text IF you can see your project listed in the Developer Portal at https://dev.valyd.vip → project was created IF the credentials modal showed a Client ID and Client Secret → credentials were issued IF you saved the Client Secret before closing the modal → you can proceed IF you closed the modal without copying the Client Secret → regenerate it from project settings (it is shown only once per generation) ``` ## Common errors 1. **Redirect URL rejected / authorization fails with a redirect mismatch** - **Cause:** The registered Redirect URL has a trailing slash, or does not exactly match the `redirect_url` your app sends. - **Fix:** Register the redirect URL with NO trailing slash and ensure the value used in the authorization request matches it character-for-character. 2. **Lost the Client Secret** - **Cause:** The Client Secret is shown only once when the project is created and was not copied before the modal was closed. - **Fix:** Regenerate the Client Secret from your project settings in the Developer Portal, then update your backend environment variable. 3. **Requests blocked from your domain** - **Cause:** The requesting domain is not listed in Allowed Web Origins. - **Fix:** Add the exact origin (scheme + host, e.g. `https://myapp.com`) to Allowed Web Origins in the project settings. ================================================================================ === FILE: https://docs.valyd.vip/docs/customize.md === ================================================================================ > Source: https://docs.valyd.vip/docs/customize # Customization What you can shape today — nothing here requires a support ticket. ## The verification session is yours to compose Valyd's verification page runs exactly the checks **your workflow** defines. You decide which steps the user goes through, in what order, and how retries behave — compose a workflow in the [Developer Portal](https://dev.valyd.vip) or [via the API](/verifications/workflows), and the page auto-adapts its steps to it. License-only, full KYC + license, liveness-gated flows: same page, your composition. ## Your name on the consent screen The consent screen shows **your app's registered name and icon** — set them once when you create the app in the [Developer Portal](https://dev.valyd.vip). The name is not something a request can pass in, so it can't be spoofed: an app registered as *Acme* always appears as *Acme*. ![The consent screen showing the requesting app's registered name](/images/screenshots/idp-consent-screen.png) To change how your app appears, edit its name (and upload an icon) in the portal — not on the authorize URL. ## The Sign-in button The drop-in button accepts: | Attribute | Values | Default | | --- | --- | --- | | `data-theme` | `dark` · `light` | `dark` | | `data-text` | any label | `Sign in with Valyd` | Or skip it entirely and render your own button — it only needs to link to the [authorize URL](/docs/flows/authorization-code). ## Coming soon: full verification-page branding Full layout, design, and branding customization of Valyd's verification page is on the roadmap — the goal is a capture experience that looks and feels like *your* product end to end: - **Custom colors, logo, and typography** on the verification pages - **Your own domain** for the verification page - **Branded emails** sent under your name We're shaping this with early integrators — if verification-page branding matters for your rollout, tell us what you need: [javi@valyd.id](mailto:javi@valyd.id). ================================================================================ === FILE: https://docs.valyd.vip/docs/data-and-trust.md === ================================================================================ > Source: https://docs.valyd.vip/docs/data-and-trust # Data & trust What a Valyd verification actually asserts, what happens to the data a check touches, and what your app can (and cannot) receive. ## In Security & data - **[Trust Center](/docs/security-trust)** — compliance status, DPA, pen-test summaries, and how to request reports. - **[Data retention](/docs/data-retention)** — how long each class of data is kept. - **[Data residency](/docs/data-residency)** — where data is processed and stored, and the subprocessor list. - **[API key lifecycle](/docs/api-key-lifecycle)** — creating, rotating, and revoking API keys. - **[Security disclosure](/docs/security-disclosure)** — how to report a vulnerability. - **[Audit logging](/docs/audit-logging)** — what activity Valyd records and how to read it. ## What "verified" means A verification is a **point-in-time assertion**: at the moment the check ran, the document was authentic, the selfie was live, the face matched, the license was active in its registry. Proofs saved to an account are durable outcomes of those checks — the account's `identity` object carries a `verified_at` timestamp so you can judge freshness, and a license's registry record carries its own `status` and `expires_at`. Re-run a check when your policy needs a fresher answer. ## Biometrics: vectors, never images > **Biometrics are irreversible vectors, never images.** Valyd does not store or return face > images. Enrollment converts a selfie into a one-way biometric vector (template); every later > face match compares vectors. The photos you submit to a check are processed transiently for > that check and are not retrievable from a Valyd account. Two clarifications that scope this claim precisely: - **The template itself is never exposed through any API** — not to the user, not to integrators. It exists only to be compared against inside Valyd. - **The KYC `portrait` field is not a stored account photo.** The `portrait` returned by ID/KYC checks (e.g. `kyc-credential`, `id-verification`) is extracted from the ID document **you** submitted in that request and is returned in that response only. ## What data goes where | Data | What happens | | --- | --- | | ID images & selfies | Processed transiently for the check that received them; not retrievable from a Valyd account. | | Face biometrics | Stored only as an irreversible one-way vector; matching compares vectors (see above). | | Legal name | Returned to your app at login via [`userinfo`](/docs/endpoints) under the `profile` scope the user approved. | | DOB, document number, gender, nationality | **Vault-only.** Released solely through the explicit [consent flow](/docs/request-data), sealed on the user's device to your X25519 key — end-to-end encrypted; the server is blind to them. | | Age | Shared as derived **age bands** (`is_18_plus`, …) without exposing the raw DOB; the raw `dob` needs the consent flow. | | Verification status & proofs (`id_verified`, license badges, age bands) | Read via the [Account API](/docs/endpoints#resource-api--user-data), gated by the [scopes](/docs/scopes) the user approved at login. | | [Standalone](/verifications/unique-human) check results | Returned to your system only; nothing is added to a Valyd account. Raw KYC fields in a hosted decision are released only after the required ID, liveness, and face-match gates pass — until then they remain encrypted. | | Account-connected results | The account APIs return **proofs only**, never raw account KYC ([details](/verifications)). | Two rules cover everything: - **The Account API never runs a check** — it reads what previous checks already proved. - **A check never touches an account** — unless you attach the user's token. ## Security properties **Encryption, per Valyd's data policies:** - **Identity fields at rest** — the personal data a Valyd account holds (legal name, KYC fields) is stored encrypted at rest on Valyd's systems. - **Raw KYC data** — document fields extracted during a hosted KYC stay encrypted and are released to your integration only after the required ID, liveness, and face-match checks pass. - **Consent-released attributes** — data released through the consent flow is sealed end-to-end to a key only you hold; Valyd cannot read it in transit. - **Biometrics** — accounts hold irreversible face vectors, never images; templates are never exposed through any API. - **Secrets** — API keys and webhook signing secrets are stored encrypted; transport is TLS everywhere. This is why account-connected verification is the recommended mode: the sensitive data stays inside these controls, and your application reads verified status. - **TLS everywhere** — every documented endpoint is HTTPS; register HTTPS redirect URIs in production. - **Sensitive KYC fields stay encrypted** until a one-off decision's required ID, liveness, and face-match gates pass. - **Consent data is end-to-end encrypted** — vault-only attributes are sealed on the user's device to your public key (libsodium sealed box); with self-custody, Valyd cannot read the released values. - **Webhooks are signed** (HMAC-SHA256 over the raw body with a per-app secret) and sent only to the active URL configured for your app — [verify them](/verifications/webhooks). - **The user stays in control**: consent is per-field and per-app, remembered but revocable from *Connected sites* in their Valyd account. For what each check asserts individually, see [Verification types](/verifications/types); for what your app should do with results, see [Decisions & statuses](/verifications/statuses). ================================================================================ === FILE: https://docs.valyd.vip/docs/data-residency.md === ================================================================================ > Source: https://docs.valyd.vip/docs/data-residency # Data residency Where Valyd processes and stores the data an integration touches, and which third parties (subprocessors) are involved. These docs do not publish Valyd's processing regions or a subprocessor list. Contact **support@valyd.id** for the current data-residency options and subprocessor list. ## Processing & storage location | Question | Answer | | --- | --- | | Primary processing / storage region | Available on request — contact **support@valyd.id** for current data-residency options | | EU / regional data-residency option | Available on request — contact **support@valyd.id** to discuss regional data-residency options | | Cross-border transfer mechanism | Available on request — contact **support@valyd.id** | ## Infrastructure & hosting | Question | Answer | | --- | --- | | Cloud / hosting provider | Available on request — contact **support@valyd.id** | | Biometric / verification engine hosting | Available on request — contact **support@valyd.id** | ## Subprocessors Valyd relies on third parties to deliver parts of the verification flow (for example, document and registry checks). A current, itemized subprocessor list is not published in these docs. | Question | Answer | | --- | --- | | Published subprocessor list | Available on request — contact **support@valyd.id** for the current subprocessor list | | Subprocessors for ID / document verification | Available on request — contact **support@valyd.id** | | Subprocessors for registry / license lookups | Available on request — contact **support@valyd.id** | | Change-notification process | Available on request — contact **support@valyd.id** | ## See also - [Trust Center](/docs/security-trust) - [Data retention](/docs/data-retention) - [Data & trust](/docs/data-and-trust) ================================================================================ === FILE: https://docs.valyd.vip/docs/data-retention.md === ================================================================================ > Source: https://docs.valyd.vip/docs/data-retention # Data retention What each class of data a Valyd integration touches is retained for. Two rows are fixed by Valyd's documented [Data & trust](/docs/data-and-trust) policy — submitted images are processing-only, and the face biometric exists only as an irreversible vector held until the account is unlinked or deleted. Retention windows for the remaining data classes are not published here — contact **support@valyd.id** for the current retention schedule. ## Retention matrix | Data class | What it is | Retained | | --- | --- | --- | | ID document image | Front/back photo submitted to an ID or KYC check | **Transient / processing-only** — processed for the check that received it and not retrievable from a Valyd account afterward ([Data & trust](/docs/data-and-trust#what-data-goes-where)) | | Selfie image | Liveness / face-match capture | **Transient / processing-only** — same handling as ID images ([Data & trust](/docs/data-and-trust#what-data-goes-where)) | | Biometric vector (face template) | Irreversible one-way vector derived from an enrolled selfie | **Until the account is unlinked or deleted** — stored only as a vector, never an image, and never exposed through any API ([Data & trust](/docs/data-and-trust#biometrics-vectors-never-images)) | | Verification decision & proofs | Session decision (`APPROVED` / `DECLINED` / `IN_REVIEW`) and durable proofs (`id_verified`, license badges, age bands) | Available on request — contact **support@valyd.id** for the current retention schedule | | Webhook delivery log | Record of signed events POSTed to your endpoint | Available on request — contact **support@valyd.id** for the current retention schedule | | Application / request logs | Operational logs keyed by `X-Request-Id` | Available on request — contact **support@valyd.id** for the current retention schedule | | Audit & billing records | Account, usage, and billing history | Retained per applicable legal, tax, and accounting requirements — contact **support@valyd.id** for the current schedule | ## Deletion - **Biometric vectors** are held until the account is unlinked from your app or the account is deleted. Account deletion behavior surfaces in the error catalog via `user_deleted` (410) — a device linked to a deleted account must clear local data and re-register. - **Submitted images** are never persisted to an account, so there is nothing to delete after a check completes — they exist only for the duration of processing. - **End-user erasure requests / right-to-be-forgotten** — deleting or unlinking a Valyd account clears the stored biometric vector (a device linked to a deleted account re-registers, surfaced as `user_deleted` (410)). For a formal erasure request or the completion timeline, contact **support@valyd.id**. ## See also - [Trust Center](/docs/security-trust) - [Data residency](/docs/data-residency) - [Data & trust](/docs/data-and-trust) ================================================================================ === FILE: https://docs.valyd.vip/docs/deprecations.md === ================================================================================ > Source: https://docs.valyd.vip/docs/deprecations # Deprecations & removals One place to check whether something you integrated against has changed. Anything not listed here is current. | What | Status | Since | Use instead | | --- | --- | --- | --- | | Age check `bands.*.verified` response field | **Deprecated alias** | 2026-08-19 | Read `satisfied` — same value, honest name ([why](/verifications/unique-human)) | | `POST /api/auth/tpsso/token`, `/refresh`, `/tpsso/authorize` (legacy TPSSO OAuth) | **Removed — 410 Gone** | 2026-08-18 | [`/api/auth/oidc/*`](/docs/endpoints) — see the [OIDC guide](/docs/oidc) | **Policy:** removed endpoints return an explicit error (`410` with a pointer, never a silent 404). Deprecated SDK methods keep compiling but stop being documented. Breaking changes land in the [changelog](/docs/changelog) first. ================================================================================ === FILE: https://docs.valyd.vip/docs/developer-accounts.md === ================================================================================ > Source: https://docs.valyd.vip/docs/developer-accounts # Developer accounts & sign-in ## How sign-in works (passwordless) The Developer Portal at `https://dev.valyd.vip` has **two** ways in — there is no password: 1. **Magic link (email).** Enter your email; Valyd sends a one-time sign-in link. Opening it signs you in. This is the usual path for owners, admins and developers. 2. **Face — "Connect with Valyd".** If your account has a verified Valyd identity linked, you can sign in with your face (the same OAuth/OIDC "Connect with Valyd" flow your own app uses). Workforce **members** (the people your organization onboards to *use* your apps, not build them) do not use either of the above — they activate and sign in **by face only**, from an invite. See [Organizations & teams](/docs/organizations). ### Which one should I use? - Just getting started, or no Valyd identity yet → **magic link**. - You want faster, phishing-resistant sign-in → **link your face** (below), then use Connect with Valyd. ## Connect your Valyd ID An account created by magic link starts as **email-only**. You can attach a Valyd face identity to it at any time so you can sign in by face and prove who you are: 1. Sign in (magic link) and open **Profile**. 2. Under your identity, choose **Connect your Valyd ID**. 3. Complete Connect with Valyd (face). The returned identity is linked to your current, already signed-in account. Notes: - Linking **requires an authenticated session** by design — you connect a face *to the account you are already signed in to*. You cannot link a face to an account you are not signed in to. - This is different from **Connected apps** on an *end-user's* Valyd account (the third-party sites a user authorized via Connect with Valyd). Developer "Connect your Valyd ID" links *your own* face to *your own* console account. ## One person, multiple accounts A single Valyd identity (your face) can **own several Developer Portal accounts** — most commonly one account per company you work with. Valyd keeps them separate but lets you move between them without signing in again. - **When it appears:** if your email or face maps to more than one account, sign-in shows an **account picker** — choose which account to enter. - **Switching later:** use **Switch account** (`/switch`) in the portal to hop between the accounts your identity already owns. No re-scan, no new magic link. - **What stays separate per account:** projects/apps (`client_id`/`client_secret`), Verify workflows and API keys, billing, team and members. Switching accounts changes all of these. > At the **identity** layer, a person is still **one face = one Valyd identity**. "Multiple accounts" > is a **Developer Portal** concept: the one identity is the owner of multiple console tenants. (To > use that one identity across multiple **devices**, pair each device — see > [device pairing](/docs/create-project). That extends the same identity to another device; it does > not create another account.) ## Projects live under an organization Everything you build lives inside an **organization** — there is no separate "personal vs organization app" split. On first sign-in Valyd creates your **personal organization** automatically: an org of one that can't be deleted. Every project (a "Connect with Valyd" app with its own verification) belongs to whichever organization is **active in the sidebar switcher**: - **Your personal organization** — your solo workspace. Its projects and its bill are yours. - **Team organizations** — shared tenants you create or are invited to. Projects, members and the per-seat bill belong to the org, and ownership stays with it even when a person leaves. Access is role-gated: **owner / admin** manage everything, **developers** build with the keys, and **members** are read-only and can't see projects. Switch between the organizations you own or joined from the **org dropdown** at the top of the sidebar. Billing is a flat **$0.99 per active member**, drawn from the organization's prepaid balance. See [Organizations & teams](/docs/organizations) for roles, the face-verified workforce, and the Members API. ## Managing your developer account - **Rotate a client secret:** open the app in the portal and use **Rotate secret** on its credentials. The old secret stops working immediately, so deploy the new one first. - **Delete your developer account:** Profile → danger zone. Deleting cascades to the apps you own; other console accounts your identity also owns are kept and simply unlinked. ## Common questions - **"What's my password?"** — There isn't one. Use the magic link, or face if you've linked it. - **"I got an account picker I didn't expect."** — Your email/identity owns more than one account. Pick the right company; use **Switch account** later to change. - **"Can I automate portal login for CI?"** — No. Sign-in, linking and switching are human portal steps. For server-to-server automation use an app's `client_id`/`client_secret` (Connect with Valyd) or an app's **API key** (Verification APIs) — not a portal login. ================================================================================ === FILE: https://docs.valyd.vip/docs/disaster-recovery.md === ================================================================================ > Source: https://docs.valyd.vip/docs/disaster-recovery # Disaster recovery Valyd's continuity and recovery posture — backups, recovery objectives, failover, and restore testing. Valyd's continuity and recovery details are shared with enterprise customers on request — contact your Valyd account team for backup, RTO/RPO, and DR-plan specifics. What *is* documented publicly lives on adjacent pages and is cross-linked below; this page asserts no recovery guarantee that is not already stated elsewhere. ## Backups | Question | Answer | | --- | --- | | Backup cadence & method | Available on request — contact your Valyd account team | | Backup retention & location | Available on request — contact your Valyd account team | | Backup encryption | Available on request — contact your Valyd account team | Where the underlying data is processed and stored is documented on [Data residency](/docs/data-residency), and how long each class of data is kept is documented on [Data retention](/docs/data-retention). ## Recovery objectives | Question | Answer | | --- | --- | | RPO (Recovery Point Objective) | Available on enterprise plans — contact your Valyd account team | | RTO (Recovery Time Objective) | Available on enterprise plans — contact your Valyd account team | [Operations & SLA](/docs/operations-sla) summarizes the documented degraded-mode / failover behavior; this page is the fuller home for backup, RTO/RPO, and DR-plan questions. ## Failover & redundancy | Question | Answer | | --- | --- | | Multi-region / multi-AZ | Available on request — contact your Valyd account team | | Failover trigger | Available on request — contact your Valyd account team | Note the observable degraded-dependency behavior that *is* documented: when an upstream dependency is unreachable, Valyd surfaces a machine-readable error to retry against rather than failing silently — see [Operations & SLA — degraded-dependency behavior](/docs/operations-sla#degraded-dependency-behavior). ## Restore testing & DR plan | Question | Answer | | --- | --- | | Restore testing | Available on request — contact your Valyd account team | | DR plan document | A DR-plan summary is available under NDA — contact your Valyd account team or **security@valyd.id** | ## See also - [Operations & SLA](/docs/operations-sla) — uptime, status, incident comms, degraded-mode signals - [Data residency](/docs/data-residency) — where data is processed and stored - [Data retention](/docs/data-retention) — what is kept and for how long - [Trust Center](/docs/security-trust) ================================================================================ === FILE: https://docs.valyd.vip/docs/endpoints.md === ================================================================================ > Source: https://docs.valyd.vip/docs/endpoints # Account API This page is only for Connect with Valyd and reading a connected user's account. These endpoints do not start any check. To run a check, use a configured workflow — see [Reusable Verification](/verifications) and the [Verification API reference](/verifications/api-reference). ## General notes - Every API response carries an **`X-Request-Id`** header. Log it, and quote it when contacting support — never send API keys, tokens, or identity data. - All API requests must be made over HTTPS. - Endpoints that require authentication expect a Bearer token in the `Authorization` header: `Authorization: Bearer YOUR_ACCESS_TOKEN`. - If you are using the SDK, prefer the typed helpers (`getAuthorizationUrl()`, `exchangeCode()`, `handleCallback()`, `refreshToken()`) — they call these endpoints for you. - **One API namespace:** authorize, token, JWKS, UserInfo, licenses, and verifications are under `https://idp.valyd.vip/api/auth/oidc`. Discovery is at `/.well-known/openid-configuration` (the `/api/.well-known/...` alias also works). ## SDK methods (@valyd/sdk) ### `valyd.auth.createAuthorizationRequest({ scope, redirectUri? })` Recommended Connect entry point. Generates strong `state`, `nonce`, and an S256 PKCE verifier/challenge together. Store the returned transaction server-side and redirect to `transaction.url`. ### `valyd.auth.getAuthorizationUrl({ state, nonce, codeChallenge, scope, redirectUri? })` Low-level URL builder. `state` is required. Prefer `createAuthorizationRequest()` so PKCE and nonce cannot be forgotten. ### `valyd.auth.exchangeCode(code)` Exchanges the authorization code at `POST /api/auth/oidc/token`. The SDK verifies the ID token against discovery/JWKS before returning `{ accessToken, refreshToken, idToken, claims, expiresIn, scope, tokenType }`. ### `valyd.auth.handleCallback(url, { transaction })` One callback call: compares state, sends the PKCE verifier, exchanges the code, verifies RS256/JWKS plus issuer/audience/expiry/nonce, and fetches UserInfo. ### `valyd.auth.refreshToken(refreshToken)` Refreshes at `POST /api/auth/oidc/token` with `grant_type: "refresh_token"`. Rotation is on — persist the returned `refreshToken` every time. ## OIDC endpoints (current — use these) ### GET /api/.well-known/openid-configuration — Discovery - **Method:** GET - **Full URL:** `https://idp.valyd.vip/api/.well-known/openid-configuration` - **Auth:** none Standard OIDC discovery document: issuer, `authorization_endpoint`, `token_endpoint`, `userinfo_endpoint`, `jwks_uri`, supported scopes/grants/algorithms. Point any OIDC-capable framework at this URL to auto-configure. See the [OIDC integration guide](/docs/oidc) for the full response. ### GET /api/auth/oidc/authorize — Authorization - **Method:** GET (browser redirect) - **Full URL:** `https://idp.valyd.vip/api/auth/oidc/authorize` - **Auth:** none (user authenticates interactively) Query parameters: `client_id`, `redirect_uri`, `response_type=code`, `scope` (space-separated, **must include `openid`**), `state` (required — echoed back unchanged on the callback), `nonce` (recommended — bound into the `id_token`). On consent, Valyd redirects to your `redirect_uri` with `?code=...&state=`. ### POST /api/auth/oidc/token — Token (exchange + refresh) - **Method:** POST - **Full URL:** `https://idp.valyd.vip/api/auth/oidc/token` - **Auth:** client credentials in the body (`client_secret_post`) or HTTP Basic (`client_secret_basic`) - **Required headers:** `Content-Type: application/json` Two grants: | `grant_type` | Body fields | |---|---| | `authorization_code` | `client_id`, `client_secret`, `code`, `redirect_uri` (exact match), `code_verifier` when PKCE was used | | `refresh_token` | `client_id`, `client_secret`, `refresh_token` | Returns a **standard top-level token JSON** (no `data` wrapper): ```json { "access_token": "eyJhbGciOi...", "refresh_token": "rfrsh_abc123...", "id_token": "eyJhbGciOiJSUzI1NiIs...", "token_type": "Bearer", "expires_in": 900, "scope": "openid profile verifications" } ``` Notes: - Authorization codes are single-use and client-bound — exchange immediately. - The `id_token` is an RS256 JWT; validate it against the JWKS below and check its `nonce` claim. - Refresh **rotation is on for every refresh**: the `refresh_token` you sent is revoked and a new one is returned — always persist the new value. Replaying a rotated-away token revokes every refresh token for that user and client. - The returned `access_token` works on all resource endpoints below (`/userinfo`, `/licenses`, `/verifications`). - What each of the three tokens is for, with decoded examples: [Tokens](/docs/tokens). ### GET /api/auth/oidc/logout — RP-initiated logout - **Method:** GET (browser redirect) - **Full URL:** `https://idp.valyd.vip/api/auth/oidc/logout` - **Auth:** none (identity proven by `id_token_hint`) Query parameters: `id_token_hint` (the id_token you received at login — an expired one is accepted, its signature still proves the user/client), `post_logout_redirect_uri` (must **exactly match** one of your registered redirect URIs — register your post-logout URL as an additional redirect URI), `state` (optional, echoed back). Revokes the user's refresh tokens and access tokens **for your client**, then redirects. Advertised in discovery as `end_session_endpoint`. ### GET /api/auth/oidc/jwks.json — Signing keys - **Method:** GET - **Full URL:** `https://idp.valyd.vip/api/auth/oidc/jwks.json` - **Auth:** none Public RSA keys (JWK set) for validating `id_token` signatures (RS256). ### GET /api/auth/oidc/userinfo — Standard OIDC userinfo - **Method:** GET - **Full URL:** `https://idp.valyd.vip/api/auth/oidc/userinfo` - **Auth:** `Authorization: Bearer YOUR_ACCESS_TOKEN` Returns top-level standard OIDC claims such as `sub`, `valyd_id`, `preferred_username`, `email`, `name`, and `id_verified` according to the granted scopes. --- ## Resource API — user data > 🔑 **Auth:** Bearer access token (from login) · 👤 **User login:** required · 📄 **Scope-gated** — these READ the account; they never run a new check These canonical `/api/auth/oidc/*` endpoints accept access tokens minted by the [OIDC token endpoint](#post-apiauthoidctoken--token-exchange--refresh). The **Account API never runs a check** — it reads what previous checks already proved. To run a new check, see [Verification](/verifications). **Raw identity data** (DOB, document number, address …) is never returned by these endpoints — it requires the user's explicit approval via the [consent flow](/docs/request-data), and comes back end-to-end encrypted. ## GET /userinfo — Get User Profile - **Method:** GET - **Full URL:** `https://idp.valyd.vip/api/auth/oidc/userinfo` - **Base URL:** `https://idp.valyd.vip/api/auth/oidc` - **Path:** `/userinfo` - **Auth / required scope:** Bearer access token required; required scope: `profile` - **Required headers:** - `Accept: application/json` - `Authorization: Bearer YOUR_ACCESS_TOKEN` Retrieve the authenticated user's profile information including name, email, and verification status. `YOUR_ACCESS_TOKEN`: the `access_token` returned by [`POST /api/auth/oidc/token`](#post-apiauthoidctoken--token-exchange--refresh) (code exchange or refresh grant). ### Example ```bash curl -X GET "https://idp.valyd.vip/api/auth/oidc/userinfo" \ -H "Accept: application/json" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ### Response ```json { "sub": "valyd_225c7f2ac450496f97bbbc57354a5898", "valyd_id": "valyd_225c7f2ac450496f97bbbc57354a5898", "preferred_username": "johndoe", "email": "user@example.com", "name": "John Doe", "id_verified": true } ``` ## GET /licenses — Get Professional Licenses - **Method:** GET - **Full URL:** `https://idp.valyd.vip/api/auth/oidc/licenses` - **Base URL:** `https://idp.valyd.vip/api/auth/oidc` - **Path:** `/licenses` - **Auth / required scope:** Bearer access token required (no specific scope declared on this endpoint in the source) - **Required headers:** - `Accept: application/json` - `Authorization: Bearer YOUR_ACCESS_TOKEN` Returns a snapshot of the user's professional licenses as verified by Valyd. Includes nursing licenses, CDL endorsements, CPR/BLS certifications, Food Handler permits, and more. `YOUR_ACCESS_TOKEN`: the `access_token` returned by [`POST /api/auth/oidc/token`](#post-apiauthoidctoken--token-exchange--refresh) (code exchange or refresh grant). ### Example ```bash curl -X GET "https://idp.valyd.vip/api/auth/oidc/licenses" \ -H "Accept: application/json" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ### Response ```json { "success": true, "data": { "licenses": [ { "type": "nurse_licenses", "number": "RN-123456", "status": "Active", "expires_on": "2027-06-30", "issuer": "CA Board of Nursing" }, { "type": "cpr_certification", "number": "CPR-998877", "status": "Active", "expires_on": "2026-05-15", "issuer": "American Heart Association" } ] } } ``` ## GET /verifications — Get Identity Verifications - **Method:** GET - **Full URL:** `https://idp.valyd.vip/api/auth/oidc/verifications` - **Base URL:** `https://idp.valyd.vip/api/auth/oidc` - **Path:** `/verifications` - **Auth / required scope:** Bearer access token required; required scope: `verifications` - **Required headers:** - `Accept: application/json` - `Authorization: Bearer YOUR_ACCESS_TOKEN` Returns the user's verification status: whether they passed a human (liveness) check, whether they completed identity (KYC) verification, and any professional licenses linked to their Valyd identity. Use alongside `/userinfo` for a complete user picture. `YOUR_ACCESS_TOKEN`: the `access_token` returned by [`POST /api/auth/oidc/token`](#post-apiauthoidctoken--token-exchange--refresh) (code exchange or refresh grant). **Response fields (`data.verifications`):** | Field | Type | Description | |---|---|---| | `human_verified` | boolean | The user passed a liveness / anti-spoof human check. Falls back to `id_verified` when no explicit human check is on file. | | `id_verified` | boolean | The user completed identity (KYC) document verification. | | `licenses` | array | Professional / credential licenses linked to the user. Empty array if none. | | `licenses[].license_type` | string | The kind of license (e.g. `drivers_license`, `medical`). | | `licenses[].verified` | boolean | Whether that license is currently verified. | | `licenses[].verified_from` | string \| null | Source the license was verified against. | | `licenses[].expire_at` | string \| null | ISO-8601 expiry timestamp, or `null` if it does not expire. | ### Example ```bash curl -X GET "https://idp.valyd.vip/api/auth/oidc/verifications" \ -H "Accept: application/json" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ### Response ```json { "success": true, "data": { "verifications": { "human_verified": true, "id_verified": true, "licenses": [ { "license_type": "drivers_license", "verified": true, "verified_from": "kyc", "expire_at": "2027-03-01T00:00:00+00:00" } ] } } } ``` ================================================================================ === FILE: https://docs.valyd.vip/docs/environments.md === ================================================================================ > Source: https://docs.valyd.vip/docs/environments # Environments & credentials Valyd runs the same product on independent environments. **These docs describe the development environment (`*.valyd.work`).** Each environment has its own hosts, its own Developer Portal, and its own credentials — an app you register in one environment does not exist in another. ## Hosts per environment Every environment exposes the same three hosts under a different domain: | Role | Development | What it is | | --- | --- | --- | | **API** (`idp`) | `https://idp.valyd.vip` | The Valyd API your app calls — verification, the account APIs, and sign-in (OAuth 2.0 / OIDC). | | **Developer Portal** (`dev`) | `https://dev.valyd.vip` | Where a human creates apps, gets keys, and composes verification workflows. No API automates app creation. | | **Docs** (`docs`) | `https://docs.valyd.vip` | These docs, the [API Playground](/sandbox), and the live demos. | The production and testing environments mirror this layout on their own domains. Point your integration at the API host for the environment you registered your app in, and never mix a key from one environment with the host of another. ## Where each credential comes from You get every credential from the [Developer Portal](https://dev.valyd.vip) for the matching environment — see [Create an app](/docs/create-project) for the full walkthrough. Which ones you need depends on your integration: | Credential | Format | Issued for | Used as | | --- | --- | --- | --- | | Client ID | `9357c59b…` | Connect with Valyd (OAuth 2.0 / OIDC) | `client_id` in the authorize + token requests | | Client Secret | `sk_live_…` (shown once) | Connect with Valyd | `client_secret` in the server-side token exchange | | App API key | `vrf_…` (shown once) | Verification API | the `X-API-Key` header on every verification call | | Webhook signing secret | `whsec_…` | Verification sessions | verifying the HMAC signature on incoming webhooks | | Workflow ID | `wf_…` | Verification sessions | the `workflow_id` you pass when creating a session | Connect with Valyd integrations hold the `client_id` + `client_secret`; verification integrations hold the App API key (plus a workflow ID and webhook secret for verification sessions). The two credential families are independent — the App API key never authenticates Connect, and `client_secret` never authenticates verification calls. ## Environment variables used across the quickstarts The [quickstarts](/docs/quickstarts) and SDK read these from your server environment. Keep them per-environment (a separate `.env` for dev vs production) and never expose a secret in frontend code: ```bash # .env (server-side only) VALYD_IDP_URL=https://idp.valyd.vip # the API host for THIS environment # Connect with Valyd (OAuth 2.0 / OIDC) VALYD_CLIENT_ID=9357c59bc1794b4c9efe8823e5878147 VALYD_CLIENT_SECRET=sk_live_a1b2c3d4e5f6g7h8i9j0... VALYD_REDIRECT_URI=https://yourapp.com/callback # exact registered callback, no trailing slash # Verification API (independent of the two OIDC values above) VALYD_API_KEY=vrf_... VALYD_WEBHOOK_SECRET=whsec_... # verification sessions only VALYD_WORKFLOW_ID=wf_... # verification sessions only ``` Setting `VALYD_IDP_URL` per environment is how the same code deploys everywhere: change the host, supply that environment's credentials, and nothing else moves. ## Separate test and production apps Even inside one environment, create **separate apps** (for example "Test" and "Production") so each has its own `client_id` / `client_secret`, App API key, workflows, and webhook endpoint. You can rotate or revoke the test app's key without touching production. See [Testing](/docs/testing) for how verification runs for real against your wallet, and the one-time credit every new account starts with. ## Related - [Apps & API keys](/docs/create-project) — the portal walkthrough. - [Testing your integration](/docs/testing) — real checks, the welcome credit, and webhook testing. - [API key lifecycle](/docs/api-key-lifecycle) — rotating and revoking the App API key. ================================================================================ === FILE: https://docs.valyd.vip/docs/errors.md === ================================================================================ > Source: https://docs.valyd.vip/docs/errors # Errors & troubleshooting Every Valyd error is machine-readable: a stable `code` string plus an HTTP status, and every response carries an `X-Request-Id` header — quote it to support (never API keys or tokens). Read the status first (it tells you the *category*), then the code (the *exact* cause). ## Contacting support Include: the **`X-Request-Id`** header value, the session/event id if relevant, a timestamp, and the endpoint called. **Never send** API keys, tokens, or the person's identity data — support will never ask for them. ## 1. Error shapes **Login / account APIs** (`/api/auth/...`) return the envelope: ```json { "success": false, "error": { "code": "invalid_token", "message": "Session expired. Please login again." } } ``` **OIDC token endpoint** (`/api/auth/oidc/token`) returns standard top-level OAuth errors: ```json { "success": false, "error": { "code": "invalid_grant", "message": "Invalid or expired authorization code" } } ``` **Verification API** (`/api/v2/...`) errors carry the code in `error` with check context where relevant. ## 2. What the HTTP status means | Status | Category | Typical causes | | --- | --- | --- | | `400` | Bad request | Missing/malformed parameter, invalid scope, expired code | | `401` | Not authenticated | Wrong `client_id`/secret, missing/expired token, bad API key | | `402` | Payment required | Wallet balance / quota exhausted — top up in the console | | `403` | Not allowed | Missing scope, app not permitted for this user, consent declined | | `404` | Not found | Wrong workflow/session/resource id, or it belongs to another project | | `409` | Conflict | Duplicate — e.g. the member already exists on your roster | | `410` | Gone | Removed legacy endpoint (TPSSO) — migrate to `/api/auth/oidc/*` | | `422` | Rejected input | Image/selfie quality, face rescan required, validation failure | | `429` | Rate limited | Too many requests — back off and retry after the window | | `5xx` | Our side | Retry with backoff; if it persists, contact support | ## 3. Complete code catalog _Generated from the API source — 147 codes. Do not edit by hand; run `node scripts/gen-error-catalog.mjs`._ | Code | HTTP | Meaning / fix | | --- | --- | --- | | `access_denied` | 403 | The user declined, or the app is not permitted for this account. | | `account_dob_unavailable` | 422 | This Valyd account has no verified date of birth. | | `account_not_found` | 404 | No Valyd account matches that token. | | `activation_failed` | 500, 503 | Activation failed. | | `already_exists` | 409 | E2E encryption is already active for this device | | `already_paired` | 409 | This device is already paired and has E2E keys | | `app_default_protected` | 422 | The default app cannot be deleted. Set another app as default first. | | `back_image_not_found` | 404 | Back image not found | | `cache_clear_failed` | 400 | Failed to clear caches: | | `callback_not_allowed` | 422 | callback must exactly match an active, approved HTTPS webhook destination for this project. | | `challenge_expired` | 400 | The liveness instruction expired — please try again. | | `challenge_required` | 400 | This device must complete challenge flow before login. Call POST /api/auth/face/challenge, then send c, d, s, and face_image. | | `client_not_found` | 404 | Client not found | | `code_expired` | 410 | The one-time code has expired | | `database_error` | 500 | Failed to store device keys | | `decrypt_failed` | 500 | Could not open the managed payload | | `deletion_failed` | 500 | Could not delete your account. Please try again. | | `denied` | 403 | User denied the request | | `device_already_paired` | 409 | This device is already paired | | `device_key_unknown` | 410 | This browser's device key is no longer registered. Please register this device again. | | `device_mismatch` | 400 | device_id does not match | | `device_not_found` | 404 | Device not registered or not linked to a user | | `empty_face_feature` | 400 | Empty face feature from SDK | | `endpoint_not_found` | — | | | `endpoint_removed` | 410 | You are calling a removed legacy TPSSO endpoint — migrate to /api/auth/oidc/*. | | `engine_unreachable` | 503 | Verification engine did not respond. Please retry. | | `expired` | 410 | Request expired | | `face_feature_extraction_failed` | 400 | Failed to extract face features | | `face_match_failed` | 500 | Face match failed | | `face_match_unavailable` | 502 | Could not verify your selfie right now. Please try again. | | `face_mismatch` | 403 | Face did not match | | `face_not_enrolled` | 409 | No face is enrolled on this account | | `face_not_matched` | 403 | Face does not match logged-in user | | `face_not_verified` | 403 | Face was not verified for this tracking ID | | `face_required` | 409 | A face check is required to release identity data | | `face_rescan_required` | 422 | We could not confidently recognize you. Please rescan your face. | | `failed_to_extract_id_portrait_feature` | — | | | `feature_extraction_failed` | 400 | Feature extraction failed | | `feature_failed` | 502 | Feature extraction failed | | `feature_size_mismatch` | — | | | `forbidden` | 403 | Invalid internal auth | | `frames_required` | 400 | The demo needs a 3-8 frame live burst from your camera. | | `front_image_id_required` | 400 | front_image_id is required | | `front_image_not_found` | 404 | Front image not found | | `idempotency_in_progress` | 409 | A request with this Idempotency-Key is still being processed. Retry shortly. | | `idempotency_key_reused` | 422 | This Idempotency-Key was already used with a different request body. | | `identity_locked` | 403 | Your identity is verified. Only your email and phone number can be changed. Contact support if something else is wrong. | | `image_not_found` | 404 | Image not found | | `image_too_large` | 413 | That photo is too large to upload. Please use a smaller image (under 20 MB). | | `insufficient_scope` | 403 | The access token lacks a required scope (openid is required for OIDC resource calls). | | `invalid_activation` | 404 | This activation link is invalid or has expired. | | `invalid_api_key` | 401 | Invalid verification API key. | | `invalid_audience` | — | | | `invalid_challenge` | 403 | Invalid or expired challenge, or signature verification failed | | `invalid_challenge_result` | 403 | Invalid challenge result | | `invalid_client` | 401 | Check client_id/client_secret and that the app is active in the Developer Portal. | | `invalid_client_metadata` | 400 | client_name must be between 1 and 120 characters | | `invalid_config` | 500 | AGENT_API_KEY not configured | | `invalid_date` | 400 | Invalid expire_at format | | `invalid_feature` | 400 | Invalid face vector | | `invalid_frames` | 400 | Live verification needs at least 3 frames; send one `selfie` for single-click mode. | | `invalid_grant` | 400, 401 | Code/refresh token expired, already used, or issued to another client — restart the flow. | | `invalid_idp_response` | — | | | `invalid_image` | 400, 422 | A selfie is required. | | `invalid_recovery_phrases` | 403 | Invalid recovery phrases | | `invalid_redirect_uri` | 400 | redirect_uris (non-empty array) is required | | `invalid_request` | 400, 401, 404, 422 | A required parameter is missing or malformed — compare against the reference. | | `invalid_scope` | 400 | Enable the scope for your app in the Developer Portal before requesting it. | | `invalid_session` | 400 | Session has no user_ref (pollus_id) | | `invalid_state` | 409 | Share request already decided | | `invalid_status` | 400 | Invalid status | | `invalid_token` | 401, 403 | Token missing/expired — refresh it or sign the user in again. | | `kyc_required` | 400 | Complete ID verification before verifying the license. | | `legacy_ocr_failed` | — | | | `license_not_found` | 400, 404 | CPR license not found | | `limit_reached` | 422 | Vault item limit reached | | `liveness_unavailable` | 502 | Could not check your selfie right now. Please try again. | | `logo_invalid` | 422 | Logo must be an image data URL. | | `logo_too_large` | 422 | Logo image is too large. Please use a smaller file. | | `misconfigured` | 500 | Invalid signing key | | `misconfigured_oidc_endpoints` | — | | | `missing_dob` | 400 | A date of birth is required (provide `dob` or run id-verification first). | | `missing_document` | 400 | Upload a selfie before running liveness. | | `missing_parameter` | 400 | Provide valyd_id or vendor_data. | | `no_existing_e2e` | 400 | User has no existing E2E keys. Use bootstrap/complete for first device setup. | | `no_face` | 422 | No usable face detected in the selfie | | `no_images_provided` | 400 | At least one image (front or back) is required | | `no_reusable_record` | 400 | No reusable verification found — please verify fully. | | `no_verification` | 404 | This app has no verification set up yet. Open Verification in the dev console once to provision it. | | `not_found` | 404 | User not found | | `not_linked` | 400 | Sign in with Valyd before reusing your identity. | | `otp_expired` | 410 | This OTP has expired | | `otp_invalid` | 400 | Invalid OTP | | `otp_not_found` | 404 | Invalid OTP | | `pairing_already_exchanged` | 409 | Tokens have already been issued for this pairing | | `pairing_already_fulfilled` | 409 | This pairing request has already been fulfilled | | `pairing_expired` | 410 | Pairing request has expired | | `pairing_incomplete` | 400 | Pairing request is missing Device B public key. Device B must provide its public key first. | | `pairing_not_found` | 404 | Pairing request not found | | `pairing_not_fulfilled` | 400 | Pairing is not yet complete. Status: | | `portrait_invalid` | — | | | `portrait_not_found` | — | | | `rate_limited` | 429 | Back off and retry after the window resets. | | `recovery_phrases_not_set` | 404 | Recovery phrases not set for this user | | `registration_failed` | 500 | Registration failed. Please try again. | | `required_face_checks_incomplete` | 409 | Approval requires passed ID verification, liveness, and face match checks. | | `requires_login` | 401 | User must authenticate first | | `reuse_not_available` | 400 | Your Valyd account is no longer verified — please complete the full verification. | | `review_not_pending` | 409 | Only an IN_REVIEW session can be manually decided. | | `sdk_compare_failed` | — | | | `server_error` | 500 | Unexpected error | | `session_closed` | 409 | This verification session is already closed. | | `session_expired` | 410 | Verification session has expired | | `session_not_found` | — | Wrong or expired session id. | | `sso_error` | 500 | Sign-in failed. Please try again. | | `sso_exchange_failed` | 401 | Could not complete sign-in with Valyd. | | `sso_no_identity` | 422 | Valyd did not return any account identity. | | `sso_not_configured` | 503 | Valyd SSO is not configured on this server. | | `stored_reference_invalid` | — | | | `stored_reference_invalid_type` | — | | | `tamper_detected` | 409 | payload_hash does not match the server-signed attestation | | `token_error` | 500 | Could not issue tokens | | `token_issuance_failed` | 500 | Token issuance failed | | `token_missing` | 401 | Session token is required. | | `too_many_attempts` | 429 | Too many failed face checks | | `too_many_frames` | 400 | At most 8 frames are accepted. | | `tracking_already_used` | 403 | Tracking ID has already been used | | `tracking_expired` | 403 | Tracking ID has expired | | `tracking_not_found` | 404 | Tracking ID not found or invalid | | `unauthenticated` | 401 | Account not found. | | `unauthorized` | 401, 403 | Not authorized. | | `unauthorized_client` | 403 | This app is not active. | | `unauthorized_domain` | 403 | Unauthorized domain | | `unknown_band` | — | | | `unsupported_grant_type` | 400 | only authorization_code supported | | `unsupported_response_type` | 400 | Only response_type=code is supported | | `user_deleted` | 410 | This device was linked to a deleted account. Please clear your local data and register again. | | `user_has_no_face_reference` | — | | | `user_not_found` | 400, 404 | User not found | | `userinfo_failed` | — | | | `valyd_account_mismatch` | 403 | This verification belongs to a different Valyd account. Sign in as that account to continue. | | `valyd_login_required` | 401 | Sign in to your Valyd account to continue this verification. | | `verification_soft_locked` | 423 | Verification is temporarily locked. Please contact support. | | `wc_error` | 400 | WC API error | | `workflow_not_found` | 404 | The workflow id does not belong to this project — copy it from the portal. | | `wrapped_ku_recovery_not_set` | 404 | WrappedKuRecovery not set for this user | | `wrong_endpoint` | 400 | This endpoint expects image IDs, not file uploads. Use the file upload endpoint instead. | ## 4. Troubleshooting the common integration mistakes **State mismatch on the callback.** The `state` on your callback must equal the one you stored before redirecting (the button stores it in the `valyd_oidc_state` cookie; the SDK transaction carries it). If it differs, the login is forged or expired — restart the flow. Never skip this check. **`invalid_grant` on token exchange.** Authorization codes are single-use and expire in ~2 minutes, and are bound to your client and `redirect_uri`. Exchange immediately, exactly once, with the same `redirect_uri` you authorized with. **Redirect URI mismatch.** URIs are matched **exactly** — scheme, host, port, and path. Register every environment's callback in the Developer Portal. **`insufficient_scope` on userinfo.** OIDC resource calls require the `openid` scope in the token. The SDK and button add it automatically; raw integrations must include it in `scope`. **Wrong environment host.** Each environment has its own IdP host (this documentation's is `idp.valyd.vip`). A token from one environment never works on another — and the Sign-in button targets whichever host served its script. **Refresh suddenly failing.** Refresh tokens rotate on every use. If you replay an old one, every refresh token for that user+client is revoked (theft protection) — persist the newest token atomically, then re-login once. ================================================================================ === FILE: https://docs.valyd.vip/docs/flows/authorization-code.md === ================================================================================ > Source: https://docs.valyd.vip/docs/flows/authorization-code # Authorization Code flow > 🔑 **Auth:** `client_id` + `client_secret` (server-side) · 👤 **This IS the login** — standard OpenID Connect · 📖 **Result:** tokens on your backend, user profile via UserInfo The Authorization Code flow is the main way an app logs a user in with Valyd. The browser only ever carries a one-time `code`; your backend exchanges it for tokens using your `client_secret`, so no token ever touches the front end. ## When to use it - Any app with a backend (web app, SSR site, mobile app with a server) that wants **Connect with Valyd**. - You want the user's profile, `id_verified` status, licenses, or verification proofs after login. - You're using the [drop-in button](/docs/flows/button), the `@valyd/sdk`, or [your own OIDC library](/docs/oidc) — all of them run this exact flow underneath. Don't use it for the [Unique Human API](/verifications/unique-human) — those API-key calls answer "is this a live, unique human?" with no user login involved at all. ## How it works ```mermaid sequenceDiagram participant B as Browser participant Y as Your backend participant V as Valyd IdP B->>Y: 1. GET /login Note over Y: generate state + nonce (+ PKCE), store server-side Y-->>B: 2. 302 to /api/auth/oidc/authorize?client_id=... B->>V: 3. follow redirect Note over B,V: user signs in and approves the scopes (consent screen) V-->>B: 4. 302 to your redirect_uri?code=...&state=... B->>Y: 5. GET /callback?code&state Note over Y: 6. compare state (CSRF) Y->>V: 7. POST /api/auth/oidc/token V-->>Y: access_token + refresh_token + id_token Note over Y: 8. verify id_token (RS256/JWKS, nonce) Y->>V: 9. GET /userinfo (Bearer) V-->>Y: sub, name, id_verified, ... Y-->>B: 10. your own app session ``` ## Steps 1. **Start the flow.** Your login route generates a random `state` + `nonce` (and an S256 PKCE pair), stores them server-side, and redirects the browser to `https://idp.valyd.vip/api/auth/oidc/authorize` with `client_id`, `redirect_uri`, `response_type=code`, `scope` (must include `openid`), `state`, and `nonce`. With the SDK this is `valyd.createAuthorizationRequest({ scope: [...] })`. 2. **User authenticates and consents.** Valyd shows the consent screen with the requested scopes; on approval it issues a one-time authorization `code`. 3. **Callback.** Valyd redirects the browser to your registered `redirect_uri` with `?code=…&state=…`. The `state` is echoed back unchanged. 4. **CSRF check.** Compare the callback `state` strictly against the value you stored. Reject with HTTP 400 on any mismatch, before touching the code. 5. **Exchange the code (server-side).** `POST https://idp.valyd.vip/api/auth/oidc/token` with `grant_type: "authorization_code"`, your client credentials, the `code`, and the **same** `redirect_uri`. The response is a top-level token JSON: `access_token`, `refresh_token`, `id_token`, `expires_in` (≈ 900), `scope`, `token_type`. 6. **Validate the ID token.** Verify the RS256 signature against the JWKS at `https://idp.valyd.vip/api/auth/oidc/jwks.json`, and check `iss`, `aud` (= your `client_id`), `exp`, and that `nonce` equals the value you sent. The SDK's `handleCallback()` does steps 4–6 in one call. 7. **Fetch the user.** `GET https://idp.valyd.vip/api/auth/oidc/userinfo` with `Authorization: Bearer ` returns `sub` (stable `valyd_…` id), `preferred_username`, `name`, `id_verified`, and more per the granted scopes. Set your own app session and you're done. ## Security notes - **Codes are single-use, short-lived, and client-bound** — exchange immediately; a replay returns `invalid_grant`. - **The `state` comparison is your CSRF protection.** Never skip it. - **The `nonce` check is your replay protection** for the ID token. - **`client_secret` and tokens live on your backend only** — the exchange must never run in the browser. - Access tokens last ~15 minutes; use the [refresh flow](/docs/flows/refresh) to renew. Refresh tokens rotate on every use. - The `redirect_uri` must exactly match a registered redirect URI — scheme, host, and path. ## Build it - Drop-in front end: the [Sign-in button flow](/docs/flows/button) - Full raw-HTTP walkthrough with SDK + Python/PHP/Java examples: [Authentication](/docs/authentication) - Complete Express example: [Node.js quickstart](/docs/quickstart/node) - Bring your own library via discovery: [Use any OIDC library](/docs/oidc) - What's inside each token: [Tokens](/docs/tokens) ================================================================================ === FILE: https://docs.valyd.vip/docs/flows/button.md === ================================================================================ > Source: https://docs.valyd.vip/docs/flows/button # Sign-in button flow > 🔑 **Auth:** `client_id` in the tag, `client_secret` stays on your backend · 👤 **This IS the login** — the button runs the Authorization Code flow for you The drop-in button (`https://idp.valyd.vip/signin/client.js`) is a front end for the [Authorization Code flow](/docs/flows/authorization-code). It generates `state` and `nonce`, builds the authorize URL, and redirects the user to Valyd. The code exchange still happens on your backend with your `client_secret`. ```html ``` ## Redirect mode Before redirecting, the button stores the generated values in `valyd_oidc_state` and `valyd_oidc_nonce` cookies so your callback route can compare them. ```mermaid sequenceDiagram participant B as Browser participant Y as Your backend participant V as Valyd IdP Note over B: click button — set valyd_oidc_state + valyd_oidc_nonce cookies B->>V: navigate to /authorize Note over B,V: login + consent V-->>B: 302 to redirect_uri?code&state B->>Y: GET /auth/valyd/callback Note over Y: compare state vs cookie Y->>V: exchange code V-->>Y: tokens Y-->>B: your app session ``` Backend handler (the whole thing): ```typescript app.get("/auth/valyd/callback", async (req, res) => { const { user } = await valyd.handleCallback(req.url, { expectedState: req.cookies.valyd_oidc_state, // the button set this cookie nonce: req.cookies.valyd_oidc_nonce, }); res.redirect("/dashboard"); }); ``` ## Security notes - Your backend owns the code exchange, ID-token validation (RS256/JWKS, `nonce`, `aud` = your `client_id`), and session creation. Never accept tokens minted anywhere but your own backend. - Codes are single-use and expire fast — send them to your backend immediately. - The cookie comparison **is** the CSRF check; if cookies are being blocked (third-party contexts, `SameSite`), legitimate logins fail with `state mismatch` — see [Errors](/docs/errors). - `client_secret` never appears in the page. ## Build it - The flow underneath: [Authorization Code flow](/docs/flows/authorization-code) - Button + callback walkthrough: [Connect with Valyd](/docs) - Full working app: [Node.js quickstart](/docs/quickstart/node) - What comes back in the tokens: [Tokens](/docs/tokens) ================================================================================ === FILE: https://docs.valyd.vip/docs/flows/refresh.md === ================================================================================ > Source: https://docs.valyd.vip/docs/flows/refresh # Refresh & logout flow > 🔑 **Auth:** `client_id` + `client_secret` + `refresh_token` (backend only) · 🔁 **Rotation:** every refresh returns a NEW refresh token · 🚪 **Logout:** RP-initiated via `end_session_endpoint` Access tokens live ~15 minutes (`expires_in` ≈ 900). The refresh grant renews them from your backend without bothering the user — for up to 30 days per refresh-token family. ## How it works ```mermaid sequenceDiagram participant Y as Your backend participant V as Valyd IdP Y->>V: POST /api/auth/oidc/token (grant_type refresh_token, RT_1, client credentials) Note over V: RT_1 revoked, RT_2 issued V-->>Y: new access_token (15 min) + RT_2 — persist this! Y->>V: ...later... refresh with RT_2 Note over V: RT_2 revoked, RT_3 issued Y->>V: replay of RT_1 (already rotated away) Note over V: THEFT SIGNAL — the whole family is revoked V-->>Y: rejected (RT_2, RT_3, ... all revoked — user must log in again) ``` ## Steps 1. `POST https://idp.valyd.vip/api/auth/oidc/token` with `{ "grant_type": "refresh_token", "refresh_token": "…", "client_id": "…", "client_secret": "…" }`. 2. Read the top-level response: a fresh `access_token` **and a new `refresh_token`**. 3. **Persist the new refresh token, atomically replacing the old one.** With the SDK (`@valyd/sdk`): `const next = await valyd.auth.refreshToken(stored)` — store both `next.accessToken` and `next.refreshToken`. ## Rotation & theft detection - **Rotation is on for every refresh**: the token you sent is revoked the moment the new one is issued. A "refresh token" is therefore a chain, not a value — always save the latest link. - **Replaying a rotated-away token is treated as theft** and revokes *every* refresh token for that user and client. That's a feature: if a token is stolen, either the thief or the real client eventually replays a stale one, and the whole family dies instead of living for 30 days. - Refresh tokens are client-bound — a token leaked from one app cannot be used by another. - Practical consequences: don't refresh the same stored token from two processes concurrently, and if your persist step can fail, treat "refresh succeeded but save failed" as a forced re-login. ## Logout & revocation RP-initiated logout is `GET https://idp.valyd.vip/api/auth/oidc/logout`, advertised in discovery as `end_session_endpoint`: ```text https://idp.valyd.vip/api/auth/oidc/logout?id_token_hint=ID_TOKEN&post_logout_redirect_uri=https://yourapp.com/logged-out&state=RANDOM ``` - `id_token_hint` — the `id_token` from login; an **expired one is accepted** (its signature still proves the user/client). - `post_logout_redirect_uri` — must **exactly match** one of your registered redirect URIs, so register your post-logout URL as an additional redirect URI. - `state` — optional, echoed back. It revokes the user's refresh and access tokens **for your client**, then redirects. Also clear your own app session — Valyd can't do that for you. ## Build it - Where the first refresh token comes from: [Authorization Code flow](/docs/flows/authorization-code) - Token/logout endpoint details: [API reference](/docs/endpoints#post-apiauthoidctoken--token-exchange--refresh) - Lifetimes and claims: [Tokens](/docs/tokens) - Login session vs verification session: [Sessions](/docs/tokens) ================================================================================ === FILE: https://docs.valyd.vip/docs/go-live.md === ================================================================================ > Source: https://docs.valyd.vip/docs/go-live # Go-live checklist Everything below is enforced or documented elsewhere in these docs — this page just puts it in one place for the day you flip to production. ## Credentials - [ ] **All secrets live server-side.** `client_secret`, tokens, the App API key (`X-API-Key`), and the webhook signing secret never enter browser code or client storage. - [ ] **Separate production app.** Use a distinct app in the [Developer Portal](https://dev.valyd.vip) for production, with its own key and workflows — never your test credentials. - [ ] **Rotate keys you may have leaked** during development (the console can rotate the App API key and webhook signing secret). ## Login (OIDC) - [ ] **Exact HTTPS redirect URI** registered — swap the `http://localhost` URI you used in development for your real callback URL, matched character-for-character. - [ ] **State, nonce, and PKCE handled by the SDK** (`createAuthorizationRequest` → `handleCallback`). Never hand-roll them — the [complete example](/docs/quickstart/node) shows the pattern, including the server-side transaction store. - [ ] **Production session config**: shared session store, `secure: true` cookies, correct trusted-proxy handling ([details](/docs/quick-start#five-minute-acceptance-check)). ## Verification results - [ ] **Never trust the redirect `?status=` param** — it is a hint only. The authoritative outcome is the signed webhook and `GET /api/v2/session/{id}/decision` ([statuses](/verifications/statuses)). - [ ] **Handle every terminal status**: `APPROVED` (grant), `DECLINED` (deny + retry path per your policy), `ABANDONED` / `EXPIRED` (treat as not verified; offer a new session), and the non-terminal `IN_REVIEW` (wait — do not grant access). - [ ] **Send `Idempotency-Key`** on billable `POST /api/v2/*` calls so retries can't double-run or double-charge ([idempotency](/verifications/unique-human#idempotency)). ## Webhooks - [ ] **Verify the HMAC signature on the RAW body** (`X-Valyd-Signature`, `HMAC_SHA256("{timestamp}.{rawBody}", secret)`, constant-time compare) — no JSON re-serialization ([webhooks](/verifications/webhooks)). - [ ] **Reject stale timestamps** (> 5 minutes) for replay protection — `X-Valyd-Timestamp` is signed into the HMAC. - [ ] **Deduplicate on `X-Valyd-Event-Id`.** Delivery is at-least-once; retries and manual resends carry the same event id. - [ ] **Return 2xx fast**, defer heavy work to a queue — non-2xx deliveries are retried for ~2.5 hours. ## Data hygiene - [ ] **Don't log PII.** Keep decision payloads (extracted ID fields, portraits) out of application logs; store only what your policy requires. - [ ] **Prefer proofs over raw fields** — request `is_18_plus` rather than `dob`; raw identity data goes through the [consent flow](/docs/request-data) only. - [ ] **Read sealed consent payloads promptly** — they are purged about 5 minutes after approval. ## Operations - [ ] **Wallet funded** — checks are billed per call; an empty balance returns `402` ([errors](/docs/errors)). - [ ] **Generous timeouts** for credential lookups (10–60 s; e.g. `timeoutMs: 90_000`). - [ ] **Build defensively** for additive API changes: ignore unknown response fields and enum values, pin `/api/v2` ([versioning](/verifications/versioning)). - [ ] **Subscribe to the [changelog](/docs/changelog)** for deprecation notices. ================================================================================ === FILE: https://docs.valyd.vip/docs/how-valyd-works.md === ================================================================================ > Source: https://docs.valyd.vip/docs/how-valyd-works # How Valyd works New to Valyd or OIDC? Here's the whole mental model in one picture: **a person verifies once, the proof lives on their Valyd ID, and every app that adds Connect with Valyd just reads it.** The three pictures below unpack that loop. ## 1. A person verifies once Somewhere — in your app, or any app using Valyd — a person proves who they are: they scan their ID, pass a liveness check, match their face, or have a professional license looked up. Valyd runs that check for real (there are no simulated results) and, when the person has a Valyd account, the passed outcome is saved to it as a **proof**. ## 2. They connect their identity to your app **Connect with Valyd** is a standard OIDC button (its "Sign in with Valyd" form can double as your login button, like "Sign in with Google"). The person authenticates with their verified identity — face, not passwords — and approves what your app may read (the [scopes](/docs/scopes)). Your backend gets tokens. You never see their documents — you read the **answers**: `id_verified: true`, verified licenses, age bands. ## 3. Need a new check? Attach it to the account If the account doesn't hold the proof you need yet, run the check yourself — with the signed-in user's token attached, so the result saves back to their account: That's the loop: **verify once → sign in anywhere → read the proof — and only re-verify when your policy wants a fresher answer.** ## The two products Everything above is **Reusable Verification**: the person connects their Valyd identity via Connect with Valyd, your app [reads the verified data](/docs/user-token/account) it already holds, and [runs a verification](/verifications/quickstart) through a configured [workflow](/verifications/workflows) for anything missing. There is one deliberately separate product: the **[Unique Human API](/verifications/unique-human)**. It answers a single question — *is this a live, unique human?* — with nothing but your API key: [Liveness](/verifications/unique-human/antispoof) stops a photo or replay standing in for a real person, and [Uniqueness](/verifications/unique-human/face-uniqueness) catches the same face opening a second account. No user login is involved, the result returns straight to your caller, and storing and protecting the person's data is your responsibility. ## What workflows let you build The [workflow checks](/verifications/types) aren't just onboarding KYC — you run a verification **whenever** you need fresh proof, tied to the signed-in person: - **Prove it's really them, right now** — a face match against their enrolled face before a sensitive action (a payout, a settings change, a shift clock-in). - **Confirm they're actually there** — a location check proves the person is where they claim (home-visit care, field work, geofenced access). - **Re-check a live credential** — re-verify a professional license against the registry so an expired or revoked one is caught, not trusted from last year. - **Confirm they're a live human** — liveness stops a photo or replay standing in for the real person. - **Gate by age** — an age band (`is_18_plus`, …) without ever touching their date of birth. Compose several into one [workflow](/verifications/workflows) and run them in a single [session](/verifications/quickstart) — each passed check saves as a reusable proof on the person's Valyd ID, so next time you just read it. **Verify once, then re-prove exactly what your policy needs, exactly when it needs it.** ## Where to next - Start at the beginning → [Introduction](/docs/introduction) - Add the button → [Connect with Valyd](/docs) - Run your first check → [Verification quickstart](/verifications/quickstart) - The exact meaning of every term → [Concepts & terms](/docs/introduction#concepts--terms) ================================================================================ === FILE: https://docs.valyd.vip/docs/idempotency.md === ================================================================================ > Source: https://docs.valyd.vip/docs/idempotency # Idempotency Every **billable** verification the SDK runs accepts an optional `idempotencyKey`. Send one and a network retry can never double-run or double-charge the same check: Valyd runs the operation once, records the result against your key, and returns that stored result for any later request carrying the same key. ## Why it matters Verification checks are real and billed against your app's wallet. If a request times out or your connection drops, you cannot know whether the check ran. Retrying without an idempotency key risks running — and charging — the check twice. Retrying **with** the same key is safe: the second request returns the first request's outcome instead of starting a new check. ## With the Node SDK Generate a unique key per logical operation (a UUID is ideal) and reuse it across retries of that same operation. Pass `idempotencyKey` on any billable check and the SDK carries it for you: ```javascript const session = await verify.sessions.create({ workflowId, redirectUrl: "https://yourapp.com/checked", idempotencyKey: "5f2c…-your-unique-id", }); ``` Applies to every billable verification the SDK runs — session creation for both the [Unique Human API](/verifications/unique-human) and [Reusable Verification](/verifications). (All checks — liveness, uniqueness, ID/KYC, face match, age, professional license, location — run as [workflow checks](/verifications/types) on a session, not as direct calls.) See the [Node SDK reference](/verifications/sdk) for the full list of checks that accept `idempotencyKey`. ## Rules of thumb - **One key per operation, reused on retry.** A new operation gets a new key; a retry of the same operation reuses the original key. - **Change any input, change the key.** Reusing a key with a different payload returns the original stored result, not a fresh run. - **Keys are per app.** They are scoped to the App API key that issued the request. ## Related - [Testing your integration](/docs/testing) — why checks are always real and billed. - [Rate limits](/docs/rate-limits) — retry/backoff behaviour on `429`. - [Liveness (anti-spoof)](/verifications/unique-human/antispoof) — the Unique Human API's anti-spoof check. ================================================================================ === FILE: https://docs.valyd.vip/docs/introduction.md === ================================================================================ > Source: https://docs.valyd.vip/docs/introduction # Introduction Valyd is a verified-identity platform. It gives developers **two products**: - **[Unique Human API](/verifications/unique-human)** — determine whether you're interacting with a **live, unique human**. You create an app, copy its API key, start a session for a workflow with the liveness and/or uniqueness checks, and redirect the person to Valyd's verification page. The verdict returns to your system; no account is involved and nothing is saved to one. - **[Reusable Verification](/verifications)** — users **connect their Valyd identity** (standard OpenID Connect), you read the verified information they already hold and have consented to share, and for anything missing you run a **[workflow](/verifications/workflows)** — KYC, professional licenses, face match, liveness, location, and more. Passed proofs save to the user's Valyd ID, so they're reusable the next time. | If you want to… | Use | Start here | | --- | --- | --- | | Know whether this person is live and unique | **Unique Human API** | [Overview](/verifications/unique-human) | | Let users reuse verified identity — and verify what's missing | **Reusable Verification** | [Overview](/verifications) | > Connect with Valyd is built on OpenID Connect and can also serve as your app's sign-in — see > [Connect with Valyd](/docs). It's part of Reusable Verification, not a separate product. ## Onboarding a workforce? Use an organization If you're bringing a whole team or workforce onto Valyd — employees, staff, contractors — use an **Organization**. Your people connect with Valyd (standard OIDC), and each active member gets **unlimited verifications for $0.99 / month** (14-day free trial). Face login, no passwords, and you always know exactly who signed in. → **[Organizations](/docs/organizations)** — how it works, roles, member onboarding, and pricing. ## Where everything lives | Host | What it is | | --- | --- | | **`idp.valyd.vip`** | The Valyd API — verification, the account APIs, and sign-in. Your app talks to this host. | | **`dev.valyd.vip`** | The **Developer Portal** — create apps, get your keys, and configure workflows. | | **`docs.valyd.vip`** | These docs, the [API Playground](/sandbox), and the [live demos](/demos). | New accounts start with a **$100 credit** so you can build and [test](/docs/testing) for free. ## Concepts & terms | Term | Meaning | | --- | --- | | **Valyd ID** | A person's verified identity: their face vector, verified legal name, and every proof they've earned. One per human. | | `valyd_id` | The stable identifier of that identity — key your users on it. | | **App** | What you register in the [Developer Portal](https://dev.valyd.vip): the credentials your integration uses. | | **Check** | One verification action (KYC, liveness, face match, license lookup). Returns one result. | | **Workflow** | The saved bundle of checks you configure for your app — a user completes it in one [verification session](/verifications/quickstart). | | **Proof / badge** | The durable outcome of a passed check, saved on a Valyd account and reusable later. | | **Scope** | What a user permits your app to read when they connect. [Managed per app](/docs/scopes). | | **Organization** | A shared workspace: one team, shared apps, a workforce roster, one bill. [Details](/docs/organizations). | | **Consent flow** | The explicit approval step for releasing raw identity data — separate from connecting. | ================================================================================ === FILE: https://docs.valyd.vip/docs/oidc.md === ================================================================================ > Source: https://docs.valyd.vip/docs/oidc # Use any OIDC library or platform > 🔑 **Auth:** `client_id` + `client_secret` · 👤 Standard OpenID Connect · 🔌 Works with any OIDC-capable library or SSO console Valyd is a standards-compliant OpenID Connect provider. If your stack already speaks OIDC — Auth.js, Passport, Spring Security, django-allauth, or an enterprise platform's SSO console — point it at Valyd's discovery document and you're done. No Valyd-specific code. ## 1. Register a client In the [Developer Portal](https://dev.valyd.vip) create an app, enable the scopes you need, and register your platform's exact callback URL (for example `https://your-app.example.com/oidc/callback`). Copy the `client_id` and one-time `client_secret`. ## 2. Point your library at discovery ```text https://idp.valyd.vip/api/.well-known/openid-configuration ``` Most libraries take just the issuer + credentials. Auth.js example: ```typescript // auth.ts (Auth.js / NextAuth) providers: [ { id: "valyd", name: "Valyd", type: "oidc", issuer: "https://idp.valyd.vip", wellKnown: "https://idp.valyd.vip/api/.well-known/openid-configuration", clientId: process.env.VALYD_CLIENT_ID, clientSecret: process.env.VALYD_CLIENT_SECRET, authorization: { params: { scope: "openid profile" } }, }, ] ``` ## 3. Manual values (if your console has no discovery field) | Setting | Value | | --- | --- | | Issuer | `https://idp.valyd.vip` | | Authorization endpoint | `https://idp.valyd.vip/api/auth/oidc/authorize` | | Token endpoint | `https://idp.valyd.vip/api/auth/oidc/token` | | Userinfo endpoint | `https://idp.valyd.vip/api/auth/oidc/userinfo` | | JWKS URI | `https://idp.valyd.vip/api/auth/oidc/jwks.json` | | Scopes | `openid profile` (add `email`, `phone`, `verifications`, `doctor_license` as needed) | | Auth method | `client_secret_post` or `client_secret_basic` | | ID token algorithm | `RS256` | | PKCE | S256 supported | ## 4. Map claims to your user fields | Your user field | OIDC claim | | --- | --- | | Username / unique key | `sub` (stable `valyd_` id — use this as the primary key) | | Display username | `preferred_username` | | Email | `email` (requires the `email` scope) | | Phone | `phone_number` (requires the `phone` scope) | | Full name | `name` | | First / last name | `first_name` / `last_name` | | Identity verified | `id_verified` | | Country | `country` | Sample userinfo response: ```json { "sub": "valyd_f895da61d5174b81b8dd6a4e3b417339", "valyd_id": "valyd_f895da61d5174b81b8dd6a4e3b417339", "preferred_username": "john.doe", "email": "john.doe@example.com", "email_verified": true, "name": "John Doe", "first_name": "John", "last_name": "Doe", "id_verified": true, "country": "US" } ``` No photo is ever returned — Valyd accounts hold irreversible face vectors, not images, and the vector (template) itself is never exposed through any API. (The `portrait` a KYC check returns is extracted from the ID document submitted in that request, not a stored account photo — see [Data & trust](/docs/data-and-trust).) ## Notes - Redirect URIs are matched **exactly** — register every environment's callback URL. - Access tokens expire in ~15 minutes; refresh tokens rotate on every refresh — persist the new one. - Prefer our tooling instead? Use the [drop-in button](/docs) or the [`@valyd/sdk` quickstart](/docs/quick-start). Raw HTTP is documented in [Authentication](/docs/authentication). ================================================================================ === FILE: https://docs.valyd.vip/docs/operations-sla.md === ================================================================================ > Source: https://docs.valyd.vip/docs/operations-sla # Operations & SLA How Valyd runs the service in production: availability, status, incident communication, maintenance, and support. The one thing this page states with certainty is the support diagnostic contract — the `X-Request-Id` header documented in [Errors & troubleshooting](/docs/errors). A contractual SLA with committed availability figures is available for enterprise plans — contact your Valyd account team. Nothing on this page is itself a contractual commitment. ## In Status & reliability - **[Disaster recovery](/docs/disaster-recovery)** — backups, RPO/RTO, and failover posture. - **[Support & escalation](/docs/support-escalation)** — support channels, hours, and how to escalate an incident. ## Availability | Item | Value | | --- | --- | | Uptime / availability target | Committed availability targets are part of the enterprise SLA — contact your Valyd account team | | Formal SLA & remedies | A contractual SLA with service credits is available on enterprise plans — contact your Valyd account team | | Status page URL | Available on request — contact **support@valyd.id** | ## Incident communication | Item | Value | | --- | --- | | How incidents are communicated | Available on request — contact **support@valyd.id** | | Post-incident reviews | Post-incident reviews are available to enterprise customers on request — contact your Valyd account team | ## Maintenance | Item | Value | | --- | --- | | Scheduled-maintenance policy | Available on request — contact **support@valyd.id** | ## Degraded-dependency behavior When an upstream dependency (the verification engine, a face-match service, or a registry) is unreachable, Valyd surfaces it as a machine-readable error rather than a silent failure — retry with backoff. These are the documented signals from the [error catalog](/docs/errors#3-complete-code-catalog): - `engine_unreachable` (503) — the verification engine did not respond; retry. - `face_match_unavailable` (502) / `liveness_unavailable` (502) — the selfie could not be checked right now; retry. - `5xx` in general — Valyd-side; retry with backoff, and if it persists, contact support. Formal degraded-mode behavior and failover targets (RTO / RPO) are covered on [Disaster recovery](/docs/disaster-recovery) and, for enterprise plans, in the contractual SLA — contact your Valyd account team. ## Support & escalation Every Valyd response carries an **`X-Request-Id`** header. When you contact support, include: - the **`X-Request-Id`** header value, - the session / event id if relevant, - a timestamp, and - the endpoint you called. **Never send** API keys, tokens, or the person's identity data — support will never ask for them. This is the exact guidance from [Errors & troubleshooting](/docs/errors#contacting-support). | Item | Value | | --- | --- | | Support channel / address | **support@valyd.id** | | Support hours & response targets | Available on request — contact **support@valyd.id** | | Escalation path | See [Support & escalation](/docs/support-escalation); for production-impacting incidents contact your Valyd account team | ## See also - [Trust Center](/docs/security-trust) - [Rate limits](/docs/rate-limits) - [Errors & troubleshooting](/docs/errors) ================================================================================ === FILE: https://docs.valyd.vip/docs/organizations/api.md === ================================================================================ > Source: https://docs.valyd.vip/docs/organizations/api --- product: valyd-id api_version: oidc auth: client-credentials billable: true pii_mode: proofs human_setup_required: true source_of_truth: openapi --- # Organization API Manage your workforce roster **server-to-server**. Add and invite members, look people up, deactivate/reactivate, remove, re-send invites, and read your seats & billing — all under the base path **`/api/sdk`**. Every call is made with your organization's `client_id` + `client_secret`, so this API is **server-side only** — the secret must never reach a browser. The examples below use the `@valyd/sdk` client and raw HTTP against `https://dev.valyd.vip/api/sdk`. ```ts import { ValydClient } from "@valyd/sdk"; // Server-side only — the client secret never touches the browser. const client = new ValydClient({ clientId: process.env.VALYD_CLIENT_ID, clientSecret: process.env.VALYD_CLIENT_SECRET, }); ``` ## Authentication All endpoints authenticate with your organization app's **`client_id` + `client_secret`**, sent as request headers (`X-Client-Id` / `X-Client-Secret`), scoped to the organization that owns the app. The `@valyd/sdk` client sends them for you. ```http X-Client-Id: X-Client-Secret: Content-Type: application/json ``` ## Response envelope Every response is wrapped in a consistent envelope. **Success (any 2xx):** ```json { "success": true, "data": { } } ``` **Error:** ```json { "success": false, "error": { "code": "…", "message": "…" }, "data": { } } ``` The per-endpoint examples below show what goes inside `data`. ## The member object Wherever a member appears in a response, it is **the member object** — the same shape everywhere: ```jsonc { "id": 123, "member_id": "vmem_1a2b3c4d5e6f7a8b9c0d1e2f", // stable public id = the valyd_org_member_id login claim; prefix vmem_ + 24 hex "first_name": "Ada", "last_name": "Lovelace", "email": "ada@acme.com", "role": "member", // one of: owner | admin | developer | member "status": "active", // one of: invited | link_sent | active | deactivated "valyd_id": "usr_…", // null until the member connects their Valyd ID (face) "active": true, // convenience: status === "active" "created_at": "2026-08-21T10:00:00+00:00" } ``` - **`member_id`** — the stable public id (`vmem_` + 24 hex). It's the value of the `valyd_org_member_id` login claim, so it's your join key between your roster and Valyd's login layer. Wherever a path takes `{member_id}`, the member's **email** or **`valyd_id`** is also accepted. - **`valyd_id`** — `null` until the member scans their face and connects their Valyd ID. - **`active`** — a convenience boolean equal to `status === "active"`. --- ## List members Returns your workforce roster (**`role: member`** only), each with status and `valyd_id`, plus the organization it belongs to. ```ts const { members, organization } = await client.getMembers(); const activated = members.filter((m) => m.status === "active"); ``` ```http GET /api/sdk/members HTTP/1.1 Host: dev.valyd.vip X-Client-Id: X-Client-Secret: ``` ```json { "success": true, "data": { "members": [ { "id": 123, "member_id": "vmem_1a2b3c4d5e6f7a8b9c0d1e2f", "first_name": "Ada", "last_name": "Lovelace", "email": "ada@acme.com", "role": "member", "status": "active", "valyd_id": "usr_9f8e7d6c5b4a", "active": true, "created_at": "2026-08-21T10:00:00+00:00" } ], "organization": { "id": 42, "name": "Acme, Inc." } } } ``` ## Resolve one person Look up **one** person's membership at **any** role — so you can tell "you're a developer here" from "not in this org." Pass a `valyd_id` **or** an `email` (at least one). ```ts const { found, member } = await client.resolveMember({ valydId: "usr_9f8e7d6c5b4a" }); // or: await client.resolveMember({ email: "ada@acme.com" }); ``` ```http POST /api/sdk/members/resolve HTTP/1.1 Host: dev.valyd.vip X-Client-Id: X-Client-Secret: Content-Type: application/json { "valyd_id": "usr_9f8e7d6c5b4a" } ``` ```json { "success": true, "data": { "found": true, "member": { "id": 123, "member_id": "vmem_1a2b3c4d5e6f7a8b9c0d1e2f", "first_name": "Ada", "last_name": "Lovelace", "email": "ada@acme.com", "role": "member", "status": "active", "valyd_id": "usr_9f8e7d6c5b4a", "active": true, "created_at": "2026-08-21T10:00:00+00:00" } } } ``` Not in the organization returns `{ "found": false, "member": null }`. **Errors** — `400 missing_identifier` when neither `valyd_id` nor `email` is given. ## Add member(s) Add one or more people (1–500 per call). `email` is required per member; `first_name`, `last_name`, and `role` (default `member`) are optional. Options: `notify` (default `true`) emails the invite; `invite` (`auto` | `email` | `face`, default `auto`) picks the invite method — see [Members & onboarding](/docs/organizations/members#how-a-member-is-invited). Returns **`201`**. ```ts const { created, skipped, notified } = await client.addMembers( [ { email: "jane@acme.com", firstName: "Jane", lastName: "Doe" }, { email: "sam@acme.com" }, ], { notify: true, invite: "auto" } ); ``` ```http POST /api/sdk/members HTTP/1.1 Host: dev.valyd.vip X-Client-Id: X-Client-Secret: Content-Type: application/json { "members": [ { "email": "jane@acme.com", "first_name": "Jane", "last_name": "Doe" }, { "email": "sam@acme.com" } ], "notify": true, "invite": "auto" } ``` ```json { "success": true, "data": { "created": [ { "id": 201, "member_id": "vmem_aa11bb22cc33dd44ee55ff66", "first_name": "Jane", "last_name": "Doe", "email": "jane@acme.com", "role": "member", "status": "link_sent", "valyd_id": null, "active": false, "created_at": "2026-08-21T10:05:00+00:00", "activation_link": "https://idp.valyd.vip/m/activate/xxxxxxxxxxxx", "invite_method": "face" } ], "skipped": [ { "email": "sam@acme.com", "reason": "duplicate" } ], "notified": true } } ``` Each `created` entry is a member object plus: - **`activation_link`** — a string for **face** invites only; `null` otherwise. Email/claim links are emailed, never returned. Passing `notify: false` hands the face link back so you can deliver it yourself. - **`invite_method`** — `"face"`, `"email"`, or `"none"`. - **`reactivated`** — present and `true` when re-adding a previously **deactivated** email reactivates that person instead of skipping them. **Errors** — `400 invalid_request`, with a per-field validation map in `data.errors`. ## Deactivate Stops billing and revokes the member's app logins, but **keeps the row**. The person's Valyd identity is **not** deleted. ```ts const { member } = await client.deactivateMember("vmem_1a2b3c4d5e6f7a8b9c0d1e2f"); ``` ```http PATCH /api/sdk/members/vmem_1a2b3c4d5e6f7a8b9c0d1e2f/deactivate HTTP/1.1 Host: dev.valyd.vip X-Client-Id: X-Client-Secret: ``` ```json { "success": true, "data": { "member": { "id": 123, "member_id": "vmem_1a2b3c4d5e6f7a8b9c0d1e2f", "first_name": "Ada", "last_name": "Lovelace", "email": "ada@acme.com", "role": "member", "status": "deactivated", "valyd_id": "usr_9f8e7d6c5b4a", "active": false, "created_at": "2026-08-21T10:00:00+00:00" } } } ``` The path takes the `vmem_…` `member_id` (the member's email or `valyd_id` is also accepted). ## Remove Removes a member. **Default** (no `permanent`) is the same as [deactivate](#deactivate). Pass `permanent=true` to delete the membership row outright. The person's Valyd account is never deleted. ```ts await client.removeMember("vmem_1a2b3c4d5e6f7a8b9c0d1e2f", { permanent: true }); ``` ```http DELETE /api/sdk/members/vmem_1a2b3c4d5e6f7a8b9c0d1e2f?permanent=true HTTP/1.1 Host: dev.valyd.vip X-Client-Id: X-Client-Secret: ``` **Permanent delete** — the row is gone: ```json { "success": true, "data": { "member": null, "removed": true } } ``` **Default (deactivate)** — no `permanent` flag returns the deactivated member object: ```json { "success": true, "data": { "member": { "id": 123, "member_id": "vmem_1a2b3c4d5e6f7a8b9c0d1e2f", "first_name": "Ada", "last_name": "Lovelace", "email": "ada@acme.com", "role": "member", "status": "deactivated", "valyd_id": "usr_9f8e7d6c5b4a", "active": false, "created_at": "2026-08-21T10:00:00+00:00" } } } ``` **Already gone** — removing a member that no longer exists is idempotent: ```json { "success": true, "data": { "member": null, "already_removed": true } } ``` The path takes the `vmem_…` `member_id` (the member's email or `valyd_id` is also accepted). ## Reactivate Restores a deactivated member to **`active`** if their Valyd identity still exists, or back to **`invited`** (they must re-activate by face) if it doesn't. ```ts const { member } = await client.reactivateMember("vmem_1a2b3c4d5e6f7a8b9c0d1e2f"); ``` ```http PATCH /api/sdk/members/vmem_1a2b3c4d5e6f7a8b9c0d1e2f/reactivate HTTP/1.1 Host: dev.valyd.vip X-Client-Id: X-Client-Secret: ``` ```json { "success": true, "data": { "member": { "id": 123, "member_id": "vmem_1a2b3c4d5e6f7a8b9c0d1e2f", "first_name": "Ada", "last_name": "Lovelace", "email": "ada@acme.com", "role": "member", "status": "active", "valyd_id": "usr_9f8e7d6c5b4a", "active": true, "created_at": "2026-08-21T10:00:00+00:00" } } } ``` The path takes the `vmem_…` `member_id` (the member's email or `valyd_id` is also accepted). **Errors** — `404 not_found`. ## Re-send invite Re-issues the member's **face-activation** link — it supersedes the old one and is returned in the response. By default the link is also **emailed**; send **`{ "notify": false }`** in the body to get it back **without** an email, so you can deliver it yourself (e.g. an in-app "Connect with Valyd" button that opens the link directly). Throttled to **10 requests / minute**. ```ts const { member, activation_link } = await client.resendMemberInvite("vmem_1a2b3c4d5e6f7a8b9c0d1e2f"); ``` ```http POST /api/sdk/members/vmem_1a2b3c4d5e6f7a8b9c0d1e2f/invite HTTP/1.1 Host: dev.valyd.vip X-Client-Id: X-Client-Secret: ``` ```json { "success": true, "data": { "member": { "id": 123, "member_id": "vmem_1a2b3c4d5e6f7a8b9c0d1e2f", "first_name": "Ada", "last_name": "Lovelace", "email": "ada@acme.com", "role": "member", "status": "link_sent", "valyd_id": null, "active": false, "created_at": "2026-08-21T10:00:00+00:00" }, "activation_link": "https://idp.valyd.vip/m/activate/yyyyyyyyyyyy" } } ``` The path takes the `vmem_…` `member_id` (the member's email or `valyd_id` is also accepted). **Errors** — `404 not_found`; `409 already_active` (the member already connected a Valyd ID); `409 deactivated` (reactivate first). ## Billing & seats Read your subscription, seat count, next-charge estimate, wallet balance, and invoices. Full seat model on [Pricing & billing](/docs/organizations/billing). ```ts const billing = await client.getBilling(); ``` ```http GET /api/sdk/billing HTTP/1.1 Host: dev.valyd.vip X-Client-Id: X-Client-Secret: ``` ```json { "success": true, "data": { "subscription": { "plan": "workforce", "status": "active", "price_per_seat": 0.99, "in_trial": false, "trial_ends_at": "2026-08-14T00:00:00+00:00", "current_period_end": "2026-09-01T00:00:00+00:00" }, "seats": 12, "estimated_next_charge": 11.88, "currency": "USD", "balance": 4.20, "invoices": [ { "period": "2026-07", "seats": 10, "amount": 9.90, "created_at": "2026-08-01T00:00:00+00:00" } ] } } ``` `subscription` is `null` before a plan exists. `seats` is the count of **active** members; `estimated_next_charge` is `seats × price_per_seat`. ================================================================================ === FILE: https://docs.valyd.vip/docs/organizations/billing.md === ================================================================================ > Source: https://docs.valyd.vip/docs/organizations/billing # Pricing & billing An organization pays **per seat** for its workforce, on one wallet and one ledger. ## The seat model - **14-day free trial** — run verifications for your organization free for the first 14 days. - **$0.99 per active member / month** after the trial — that covers **unlimited verifications** for that member (KYC, license, liveness, location, face match; verify them as often as you need). - **You're billed only for `active` members.** A member becomes active the moment they **scan their face** (activate the invite) — that's when the seat turns on and billing starts for them. An invited person who hasn't scanned their face yet (`invited` / `link_sent`) is **free** and doesn't count. A `deactivated` member is free too. See the [member lifecycle](/docs/organizations/members#member-lifecycle-status). - **Add or remove people anytime** — the bill follows your active roster. - Verification checks and login stay on the **same single wallet and ledger** — one bill for everything. ## Read your seats & billing `GET /api/sdk/billing` returns your subscription, active-seat count, next-charge estimate, wallet balance, and invoices. It's authenticated like every [Organization API](/docs/organizations/api) call — server-to-server with your `client_id` + `client_secret`. ```ts const billing = await client.getBilling(); // { subscription, seats, estimated_next_charge, currency, balance, invoices } ``` ```http GET /api/sdk/billing HTTP/1.1 Host: dev.valyd.vip X-Client-Id: X-Client-Secret: ``` ```json { "success": true, "data": { "subscription": { "plan": "workforce", "status": "active", "price_per_seat": 0.99, "in_trial": false, "trial_ends_at": "2026-08-14T00:00:00+00:00", "current_period_end": "2026-09-01T00:00:00+00:00" }, "seats": 12, "estimated_next_charge": 11.88, "currency": "USD", "balance": 4.20, "invoices": [ { "period": "2026-07", "seats": 10, "amount": 9.90, "created_at": "2026-08-01T00:00:00+00:00" } ] } } ``` Field notes: - **`subscription`** — the plan (`plan`, `status`, `price_per_seat`, `in_trial`, `trial_ends_at`, `current_period_end`), or `null` before a plan exists. - **`seats`** — the number of **active** members (the billable count). - **`estimated_next_charge`** — `seats × price_per_seat`. - **`currency`** — `"USD"`. - **`balance`** — the wallet balance. - **`invoices`** — past charges, each `{ period, seats, amount, created_at }`. ================================================================================ === FILE: https://docs.valyd.vip/docs/organizations.md === ================================================================================ > Source: https://docs.valyd.vip/docs/organizations --- product: valyd-id api_version: oidc auth: client-credentials billable: true pii_mode: proofs human_setup_required: true source_of_truth: openapi --- # Organizations & teams An **organization** is a shared Valyd workspace for a company: one team, one set of apps, one workforce roster, one bill. Solo developers don't need one — every account works standalone. Create an organization when more than one person manages your apps, when apps should outlive any single person's account, or when you onboard a workforce whose members sign in by face. This section is split into five pages: | Page | What's on it | | --- | --- | | **Overview** (this page) | How organizations work, why use one, what you get, how to start | | [Roles & access](/docs/organizations/roles) | The `owner` / `admin` / `developer` / `member` roles and who can do what | | [Members & onboarding](/docs/organizations/members) | Adding members, invite methods, the lifecycle/status values, reactivation | | [Organization API](/docs/organizations/api) | The server-to-server member API — every endpoint with real request + response | | [Pricing & billing](/docs/organizations/billing) | The per-seat model and the billing endpoint | ## How organizations work Your company already runs on its own people — employees, staff, contractors — each with **your** roles and permissions in **your** system. Valyd doesn't replace any of that. It adds a **verified face-identity and verification layer** on top, so the same people sign into your apps by face and carry reusable proofs of who they are. ```mermaid flowchart LR I["Invite member
(gets a vmem_… id)"] --> F["Face activation
(scan face once)"] F --> S["Signs in with Connect with Valyd
(face, no passwords)"] S --> T["Tracked by valyd_org_member_id"] T --> V["Layer verification on top
(KYC · license · liveness)"] ``` Here's the flow — it's how real integrations work (e.g. how Cisive onboarded their workforce): 1. **Create an organization** and add your people as **members** — one at a time, by CSV upload, or over the [members API/SDK](/docs/organizations/api). No passwords to issue. Each member you add gets a stable **member id `vmem_…`** (the org member key) — store it against your own employee/user record; it's your correlation key between your roster and Valyd. 2. **Each member gets an email invite** — a face-activation link. They tap it and **scan their face once**, which binds the membership to a real Valyd identity. 3. **From then on they sign in by face.** Your apps use **Connect with Valyd**, so the very face the member activated with is how they authenticate into your tools — no passwords, on Valyd's IdP. When that member signs in, the **`valyd_org_member_id`** claim comes back on the OIDC [userinfo response](/docs/endpoints#get-userinfo--get-user-profile) and in the ID token (scoped to your org's client) — its value is that member's `vmem_…` id, so you always know exactly which of your people just logged in, and can track logins against your own records. 4. **Layer verification on top.** For any member you can run **Valyd verification** — KYC, professional license, liveness, location — tied to their identity and reusable across your apps, so you don't re-collect it every time. **Map your existing roles onto members.** Add **all** your people — employees, staff, contractors — as **members** (they log in by face). On top of that, promote specific people to **admin** or **developer**, who manage things in the Valyd **Developer Portal** — the organization, apps, workflows, and billing — while plain members only face-login to your apps and get read-only visibility into the organization (its members list, not its projects). Keep your own business roles and permissions in **your** system; the `valyd_org_member_id` is the **join key** between your roles and Valyd's identity/login layer. Valyd's `member` role means only "can face-login to these apps." And with **private apps** you scope an app to assigned members only (enforced right at the login gate), so **only your workforce can sign in** — while a **public** app lets anyone log in. See [Roles & access](/docs/organizations/roles) for exactly what each role can do. ## Why use an organization - **A verified workforce** — every active member is a real, face-activated Valyd identity, not just an email on a list. - **Face login, no passwords** — members sign in by scanning their face; nothing to reset or leak. - **Reuse identity across your apps** — proofs a member earns (KYC, license, liveness) carry over; verify once, read everywhere. - **Track exactly who logs in** — the `valyd_org_member_id` claim (that member's `vmem_…` id) comes back on every login, so you always know which of your people authenticated. - **One bill** — a 14-day verification trial, then a per-active-member monthly plan for unlimited verifications; usage and seats post to a single wallet ([pricing & billing](/docs/organizations/billing) has the current price). - **Private apps scoped to your people** — a private app only admits assigned members, enforced at the login gate. - **Onboard & offboard over the API** — add, invite, deactivate, and remove members server-to-server as your roster changes ([Organization API](/docs/organizations/api)). ## What an organization gives you ![An organization in the Developer Portal: seats, members, team, and shared apps](/images/screenshots/portal-organization.png) - **Teams & roles** — invite teammates and give each a role; clear separation between who builds and who administers. See [Roles & access](/docs/organizations/roles). - **Shared apps** — apps belong to the organization, not one person. Any developer on the team can manage them; ownership does not leave when a person does. - **Workforce by face** — add members by CSV, one at a time, or via the API. Each gets a link and joins by scanning their face — no passwords. Only **active** (face-activated) members are billable. - **Public & private apps** — a **public** app lets anyone log in (the default). A **private** app is scoped to specific members: only assigned members can sign in, enforced at the login gate. - **One billing account** — pay-as-you-go usage and per-seat subscriptions post to a single wallet and ledger. Multiple products, billed from one place. ## How to integrate 1. **Add the button** — wire [Connect with Valyd](/docs) into your app so members authenticate by face with the identity they activated. 2. **Add your people** — push your roster with the [members API](/docs/organizations/api) (or CSV / one at a time in the portal); each member gets a face-activation invite. 3. **Scope who gets in** — mark the app [private](#what-an-organization-gives-you) and assign members so only your workforce can sign in. ## How to start 1. Sign in to the developer portal (https://dev.valyd.vip) and open the **Organizations** tab. 2. Create an organization from the selector — you become its owner. 3. Invite teammates (developer or admin) and create apps under the organization. 4. Add members (your workforce) by CSV, singly, or with `addMembers()`; they join by scanning their face. 5. Mark apps public or private, and assign members to the private ones. ## Notes for integrators - Organizations do not change the login/verification API surface. Your app still uses the same OAuth `client_id` / `client_secret` for Connect with Valyd and the same App API key for the Verification APIs — an organization governs **who owns the app**, **who may log into it**, and gives you the **[Members API](/docs/organizations/api)** for workforce onboarding. - For a **private** org app, a user who is not an assigned member is refused at the OAuth authorize step. Public apps behave exactly as before. ================================================================================ === FILE: https://docs.valyd.vip/docs/organizations/members.md === ================================================================================ > Source: https://docs.valyd.vip/docs/organizations/members # Members & onboarding A **member** is one person on your workforce roster. You add members three ways — one at a time in the portal, by CSV upload, or over the [Organization API](/docs/organizations/api#add-members) (`addMembers()`). Each member you add gets a stable **member id `vmem_…`** — store it against your own employee/user record; it's your correlation key between your roster and Valyd. ## How a member is invited When you add someone, Valyd issues an invite. The **method** depends on the role, and you can force it with the `invite` option on [add member(s)](/docs/organizations/api#add-members): | `invite` | What happens | | --- | --- | | `auto` *(default)* | Valyd picks the right link for the role. A **workforce member** (`role: member`) gets a **face-activation link** — they scan their face once to bind the membership to a real Valyd identity. An **admin or developer** gets an **email claim link** to join the console. | | `email` | Force the **email** path — an email claim/activation link is sent. | | `face` | Force the **face-activation** path — a face link is issued for the person to scan. | By default (`notify: true`) Valyd emails the link for you. Pass **`notify: false`** and each created member comes back with its own `activation_link` in the API response so you can deliver it yourself (your own email / SMS / in-app message). Note: `activation_link` is returned **only for face invites** — email/claim links are emailed, never handed back. ## How a new member is notified Adding a member (portal, CSV, or `addMembers()`) emails them their invite link — for the workforce, a **face-activation link**. They tap it, scan their face once, and their membership is bound to a real Valyd identity. Prefer to deliver it yourself? Pass `notify: false` and read each created member's `activation_link`. Re-send anytime with [`resendMemberInvite(memberId)`](/docs/organizations/api#re-send-invite). To know when someone finished, **poll** [`getMembers()`](/docs/organizations/api#list-members) and watch for `status: "active"`. ## Member lifecycle (status) A membership moves through exactly four states: | `status` | Meaning | Billable? | | --- | --- | --- | | `invited` | Created, no email sent yet (the `notify: false` path). | No | | `link_sent` | Activation email sent, awaiting the person. | No | | `active` | Face-activated and bound to a Valyd identity. | **Yes** — the only billable state | | `deactivated` | Removed from the workforce; app logins revoked, row kept. | No | Only the `active` state is billable — a seat turns on the moment the member **scans their face**. See [Pricing & billing](/docs/organizations/billing). ## One face = one member A person's **face is their identity**, so the same face can hold **at most one active membership** in your org. If someone is invited under a **second email** and scans the **same face** that is already an active member, Valyd does **not** create a duplicate seat: the activation is refused with `already_member` ("you're already a member — sign in with your existing account"), and that second invite row is left **`deactivated`** with **no `valyd_id`**. A face therefore can never become two billed seats, and nobody is double-counted. The person keeps working through their existing membership. If a second email genuinely needs its own access under a **different** face, an admin reactivates the row and re-sends the invite. ## Deactivate, remove, reactivate - **Deactivate** — [`deactivateMember(memberId)`](/docs/organizations/api#deactivate) stops billing and revokes the member's app logins, but keeps the row. Their Valyd identity is **not** deleted. - **Remove** — [`removeMember(memberId)`](/docs/organizations/api#remove) is the same deactivation by default; pass `{ permanent: true }` to delete the membership row outright, so the email can be re-invited cleanly. The person's Valyd account is never deleted. - **Reactivate** — [`reactivateMember(memberId)`](/docs/organizations/api#reactivate) restores a deactivated member to `active` if their Valyd identity still exists, or back to `invited` (they must re-activate by face) if it doesn't. - **Re-send an invite** — if a member's invite expired before they connected their Valyd ID, [`resendMemberInvite(memberId)`](/docs/organizations/api#re-send-invite) issues a fresh face-activation link, superseding the old one. By default it **emails** the link and also returns it; pass **`notify: false`** to get the link back **without** emailing, so you can deliver it yourself (e.g. an in-app "Connect with Valyd" button that opens the link directly). It refuses for already-active or deactivated members. None of these ever touch the person's Valyd identity or their membership in any other organization. Only `member`-role people appear on the roster — use [`resolveMember({ valydId })`](/docs/organizations/api#resolve-one-person) to check whether someone is a workforce member vs a developer/admin, or not in your org at all. In the Developer Portal, the org owner/admin sees the full roster with each member's status on the **Organization → Members** tab, and can re-send invites, deactivate/reactivate, or **Remove** a member outright (permanent, same as the API's `permanent: true`). ## Account recovery If a member is locked out of **your app** — they forgot the email/password they sign in with — you don't delete the seat or its history. You start an **identity-backed recovery**: Valyd re-verifies the person (liveness + a face match against their enrolled face, plus a fresh document/KYC scan on the `with_id` variant) and returns a **pass/fail** to your Verify project's webhook. `startAccountRecovery` returns a hosted `recoveryUrl` for **you to deliver** (pass `deliverEmail:true` to also have Valyd email it to the member's on-file address). On a pass, **your** app lets them set a new password — Valyd stores and sets nothing. ```ts await client.startAccountRecovery({ valydId: "valyd_…", variant: "with_id" }); ``` See **[Account recovery](/docs/organizations/recovery)** for the full flow, the `startAccountRecovery` / `bindMember` functions, their responses, and the webhook contract. ================================================================================ === FILE: https://docs.valyd.vip/docs/organizations/onboarding.md === ================================================================================ > Source: https://docs.valyd.vip/docs/organizations/onboarding # Workforce onboarding Bringing a whole team onto Valyd? The workforce onboarding flow turns each person into a face-authenticated member of your organization — no passwords, and you always know exactly who signed in. This page is the flow at a glance; the full API and lifecycle live in [Members & onboarding](/docs/organizations/members). ## The flow ```mermaid flowchart LR A["Invite (portal, CSV, or addMembers)"] --> B["Email with a face-activation link"] --> C["Member scans their face once"] --> D["Membership bound to a real Valyd identity"] --> E["Signs into your apps by face"] ``` 1. **Invite the member.** Add someone one at a time in the portal, by CSV upload, or over the [Organization API](/docs/organizations/api#add-members) (`addMembers()`). Each member gets a stable **member id `vmem_…`** — store it against your own employee record as your correlation key. 2. **They receive a face-activation link.** For a workforce member (`role: member`), Valyd emails a **face-activation link** by default. Prefer to deliver it yourself? Pass `notify: false` and read each created member's `activation_link` from the API response (returned for face invites only). 3. **They activate by face.** The member taps the link and scans their face once. That single scan **binds the membership to a real Valyd identity** — the moment this happens, the seat becomes `active` (the only billable state). 4. **They sign in by face from then on.** Members log into your apps with **Connect with Valyd** (standard OIDC) using face authentication — no passwords to manage or reset. ## Member status during onboarding A membership moves through four states; watch for `active` to know onboarding finished: | `status` | Meaning | | --- | --- | | `invited` | Created, no email sent yet (the `notify: false` path). | | `link_sent` | Activation email sent, awaiting the person. | | `active` | Face-activated and bound to a Valyd identity — billable. | | `deactivated` | Removed from the workforce; app logins revoked, row kept. | To know when someone finished, **poll** [`getMembers()`](/docs/organizations/api#list-members) and watch for `status: "active"`, or re-send an expired invite with [`resendMemberInvite(memberId)`](/docs/organizations/api#re-send-invite). ## Related - [Members & onboarding](/docs/organizations/members) — the full member lifecycle, invite methods, deactivate/reactivate/remove, and recovery. - [Organization API](/docs/organizations/api) — `addMembers()`, `getMembers()`, and the rest. - [Roles](/docs/organizations/roles) — member vs admin vs developer, and what each invite path issues. ================================================================================ === FILE: https://docs.valyd.vip/docs/organizations/recovery.md === ================================================================================ > Source: https://docs.valyd.vip/docs/organizations/recovery --- product: valyd-id api_version: oidc auth: client-credentials billable: true pii_mode: proofs human_setup_required: true source_of_truth: manual --- # Account Recovery Let a locked-out member of your organization regain access by **re-verifying their identity with Valyd** — **without Valyd ever storing or resetting your passwords**. Valyd verifies the person (liveness + a face match against the face they enrolled, plus a fresh document/KYC check when you ask for it) and returns a **pass/fail** decision on your Verify webhook. On a pass, you permit the reset in **your own** system. This is the right tool when your app uses **email/password** (or any credential you own) and a user forgets it: instead of a knowledge-based reset, you get a **biometric identity proof** that the person asking is the same Valyd account. > This is **not** the Valyd end-user "recovery phrase" (E2E vault) flow. It is a server-to-server API > for organization apps that manage their own credentials, under the same `/api/sdk` surface as the > [Organization API](/docs/organizations/api). ## Prerequisites - The member must be a **claimed, active member** of your org with a **`valyd_id` and an enrolled face** on file. Members onboarded through [Workforce onboarding](/docs/organizations/onboarding) qualify; so does anyone you register with [`bindMember`](#bindmember) after they connect Valyd. Never-claimed or faceless members are **not** recoverable (fail-closed). - You have your app's `clientId` / `clientSecret` and a Verify **project with a webhook** configured (the recovery result is delivered to that webhook). ## Flow ``` 1. User can't sign in → your "Forgot password" (or an admin action) resolves their valyd_id. 2. Your server calls startAccountRecovery({ valydId }) → Valyd starts a session and returns a hosted recoveryUrl. YOU deliver it to the member (your email / SMS / in-app). Optionally pass deliverEmail:true to also have Valyd email it to the member's on-file address. 3. The member opens the link → completes liveness + a face match against their on-file Valyd face (and a fresh document/KYC scan when variant is "with_id"). 4. Valyd sends a signed webhook to your PROJECT webhook → verify.approved or verify.declined. 5. On approved, YOUR app lets the user set a new password. Valyd sets nothing. ``` ## Registering members who connect after sign-in If your users sign in with email/password first and **connect Valyd afterwards** (OIDC), register that binding once so they become recoverable. Call `bindMember` when the OIDC callback returns the member's `valyd_id`: > **You often don't need `bindMember`.** A member who connects through a **face-activation link** — a > Workforce invite, or [`resendMemberInvite`](/docs/organizations/api#re-send-invite) with > `notify: false` behind an in-app "Connect with Valyd" button — is bound **automatically** when they > scan their face, and is recoverable immediately. Use `bindMember` only when **you** obtained the > `valyd_id` yourself (e.g. an OIDC login on your own site) and need to record the binding. ### `bindMember` ```ts // After the member completes "Connect with Valyd" and you have their valyd_id: const member = await client.bindMember({ valydId: "valyd_…", // from the OIDC id_token / userinfo email: "jane@acme.com", // the email they sign in to YOUR app with firstName: "Jane", lastName: "Doe", }); // member.status === "active" → they are now a recoverable org member. ``` `POST /api/sdk/members/bind` — idempotent upsert. It marks the member **active** and bound to that `valyd_id`. Members added through Workforce onboarding are already bound and do not need this. ## Start a recovery ### `startAccountRecovery` ```ts import { ValydClient } from "@valyd/sdk"; // Server-side only — the same client you use for the Organization API. const client = new ValydClient({ clientId: process.env.VALYD_CLIENT_ID!, clientSecret: process.env.VALYD_CLIENT_SECRET!, }); const rec = await client.startAccountRecovery({ valydId: "valyd_…", // the member's Valyd id variant: "with_id", // "with_id" (default) or "without_id" — see below redirectUrl: "https://acme.com/reset", // where the user lands after verifying // deliverEmail: true, // OPTIONAL — also have Valyd email the link }); if (!rec.eligible) { // Fail-closed: unknown / inactive / unclaimed member. Show a GENERIC // "if an account exists, we've started recovery" message (avoid enumeration). return; } // Deliver the hosted link yourself (your email / SMS / in-app): await sendYourOwnEmail(memberEmail, rec.recoveryUrl); // …or pass deliverEmail:true above and Valyd emails the member's on-file address for you. // Then wait for the webhook (or poll the session) to learn the outcome. ``` `POST /api/sdk/recovery/session` takes the member's `valydId` and returns `{ eligible, recoveryUrl, emailed, sessionId, status, expiresAt }`. `recoveryUrl` is returned only to your **authenticated server** (client-credentials) — you own the user relationship, so you own delivery. Pass `deliverEmail:true` to also have Valyd email it to the member's on-file address; the app name in that email is your **organization's name**, derived from your client credentials. | Field | Meaning | |---|---| | `eligible` | `false` when no claimed, active, face-enrolled member matched — no session was started. | | `recoveryUrl` | The verification link — deliver it to the member yourself. Never expose it to an unauthenticated end user. | | `emailed` | `true` only when you passed `deliverEmail:true` and Valyd emailed the member the link. | | `sessionId` | The Verify session id — correlate it to the webhook. | | `status` | Initial session status (`NOT_STARTED`). | | `expiresAt` | When the session/link expires. | ### Verification depth — `variant` | Variant | Steps the member completes | |---|---| | `with_id` *(default)* | **Fresh document verification (KYC)** + **liveness** + **face match** against their on-file Valyd face. The government ID is re-scanned **every time** — prior KYC is **not** reused. | | `without_id` | **Liveness** + **face match** against their on-file Valyd face. **No ID** is collected — even for a member who was never KYC-verified. | Both variants match the live face against the **account's enrolled face**, never against a self-supplied document. Use `with_id` when you want a government-ID-backed recovery each time; use `without_id` for a lighter biometric-only proof that the person is the account holder. ## Handle the outcome The result is delivered to your **Verify project's configured webhook** (the same one you use for verifications) — verify the signature with your Verify client: ```ts import { VerifyClient } from "@valyd/sdk"; const verify = new VerifyClient({ apiKey: process.env.VERIFY_API_KEY!, webhookSecret: process.env.VERIFY_WEBHOOK_SECRET!, }); const event = verify.webhooks.constructEvent(rawBody, req.headers); // Correlate by the sessionId you got from startAccountRecovery. if (event.status === "APPROVED") { // Identity re-verified → permit the user to set a new password in YOUR system. } else { // Declined → do not allow the reset. } ``` To make the flow robust to webhook timing, you can also **poll** the session decision on the page the user returns to (`redirectUrl`) and treat `APPROVED` as the go-ahead — the webhook and the poll agree on the same decision. ## Security - **Fail closed** everywhere: no claimed/face-enrolled match → no session; no face match → `DECLINED`. - Valyd stores/sets **no** passwords — it only returns the pass/fail decision. - `recoveryUrl` is only ever returned to your **authenticated server** (client-credentials) — never expose it to an unauthenticated end user. When you pass `deliverEmail:true`, Valyd emails the link only to the address already **on file**, never to a caller-supplied one. - The start endpoint is **rate-limited**. Return a **generic** response whether or not an account exists, so this can't be used to probe which members are registered. - `bindMember` and `startAccountRecovery` are **server-to-server** — the client secret must never reach a browser. ================================================================================ === FILE: https://docs.valyd.vip/docs/organizations/roles.md === ================================================================================ > Source: https://docs.valyd.vip/docs/organizations/roles # Roles & access An organization has exactly four roles. **Owner** and **admin** run the organization; a **developer** builds apps; a **member** is the workforce — read-only in the portal (they see the organization and its members list, but not projects) and signs into your apps by face. | Role | Developer Portal | Face required | What they can do | | --- | --- | --- | --- | | `owner` | Yes | No | Everything — the organization, its team, members, apps and billing. Assigns roles, adds and removes people, and mutates anything. The person who created the organization. | | `admin` | Yes | No | The same management powers as the owner: manage the organization, team, members, apps and billing, assign roles, and add people. | | `developer` | Yes | No | Signs into the Developer Portal, sees the organizations they belong to, and creates & manages apps. **No member or billing administration.** No face activation — a developer is console staff, not workforce. | | `member` | Read-only | Yes | The **workforce**. Read-only in the portal — a member can see the organization and its members list, but **not** its projects, keys or settings; they exist to sign into the apps assigned to them, **by face**. Face-keyed: the membership binds to a real Valyd identity when they scan their face, and that identity is how they log in. | **Owner and admin manage and mutate.** Only they can add people, assign or change roles, deactivate/reactivate members, and touch billing. Developers and members cannot change anyone's role. **Developers get console access, no face.** A developer signs into the portal to build and manage apps. They are not part of the face-login workforce and never need to activate a face. **Members are the face-keyed workforce.** Every active member is a real, face-activated Valyd identity. When a member signs into one of your apps, the `valyd_org_member_id` claim (their `vmem_…` id) comes back so you always know which of your people logged in. ## Assigning & changing roles An owner or admin does it in the Developer Portal: - **Organization → Team** — for staff roles (`admin`, `developer`). - **Organization → Members** — for the workforce (`member`). Every mutation is admin-gated; developers and members can't change anyone's role. Over the API, the role is set when you [add a member](/docs/organizations/api#add-members) (`role` defaults to `member`). ================================================================================ === FILE: https://docs.valyd.vip/docs/quickstart/curl.mdx.md === ================================================================================ > Source: https://docs.valyd.vip/docs/quickstart/curl.mdx --- title: cURL quickstart — Connect with Valyd description: 'Walk the whole OIDC login flow by hand with cURL and a browser: authorize URL anatomy, authorization code, form-encoded token exchange, userinfo.' --- import { Callout } from 'nextra/components' # cURL quickstart > 🔑 **Auth:** `client_id` + `client_secret` (server-side) · 👤 **This IS the login** — standard OpenID Connect · 💻 Raw HTTP, no code · ⏱ ~5 minutes **What you'll build:** a complete login walked by hand — browser for the consent screen, cURL for everything else — so you can see exactly what your server will do. Your real values are pre-filled on your app's **Quick setup** tab in the [Developer Portal](https://dev.valyd.vip). Discovery lives at `https://idp.valyd.vip/api/.well-known/openid-configuration` — everything below is listed there. ## 1. Create the app In the [Developer Portal](https://dev.valyd.vip) create an application, enable the `profile` [scope](/docs/scopes), and register a redirect URI. For this walkthrough any URL you can read from the address bar works, e.g.: ```text http://localhost:8080/callback ``` Copy the `client_id` and the one-time `client_secret`. ## 2. Build the authorize URL and open it Generate a random `state` (and `nonce`), then open this URL in a browser: ```bash STATE=$(openssl rand -hex 16) # store it — you compare it on the callback NONCE=$(openssl rand -hex 16) echo "https://idp.valyd.vip/api/auth/oidc/authorize?client_id=YOUR_CLIENT_ID&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fcallback&response_type=code&scope=openid%20profile&state=$STATE&nonce=$NONCE" ``` Anatomy: `response_type=code` always; `scope` is space-separated (URL-encoded) and **must include `openid`**; `state` is required and echoed back unchanged (your CSRF check); `nonce` is recommended and bound into the `id_token`. ## 3. Grab the code from the callback After you approve the consent screen, the browser lands on your redirect URI: ```text http://localhost:8080/callback?code=AUTH_CODE_HERE&state=... ``` Check that `state` in the URL equals the `$STATE` you generated — then copy the `code`. It is single-use and expires in seconds, so exchange it immediately. ## 4. Exchange the code for tokens Form-encoded POST to the token endpoint (this is the step that must always run server-side — it carries your `client_secret`): ```bash curl -X POST https://idp.valyd.vip/api/auth/oidc/token \ -d grant_type=authorization_code \ -d code=AUTH_CODE_HERE \ -d client_id=YOUR_CLIENT_ID \ -d client_secret=YOUR_CLIENT_SECRET \ -d redirect_uri=http://localhost:8080/callback ``` The response is **top-level** token JSON — no `data` wrapper: ```json { "access_token": "eyJhbGciOi...", "refresh_token": "rfrsh_abc123...", "id_token": "eyJhbGciOiJSUzI1NiIs...", "token_type": "Bearer", "expires_in": 900, "scope": "openid profile" } ``` The `id_token` is an RS256 JWT — in production verify it against the JWKS at `https://idp.valyd.vip/api/auth/oidc/jwks.json` and check its `nonce` claim equals your `$NONCE`. ## 5. Read the user ```bash curl -H "Authorization: Bearer ACCESS_TOKEN_HERE" \ https://idp.valyd.vip/api/auth/oidc/userinfo ``` Returns the profile with `valyd_id` (the stable pseudonymous ID — use it as your user key) and `id_verified`. The same Bearer token also reads `/api/auth/oidc/licenses` and `/api/auth/oidc/verifications`, gated by the scopes the user approved. **Checkpoint:** the callback's `state` matched yours; the token exchange returned HTTP 200 with top-level `access_token`, `refresh_token`, and `id_token`; and the userinfo call returned `valyd_id` and `id_verified`. Replaying step 4 with the same code fails with `invalid_grant` — codes are single-use. Access tokens last ~15 minutes. Renew with `grant_type=refresh_token` at the same endpoint — rotation is on, so always persist the new `refresh_token` you get back. Details in [Authentication](/docs/authentication). ## Troubleshooting - **`redirect_uri` mismatch** — the URI at `/authorize` and `/token` must be identical and exactly match a registered redirect URI. See [Errors & troubleshooting](/docs/errors). - **`invalid_grant`** — the code expired or was already exchanged; go back to step 2 for a fresh one. See [Errors & troubleshooting](/docs/errors). - **`invalid_scope` / missing `openid`** — `scope` must include `openid`, and every requested scope must be enabled on the app in the portal. See [Errors & troubleshooting](/docs/errors). ## Next steps - [Scopes](/docs/scopes) — request `verifications`, `email`, or license scopes. - [Account API](/docs/endpoints) — every endpoint the Bearer token can read. - [Attach a verification](/verifications/quickstart) — run a new KYC or license check with the user's token so the proof saves to their Valyd account. - Ready to code it? The official SDK (`npm install @valyd/sdk`) does steps 2–5, plus PKCE and id_token verification, in two calls — see the [Node.js quickstart](/docs/quickstart/node). ================================================================================ === FILE: https://docs.valyd.vip/docs/quickstart/nextjs.mdx.md === ================================================================================ > Source: https://docs.valyd.vip/docs/quickstart/nextjs.mdx --- title: Next.js (App Router) quickstart — Connect with Valyd description: 'Add Connect with Valyd to a Next.js App Router app: two route handlers with @valyd/sdk server-side and the OIDC transaction in an httpOnly cookie.' --- import { Callout } from 'nextra/components' # Next.js (App Router) quickstart > 🔑 **Auth:** `client_id` + `client_secret` (server-side) · 👤 **This IS the login** — standard OpenID Connect · 🟩 Official SDK · ⏱ ~10 minutes **What you'll build:** two App Router route handlers — `/auth/login` and `/auth/callback` — that sign a user in with Valyd and hand your server their pseudonymous `valyd_id`. Your real values are pre-filled on your app's **Quick setup** tab in the [Developer Portal](https://dev.valyd.vip). ## 1. Create the app In the [Developer Portal](https://dev.valyd.vip) create an application, enable the `profile` [scope](/docs/scopes), and register this exact redirect URI: ```text http://localhost:3000/auth/callback ``` Copy the `client_id` and the one-time `client_secret`. ## 2. Install and configure ```bash npx create-next-app@latest valyd-login && cd valyd-login npm install @valyd/sdk ``` **`.env.local`** ```bash VALYD_CLIENT_ID=YOUR_CLIENT_ID VALYD_CLIENT_SECRET=YOUR_CLIENT_SECRET VALYD_REDIRECT_URI=http://localhost:3000/auth/callback VALYD_IDP_URL=https://idp.valyd.vip ``` ## 3. Share one Valyd client **`lib/valyd.ts`** ```typescript import { Valyd } from "@valyd/sdk"; export const valyd = new Valyd({ clientId: process.env.VALYD_CLIENT_ID!, clientSecret: process.env.VALYD_CLIENT_SECRET!, // server-side only — never NEXT_PUBLIC_ redirectUri: process.env.VALYD_REDIRECT_URI!, idpBaseUrl: process.env.VALYD_IDP_URL!, }); ``` ## 4. The login route Creates the OIDC transaction (`state` + `nonce` + S256 PKCE), stores it in an `httpOnly` cookie, and redirects to Valyd. **`app/auth/login/route.ts`** ```typescript import { NextResponse } from "next/server"; import { valyd } from "@/lib/valyd"; export async function GET() { const transaction = valyd.auth.createAuthorizationRequest({ scope: ["profile"] }); const response = NextResponse.redirect(transaction.url); response.cookies.set("valyd_txn", JSON.stringify(transaction), { httpOnly: true, sameSite: "lax", secure: process.env.NODE_ENV === "production", path: "/auth", maxAge: 600, // the login must finish within 10 minutes }); return response; } ``` A plain `httpOnly` cookie holding the transaction JSON is fine for a quickstart — the browser can't read it, and `handleCallback()` still enforces state, PKCE, and nonce. For production, encrypt or sign it (for example with [`iron-session`](https://github.com/vvo/iron-session)) or keep the transaction in a server-side session store, so the PKCE verifier never leaves your infrastructure even in transit. ## 5. The callback route Reads the transaction back, lets the SDK verify everything, and clears the cookie. **`app/auth/callback/route.ts`** ```typescript import { NextRequest, NextResponse } from "next/server"; import { valyd } from "@/lib/valyd"; export async function GET(request: NextRequest) { const raw = request.cookies.get("valyd_txn")?.value; if (!raw) { return new NextResponse("Login transaction missing or expired", { status: 400 }); } try { const transaction = JSON.parse(raw); const { user, tokens } = await valyd.auth.handleCallback(request.url, { transaction }); // user.valyd_id — stable pseudonymous ID; user.id_verified — identity proof. // Create your own app session here; keep Valyd tokens server-side. const response = NextResponse.redirect(new URL("/dashboard", request.url)); response.cookies.delete("valyd_txn"); // one transaction, one callback return response; } catch (error: any) { return NextResponse.json( { error: error.code ?? "login_failed", message: error.message }, { status: 400 }, ); } } ``` ## 6. Run it ```bash npm run dev ``` Point a link or button at `/auth/login` and complete the login. **Checkpoint:** visiting `/auth/login` returns a `307` redirect to `https://idp.valyd.vip/api/auth/oidc/authorize?...` containing `state`, `nonce`, and `code_challenge_method=S256`, with the `valyd_txn` cookie set; after consent, `/auth/callback` lands on `/dashboard` and the cookie is gone. Replaying the callback URL fails with "Login transaction missing or expired". ## Troubleshooting - **`redirect_uri` mismatch** — register `http://localhost:3000/auth/callback` exactly; a different port or path is rejected. See [Errors & troubleshooting](/docs/errors). - **"Login transaction missing or expired"** — the `valyd_txn` cookie's `path` must cover the callback route (`/auth` here), and login + callback must be on the same host and scheme. See [Errors & troubleshooting](/docs/errors). - **`invalid_grant`** — React strict-mode double-fetches or a prefetched callback URL can consume the single-use code; make sure nothing prefetches `/auth/callback`. See [Errors & troubleshooting](/docs/errors). ## Next steps - [Scopes](/docs/scopes) — request `verifications`, `email`, or license scopes. - [Account API](/docs/endpoints) — read userinfo, licenses, and verification proofs with the access token. - [Attach a verification](/verifications/quickstart) — run a new KYC or license check with the user's token so the proof saves to their Valyd account. ================================================================================ === FILE: https://docs.valyd.vip/docs/quickstart/node.mdx.md === ================================================================================ > Source: https://docs.valyd.vip/docs/quickstart/node.mdx --- title: Node.js (Express) quickstart — Connect with Valyd description: 'Add Connect with Valyd to an Express app in one file with @valyd/sdk: authorization request, server-side transaction, verified callback.' --- import { Callout } from 'nextra/components' # Node.js (Express) quickstart > 🔑 **Auth:** `client_id` + `client_secret` (server-side) · 👤 **This IS the login** — standard OpenID Connect · 🟩 Official SDK · ⏱ ~5 minutes **What you'll build:** a single-file Express app where "Connect with Valyd" returns the user's pseudonymous `valyd_id` and verification proofs to your backend. Your real values are pre-filled on your app's **Quick setup** tab in the [Developer Portal](https://dev.valyd.vip). Prefer to clone instead of typing? [`valyd-sandbox-starter`](https://github.com/valyd-id/valyd-sandbox-starter) on GitHub (or [download the zip](/downloads/valyd-sdk-starter.zip)) is this exact app — fill `.env`, `npm run dev`. It also has an optional **Test a verification workflow** button: set `VALYD_VERIFY_API_KEY` and `VALYD_WORKFLOW_ID` in `.env` to run a Verify workflow against the signed-in account after login. ## 1. Create the app In the [Developer Portal](https://dev.valyd.vip) create an application, enable the `profile` [scope](/docs/scopes), and register this exact redirect URI: ```text http://localhost:8080/callback ``` Copy the `client_id` and the one-time `client_secret`. Keep the secret on your server. ## 2. Install ```bash mkdir valyd-login && cd valyd-login npm init -y npm install @valyd/sdk express express-session dotenv ``` **`.env`** ```bash VALYD_CLIENT_ID=YOUR_CLIENT_ID VALYD_CLIENT_SECRET=YOUR_CLIENT_SECRET VALYD_REDIRECT_URI=http://localhost:8080/callback VALYD_IDP_URL=https://idp.valyd.vip SESSION_SECRET=replace_with_at_least_32_random_characters ``` ## 3. Write the server **`server.mjs`** ```javascript import "dotenv/config"; import express from "express"; import session from "express-session"; import { Valyd } from "@valyd/sdk"; const app = express(); app.use(session({ secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: false, cookie: { httpOnly: true, sameSite: "lax", secure: false, maxAge: 10 * 60 * 1000 }, })); const valyd = new Valyd({ clientId: process.env.VALYD_CLIENT_ID, clientSecret: process.env.VALYD_CLIENT_SECRET, redirectUri: process.env.VALYD_REDIRECT_URI, idpBaseUrl: process.env.VALYD_IDP_URL, }); app.get("/", (_req, res) => res.type("html").send('Connect with Valyd')); app.get("/login", (req, res, next) => { try { const transaction = valyd.auth.createAuthorizationRequest({ scope: ["profile"] }); req.session.valydOidc = transaction; // server-side: state, nonce, and PKCE verifier req.session.save((error) => error ? next(error) : res.redirect(transaction.url)); } catch (error) { next(error); } }); app.get("/callback", async (req, res, next) => { try { const transaction = req.session.valydOidc; if (!transaction) return res.status(400).send("Login transaction missing or expired"); delete req.session.valydOidc; await new Promise((resolve, reject) => req.session.save((error) => error ? reject(error) : resolve())); const callbackUrl = new URL(req.originalUrl, process.env.VALYD_REDIRECT_URI).toString(); const { user, tokens } = await valyd.auth.handleCallback(callbackUrl, { transaction }); // Create your own app session here. Do not send Valyd tokens to browser storage. res.json({ valydId: user.valyd_id, idVerified: user.id_verified, scopes: tokens.scope }); } catch (error) { next(error); } }); app.use((error, _req, res, _next) => { console.error(error.code ?? error.name, error.message); res.status(400).json({ error: error.code ?? "login_failed", message: error.message }); }); app.listen(8080, () => console.log("Open http://localhost:8080")); ``` ## 4. Run it ```bash node server.mjs ``` Open http://localhost:8080 and click **Connect with Valyd**. A successful callback returns the user's pseudonymous `valydId`, verification proof, and granted scopes. **Checkpoint:** `GET /login` returns a `302` to `/api/auth/oidc/authorize` whose URL contains `state`, `nonce`, `code_challenge`, and `code_challenge_method=S256`; the callback succeeds only with the stored server-side transaction; `handleCallback()` verifies state, PKCE, RS256/JWKS, issuer, audience, expiry, and nonce; and the `client_secret`, PKCE verifier, and tokens never enter browser JavaScript or local storage. For production, use a shared session store, set the cookie to `secure: true`, configure trusted proxy handling correctly, and use your HTTPS callback URI. ## Troubleshooting - **`redirect_uri` mismatch** — the URI must match a registered redirect URI exactly (scheme, host, path). See [Errors & troubleshooting](/docs/errors). - **"Login transaction missing or expired"** — the session cookie didn't survive the round-trip; keep `sameSite: "lax"` and start login and callback on the same host/port. See [Errors & troubleshooting](/docs/errors). - **`invalid_grant` on the exchange** — the code is single-use and short-lived; a double-fired callback or a slow retry consumes it. Restart from `/login`. See [Errors & troubleshooting](/docs/errors). ## Next steps - [Scopes](/docs/scopes) — request `verifications`, `email`, or license scopes. - [Account API](/docs/endpoints) — read userinfo, licenses, and verification proofs with the access token. - [Attach a verification](/verifications/quickstart) — run a new KYC or license check with the user's token so the proof saves to their Valyd account. ================================================================================ === FILE: https://docs.valyd.vip/docs/quickstart/php.mdx.md === ================================================================================ > Source: https://docs.valyd.vip/docs/quickstart/php.mdx --- title: PHP (Laravel) quickstart — Connect with Valyd description: 'Add Connect with Valyd to a Laravel app with plain OIDC: session state, hash_equals CSRF check, form-encoded token exchange with Http::asForm, userinfo.' --- import { Callout } from 'nextra/components' # PHP (Laravel) quickstart > 🔑 **Auth:** `client_id` + `client_secret` (server-side) · 👤 **This IS the login** — standard OpenID Connect · 🐘 Raw API recipe · ⏱ ~10 minutes **What you'll build:** two Laravel routes where "Connect with Valyd" returns the user's pseudonymous `valyd_id` to your backend — no SDK, just Laravel's `Http` client. Your real values are pre-filled on your app's **Quick setup** tab in the [Developer Portal](https://dev.valyd.vip). ## 1. Create the app In the [Developer Portal](https://dev.valyd.vip) create an application, enable the `profile` [scope](/docs/scopes), and register this exact redirect URI: ```text http://localhost:8000/auth/valyd/callback ``` Copy the `client_id` and the one-time `client_secret`. ## 2. Configure **`.env`** ```bash VALYD_CLIENT_ID=YOUR_CLIENT_ID VALYD_CLIENT_SECRET=YOUR_CLIENT_SECRET VALYD_REDIRECT_URI=http://localhost:8000/auth/valyd/callback ``` **`config/services.php`** ```php 'valyd' => [ 'client_id' => env('VALYD_CLIENT_ID'), 'client_secret' => env('VALYD_CLIENT_SECRET'), 'redirect_uri' => env('VALYD_REDIRECT_URI'), ], ``` ## 3. Add the two routes **`routes/web.php`** ```php use Illuminate\Http\Request; use Illuminate\Support\Facades\Http; use Illuminate\Support\Str; // GET /auth/valyd — the "Connect with Valyd" button links here Route::get('/auth/valyd', function () { $state = Str::random(32); // CSRF: Valyd echoes it back session(['valyd_state' => $state]); return redirect()->away( 'https://idp.valyd.vip/api/auth/oidc/authorize?' . http_build_query([ 'client_id' => config('services.valyd.client_id'), 'redirect_uri' => config('services.valyd.redirect_uri'), // must exactly match an app redirect URI 'response_type' => 'code', 'scope' => 'openid profile', // openid is REQUIRED 'state' => $state, ]) ); }); // GET /auth/valyd/callback — check state, exchange the code, sign the user in Route::get('/auth/valyd/callback', function (Request $request) { abort_unless( hash_equals(session()->pull('valyd_state', ''), (string) $request->query('state')), 403 // CSRF check — never skip it ); abort_if($request->query('error') || !$request->query('code'), 400); $token = Http::asForm()->post('https://idp.valyd.vip/api/auth/oidc/token', [ 'grant_type' => 'authorization_code', 'code' => $request->query('code'), 'client_id' => config('services.valyd.client_id'), 'client_secret' => config('services.valyd.client_secret'), 'redirect_uri' => config('services.valyd.redirect_uri'), // same URI used at authorize ])->throw()->json(); // TOP-LEVEL JSON: access_token, refresh_token, id_token (RS256), expires_in (~900) $user = Http::withToken($token['access_token']) ->get('https://idp.valyd.vip/api/auth/oidc/userinfo')->json(); // $user['valyd_id'] is the stable pseudonymous ID — // find-or-create your local user against it, then log them in. return response()->json([ 'valyd_id' => $user['valyd_id'], 'id_verified' => $user['id_verified'], ]); }); ``` ## 4. Run it ```bash php artisan serve ``` Open http://localhost:8000/auth/valyd and complete the login. **Checkpoint:** `/auth/valyd` redirects to `https://idp.valyd.vip/api/auth/oidc/authorize?...` with your `state` in the URL; after consent, the callback returns your `valyd_id` and `id_verified`; and a forged callback (wrong or missing `state`) is rejected with HTTP 403. For production, register your HTTPS callback URI, add a `nonce` and verify the `id_token`'s signature against `https://idp.valyd.vip/api/auth/oidc/jwks.json` (e.g. with `firebase/php-jwt`), or plug in any OIDC library — see [Use any OIDC library](/docs/oidc). ## Troubleshooting - **`redirect_uri` mismatch** — the value sent at `/authorize` and `/token` must both exactly match a registered redirect URI. See [Errors & troubleshooting](/docs/errors). - **403 on legitimate logins** — the session cookie was lost between the two routes; both must run on the same host and go through Laravel's `web` middleware (sessions enabled). See [Errors & troubleshooting](/docs/errors). - **`invalid_grant` from `->throw()`** — the code is single-use and short-lived; refreshing the callback URL replays a consumed code. Restart from `/auth/valyd`. See [Errors & troubleshooting](/docs/errors). ## Next steps - [Scopes](/docs/scopes) — request `verifications`, `email`, or license scopes. - [Account API](/docs/endpoints) — read userinfo, licenses (`/api/auth/oidc/licenses`), and verification proofs (`/api/auth/oidc/verifications`) with the access token. - [Attach a verification](/verifications/quickstart) — run a new KYC or license check with the user's token so the proof saves to their Valyd account. - On a Node stack too? The official SDK (`npm install @valyd/sdk`) does this whole flow, plus PKCE and id_token verification, in two calls — see the [Node.js quickstart](/docs/quickstart/node). ================================================================================ === FILE: https://docs.valyd.vip/docs/quickstart/python.mdx.md === ================================================================================ > Source: https://docs.valyd.vip/docs/quickstart/python.mdx --- title: Python (Flask) quickstart — Connect with Valyd description: 'Add Connect with Valyd to a Flask app with plain OIDC: session state and nonce, authorize redirect, form-encoded token exchange, userinfo.' --- import { Callout } from 'nextra/components' # Python (Flask) quickstart > 🔑 **Auth:** `client_id` + `client_secret` (server-side) · 👤 **This IS the login** — standard OpenID Connect · 🐍 Raw API recipe · ⏱ ~10 minutes **What you'll build:** a two-route Flask app where "Connect with Valyd" returns the user's pseudonymous `valyd_id` to your backend — no SDK, just `requests`. Your real values are pre-filled on your app's **Quick setup** tab in the [Developer Portal](https://dev.valyd.vip). ## 1. Create the app In the [Developer Portal](https://dev.valyd.vip) create an application, enable the `profile` [scope](/docs/scopes), and register this exact redirect URI: ```text http://localhost:5000/callback ``` Copy the `client_id` and the one-time `client_secret`. ## 2. Install ```bash mkdir valyd-login && cd valyd-login pip install flask requests ``` Export your credentials (replace with the real values from the portal): ```bash export VALYD_CLIENT_ID=YOUR_CLIENT_ID export VALYD_CLIENT_SECRET=YOUR_CLIENT_SECRET export VALYD_REDIRECT_URI=http://localhost:5000/callback export SESSION_SECRET=replace_with_at_least_32_random_characters ``` ## 3. Write the app **`app.py`** ```python import os import secrets from urllib.parse import urlencode import requests from flask import Flask, abort, redirect, request, session VALYD = "https://idp.valyd.vip" app = Flask(__name__) app.secret_key = os.environ["SESSION_SECRET"] @app.route("/") def home(): return 'Connect with Valyd' @app.route("/login") def login(): state = secrets.token_hex(16) # CSRF: Valyd echoes it back on the callback nonce = secrets.token_hex(16) # bound into the id_token (replay protection) session["valyd_state"] = state session["valyd_nonce"] = nonce return redirect(f"{VALYD}/api/auth/oidc/authorize?" + urlencode({ "client_id": os.environ["VALYD_CLIENT_ID"], "redirect_uri": os.environ["VALYD_REDIRECT_URI"], # must exactly match an app redirect URI "response_type": "code", "scope": "openid profile", # openid is REQUIRED "state": state, "nonce": nonce, })) @app.route("/callback") def callback(): if "error" in request.args or "code" not in request.args: abort(400) if not secrets.compare_digest( session.pop("valyd_state", ""), request.args.get("state", "") ): abort(400) # CSRF check — never skip it token = requests.post(f"{VALYD}/api/auth/oidc/token", data={ # form-encoded "grant_type": "authorization_code", "code": request.args["code"], "client_id": os.environ["VALYD_CLIENT_ID"], "client_secret": os.environ["VALYD_CLIENT_SECRET"], "redirect_uri": os.environ["VALYD_REDIRECT_URI"], # same URI used at authorize }) token.raise_for_status() tokens = token.json() # TOP-LEVEL: access_token, refresh_token, id_token, expires_in (~900) user = requests.get( f"{VALYD}/api/auth/oidc/userinfo", headers={"Authorization": f"Bearer {tokens['access_token']}"}, ).json() # user["valyd_id"] is the stable pseudonymous ID — # find-or-create your local user against it, then start your own session. return {"valyd_id": user["valyd_id"], "id_verified": user["id_verified"]} if __name__ == "__main__": app.run(port=5000) ``` The `id_token` is an RS256-signed JWT. This quickstart trusts it because it arrived over TLS directly from the token endpoint; for production, verify its signature against the JWKS at `https://idp.valyd.vip/api/auth/oidc/jwks.json` and check that its `nonce` claim equals `session["valyd_nonce"]` (PyJWT does both), or use an OIDC library — see [Use any OIDC library](/docs/oidc). ## 4. Run it ```bash python app.py ``` Open http://localhost:5000 and click **Connect with Valyd**. **Checkpoint:** `/login` redirects to `https://idp.valyd.vip/api/auth/oidc/authorize?...` with `state` and `nonce` in the URL; after consent, `/callback` returns your `valyd_id` and `id_verified`; and a forged callback (wrong or missing `state`) is rejected with HTTP 400. ## Troubleshooting - **`redirect_uri` mismatch** — the value sent at `/authorize` and `/token` must both exactly match a registered redirect URI. See [Errors & troubleshooting](/docs/errors). - **State check fails on legitimate logins** — the Flask session cookie was lost (different host/port between login and callback, or cookies blocked); start both routes on `localhost:5000`. See [Errors & troubleshooting](/docs/errors). - **`invalid_scope` / missing `openid`** — `scope` must include `openid`, and every requested scope must be enabled on the app in the portal. See [Errors & troubleshooting](/docs/errors). ## Next steps - [Scopes](/docs/scopes) — request `verifications`, `email`, or license scopes. - [Account API](/docs/endpoints) — read userinfo, licenses (`/api/auth/oidc/licenses`), and verification proofs (`/api/auth/oidc/verifications`) with the access token. - [Attach a verification](/verifications/quickstart) — run a new KYC or license check with the user's token so the proof saves to their Valyd account. - Moving to Node later? The official SDK (`npm install @valyd/sdk`) does this whole flow, plus PKCE and id_token verification, in two calls — see the [Node.js quickstart](/docs/quickstart/node). ================================================================================ === FILE: https://docs.valyd.vip/docs/quickstarts.mdx.md === ================================================================================ > Source: https://docs.valyd.vip/docs/quickstarts.mdx --- title: Quickstarts — Connect with Valyd description: 'Pick your stack and add Connect with Valyd in minutes: official SDK quickstarts for Node.js and Next.js, plus raw OIDC recipes for Python, PHP, and cURL.' --- import { Cards, Callout } from 'nextra/components' import { Hexagon, Layers, Terminal, FileCode2, Braces, Plug } from 'lucide-react' # Quickstarts > 🔑 **Auth:** `client_id` + `client_secret` (server-side) · 👤 **This IS the login** — standard OpenID Connect · ⏱ 5–10 minutes per path Pick your stack — every path ends in the same place: a working **Connect with Valyd** that hands your backend the user's pseudonymous `valyd_id` and verification proofs. Every quickstart needs the same two credentials: a `client_id` and a one-time `client_secret` from an app in the [Developer Portal](https://dev.valyd.vip). Your real values are pre-filled on your app's **Quick setup** tab — the [Connect with Valyd](/docs) page shows exactly where that screen lives. ## Official SDK The fastest route — `@valyd/sdk` generates `state`, `nonce`, and S256 PKCE for you and verifies everything on the callback in one call. } title="Node.js (Express)" href="/docs/quickstart/node" arrow> One file, two routes with the official SDK — the complete reference example. ~5 minutes. } title="Next.js (App Router)" href="/docs/quickstart/nextjs" arrow> Two route handlers with the SDK server-side and the transaction in a cookie. ~10 minutes. ## Raw API recipes No SDK — plain HTTP against the standard OIDC endpoints, in your framework's own idioms. } title="Python (Flask)" href="/docs/quickstart/python" arrow> Two Flask routes with `requests`: session state, form-encoded token exchange, userinfo. ~10 minutes. } title="PHP (Laravel)" href="/docs/quickstart/php" arrow> Two Laravel routes with the `Http` client and a `hash_equals` state check. ~10 minutes. } title="cURL (raw HTTP)" href="/docs/quickstart/curl" arrow> Walk the whole flow by hand — authorize URL, code, token exchange, userinfo. ~5 minutes. } title="Any OIDC library" href="/docs/oidc" arrow> Auth.js, Passport, Spring Security, or an SSO console — point it at our discovery URL. ~5 minutes. ================================================================================ === FILE: https://docs.valyd.vip/docs/rate-limits.md === ================================================================================ > Source: https://docs.valyd.vip/docs/rate-limits # Rate limits How Valyd throttles requests, what a `429` looks like, and how to back off. The Verification API limit below is confirmed from the published OpenAPI spec (`public/openapi/valyd-verify.json`); per-endpoint limits for the login / OIDC / Account APIs are not published in these docs — contact **support@valyd.id** if you need them for capacity planning. ## Verification API (`/api/v2/…`) All `/api/v2` routes are rate limited **per client IP** at **~150 requests / minute**. This covers both the Unique Human API endpoints and verification-session calls. A throttled request returns `429` with this body — and, importantly, **no `Retry-After` header**: ```json { "success": false, "data": [], "error": { "code": "rate_limited", "message": "..." } } ``` - The `429` response carries **no `Retry-After` and no `X-RateLimit-*` headers**. - **Successful** responses include `X-RateLimit-Limit` and `X-RateLimit-Remaining`, so you can watch your remaining budget and slow down before you hit the wall. | Limit | Value | | --- | --- | | Scope | Per client IP | | Rate | ~150 requests / minute | | `Retry-After` on 429 | Not sent | | Budget headers | `X-RateLimit-Limit` / `X-RateLimit-Remaining` on successful responses | | Concurrency cap | Not published — contact **support@valyd.id** | | Burst allowance | Not published — contact **support@valyd.id** | ## Login / OIDC / Account APIs (`/api/auth/…`) Per-endpoint request-rate limits for the authentication and Account APIs are not published in these docs. What the error catalog does confirm is that abuse-sensitive flows are throttled and return `429`: - `rate_limited` (429) — back off and retry after the window resets. - `too_many_attempts` (429) — too many failed face checks. | Endpoint group | Limit | | --- | --- | | OAuth authorize / token / refresh | Not published — contact **support@valyd.id** | | `userinfo` / Account API reads | Not published — contact **support@valyd.id** | | Overall burst / concurrency for `/api/auth` | Not published — contact **support@valyd.id** | ## Handling 429 — back off When you receive a `429`: 1. **Stop and wait** — do not retry immediately. 2. **Back off exponentially** — because the Verification API sends no `Retry-After`, use client-side exponential backoff with jitter (e.g. 1s, 2s, 4s, …) rather than a fixed delay. 3. **Watch your budget** — on the Verification API, read `X-RateLimit-Remaining` on successful responses and throttle yourself before you exhaust it. 4. **Use idempotency for retries** — Verify POST endpoints accept an optional `Idempotency-Key` header so a retry cannot double-run or double-charge (see the codes `idempotency_in_progress` and `idempotency_key_reused` in the [error catalog](/docs/errors#3-complete-code-catalog)). General guidance on `429` and the status categories lives in [Errors & troubleshooting](/docs/errors#2-what-the-http-status-means). ## See also - [Errors & troubleshooting](/docs/errors) - [Operations & SLA](/docs/operations-sla) - [Full OpenAPI spec](/docs/api-reference) ================================================================================ === FILE: https://docs.valyd.vip/docs/request-data.md === ================================================================================ > Source: https://docs.valyd.vip/docs/request-data # Request user data (consent) > **Status (current):** The **at-login** path (releasing attributes on the consent screen with > `attr_code`) is **temporarily disabled** — the consent screen is **login-only** right now, so the > user just signs in and no data is released there. Use the **after-login** path > (`requestAttributes` → the user approves in their Valyd app) for all data requests today. The > at-login sections below are retained for when it is re-enabled. ## Available attributes Pass any of these keys in `attributes`. They fall into three groups: **Proofs** — non-identifying, release on consent alone (no face / vault needed): | Key | Value | |---|---| | `id_verified` | boolean — the user has a completed KYC | | `is_16_plus` / `is_18_plus` / `is_21_plus` / `is_30_plus` / `is_65_plus` | boolean age bands (derived, no raw DOB) | | `preferred_username` | the user's pseudonymous username | **Raw identity** — real PII kept server-readable; released on a face-assured (or quick in-page face) session: | Key | Value | |---|---| | `legal_name` / `full_name` | full legal name | | `first_name`, `last_name` | given / family name | | `email`, `phone` | contact | | `country` | country | **Vault-only raw KYC** — sealed **on the user's device** from their encrypted identity vault; the server is blind to these. The user must have their identity **vault unlocked** on the device they consent on — if it isn't, the request is refused with an "unlock your vault" prompt (these fields are never silently dropped): | Key | Value | |---|---| | `dob` | date of birth (`YYYY-MM-DD`) | | `age` | age in years (derived from DOB on-device) | | `gender` | gender / sex from the ID | | `nationality` | nationality from the ID | | `document_number` | ID document number | > Prefer **proofs** over raw fields where they suffice — e.g. request `is_18_plus` instead of `dob`. Raw fields require face assurance; vault-only fields additionally require the user's device vault. ## At login: ask on the consent screen (recommended) Put `attributes` + your X25519 public key on the authorize URL. The consent screen renders each field as a checkbox the user can uncheck. On **Authorize**, the granted fields are sealed **on the user's device** to your key and returned with the login as `attr_code`, which you exchange for the values with `getConsentedAttributes`. Proofs (age bands, `id_verified`) release on consent alone; raw identity fields need a face-assured session or a quick **in-page** face check (never a separate device). ```javascript import { Valyd, ValydClient } from "@valyd/sdk"; const valyd = new Valyd({ clientId: process.env.VALYD_CLIENT_ID, clientSecret: process.env.VALYD_CLIENT_SECRET, // server-side only redirectUri: "https://your-app.com/callback", }); // 1. Keypair — keep secretKey and the OIDC transaction SERVER-SIDE. const { publicKey, secretKey } = await ValydClient.generateRequesterKeypair(); // 2. Send the user to the authorize URL WITH the data you want (checkboxes on consent). const transaction = valyd.auth.createAuthorizationRequest({ scope: ["openid", "profile"], attributes: ["legal_name", "dob", "is_18_plus"], requesterPublicKey: publicKey, purpose: "Confirm your identity", }); req.session.valydConsent = { transaction, secretKey }; // -> redirect the browser to transaction.url // 3. On your callback: exchange the code AND fetch the consented data with attrCode. const saved = req.session.valydConsent; delete req.session.valydConsent; const { user, attrCode } = await valyd.auth.handleCallback(callbackUrl, { transaction: saved.transaction, }); if (attrCode) { const result = await valyd.auth.getConsentedAttributes(attrCode, { secretKey: saved.secretKey }); result.attributes; // { legal_name, dob, is_18_plus } — only what the user kept checked } ``` You only receive what the user kept checked. If they unchecked a field you need, ask again after login with `requestAttributes` (below). This is opt-in — apps that only need login send none of these params and get pseudonyms as before. ### At login, without the SDK (REST) The consent screen calls this for you when the user clicks Authorize; you don't call it directly. Your app just (1) puts `attributes` + `requester_public_key` on the authorize URL, then (2) reads `attr_code` off the callback and fetches it: ```bash # Fetch the consented, sealed attributes (attr_code came back on your callback) curl "https://idp.valyd.vip/api/auth/attribute-request/$ATTR_CODE/result?client_id=$VALYD_CLIENT_ID" # -> { "data": { "status": "released", "custody": "self", "sealed_payload": "" } } # Open sealed_payload with your X25519 secret key (libsodium sealed box). ``` ## After login: request attributes (self-custody) Need data you didn't ask for at login? Generate a keypair, request the attributes with the user's `valyd_id`, then poll for the approved result — the user approves in their Valyd app. Passing your `secretKey` to `getAttributeResult` opens the sealed box locally, so the plaintext never leaves your server. ```javascript import { Valyd, ValydClient } from "@valyd/sdk"; const valyd = new Valyd({ clientId: process.env.VALYD_CLIENT_ID, clientSecret: process.env.VALYD_CLIENT_SECRET, // server-side only }); // 1. Generate an X25519 keypair. Keep secretKey SERVER-SIDE and persist it for this // request — a fresh keypair cannot open a payload sealed to an older key. const { publicKey, secretKey } = await ValydClient.generateRequesterKeypair(); // 2. Request the attributes. valydId comes from the logged-in user. // The user is prompted to approve in their Valyd app. const { requestId } = await valyd.auth.requestAttributes({ valydId, attributes: ["legal_name", "dob", "country"], requesterPublicKey: publicKey, purpose: "Confirm your legal name for payroll onboarding", // shown on the consent prompt }); // 3. Poll until approved. secretKey makes the SDK decrypt locally; Valyd stays blind. const result = await valyd.auth.getAttributeResult(requestId, { secretKey }); if (result.status === "approved" || result.status === "released") { result.attributes; // { legal_name: "Ada Lovelace", dob: "1990-01-01", country: "GB" } } ``` **Install the crypto dependency.** The self-custody methods (`generateRequesterKeypair`, `getAttributeResult` with a `secretKey`, `openSealedPayload`) use `libsodium-wrappers`, which the SDK lazy-loads and does not bundle. Run `npm i libsodium-wrappers` in your project — otherwise you get `No such module libsodium-wrappers` the first time you decrypt. Nothing else in the SDK needs it. ## Managed custody (no crypto on your side) Prefer not to install libsodium? Hand Valyd the secret key and it opens the box for you — `attributes` comes back as plaintext. The trade-off is real: **Valyd can read the released values**, so you give up end-to-end privacy. Fine for testing; prefer self-custody for real personal data. ```javascript const { requestId } = await valyd.auth.requestAttributes({ valydId, attributes: ["legal_name", "dob"], requesterPublicKey: publicKey, managedPrivateKey: secretKey, // Valyd decrypts }); const r = await valyd.auth.getAttributeResult(requestId); // no secretKey needed r.attributes; // plaintext, decrypted by Valyd ``` ## Without the SDK (REST) ```bash # 1. Create the request (send your base64 X25519 public key) curl -X POST https://idp.valyd.vip/api/auth/attribute-request \ -H "Authorization: Bearer $CLIENT_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "valyd_id": "valyd_...", "attributes": ["legal_name","dob","country"], "requester_public_key": "", "purpose": "Payroll onboarding" }' # -> { "data": { "request_id": "...", "status": "pending" } } # 2. The USER approves in their Valyd app. Then poll: curl https://idp.valyd.vip/api/auth/attribute-request//result \ -H "Authorization: Bearer $CLIENT_TOKEN" # -> { "data": { "status": "approved", "sealed_payload": "" } } # Open sealed_payload with your X25519 secret key (libsodium sealed box). ``` ## Things to know - **Consent is remembered per app.** Once a user approves a field for your app, they aren't re-prompted on later logins — you receive it inline automatically. The user can revoke an app (and its data) from *Connected sites* in their Valyd account, which asks again next login. - **At login**, the release is synchronous — the user consents on the screen and `attr_code` is on your callback. **After login** (`requestAttributes`) it is asynchronous — the user approves in their Valyd app; poll `getAttributeResult` (`pending → approved / released`, or `denied` / `expired`). - **The user chooses what to share.** You receive only the fields left checked; unchecked ones are omitted. Fields the user hasn't verified (no KYC) aren't offerable. - **Read it promptly.** The sealed payload is purged about 5 minutes after approval; the `attr_code` is one-time and short-lived. - **Needs a backend.** Keypair generation, the `secretKey`, and opening the sealed box are server-side only — a browser-only SPA can't do self-custody safely. - **Persist the keypair** for the life of the request. A new keypair cannot open a payload sealed to an older public key. - Keep `secretKey` and `clientSecret` server-side only — never in browser code. ## Related - Proofs already granted at login: `valyd.auth.getUserInfo(token)`, `getLicenses(token)`, `getVerifications(token)`. - Run a verification check for the signed-in user (KYC, liveness, face, license) on a [Reusable Verification](/verifications) session with their token: `valyd.verify.*` — returns a proof, not raw data. ================================================================================ === FILE: https://docs.valyd.vip/docs/sandbox.mdx.md === ================================================================================ > Source: https://docs.valyd.vip/docs/sandbox.mdx --- title: API Playground description: 'Run real OAuth + OIDC requests against the Valyd sandbox — pick a demo user, choose scopes, and walk the full flow from authorization code to userinfo without writing a line of code.' --- import { TryApisContent } from '@/components/sandbox/TryApisContent'
================================================================================ === FILE: https://docs.valyd.vip/docs/scopes.md === ================================================================================ > Source: https://docs.valyd.vip/docs/scopes # OAuth2 Scopes ## Scopes summary **What the user sees** — the consent screen lists exactly the scopes you request, in plain language: ![The Valyd consent screen showing the requested scopes](/images/screenshots/idp-consent-screen.png) | Scope | Required | Description | Grants access to | | --- | --- | --- | --- | | `profile` | Yes (Mandatory) | User profile: legal name, username, country, and verification status (no photo is shared) | `/userinfo` | | `email` | Optional | User's email address (a private relay address, unless your app is a trusted first party) | `/userinfo` | | `phone` | Optional | User's phone number (a private relay number, unless your app is a trusted first party) | `/userinfo` | | `verifications` | Optional | Identity verification status: human (liveness) check, ID/KYC verification, and linked professional licenses | `/verifications` | | `doctor_license` | Optional | Medical/nursing license details for verified healthcare practitioners | Doctor/nursing license endpoints | | `zkp` | Optional | Zero-Knowledge Proof age verification data | ZKP-related endpoints | | `mcp` | Optional | Access to Model Context Protocol (MCP) endpoints | MCP endpoints | ## Requesting scopes Pass the scopes to the SDK. It adds `openid` and generates state, nonce, and S256 PKCE. ```javascript const transaction = valyd.auth.createAuthorizationRequest({ scope: ["profile", "verifications", "zkp"], }); req.session.valydOidc = transaction; // keep server-side res.redirect(transaction.url); ``` Expected output: `transaction.url` is a standard OIDC authorization URL containing `openid`, the requested scopes, state, nonce, and an S256 PKCE challenge. ## Scope enforcement - Scopes are verified against your app's settings in the Developer Portal. - If you request a scope not enabled for your app, authorization will fail. - If your access token doesn't have a required scope, the endpoint returns `403 Forbidden`. Decision tree when authorization or a scoped request fails: ```text IF authorization fails immediately (before the consent screen): → the requested scope is not enabled for your app. Enable it in the Developer Portal → your app → Scopes: https://dev.valyd.vip IF a scoped endpoint returns 403 with code "insufficient_scope": → the access token is missing that scope. Add the scope to your authorization URL and have the user re-authenticate. IF unsure which scopes a token carries: → re-run the authorization flow and confirm the requested `scope` parameter matches the scopes the endpoint requires. ``` --- ## `profile` scope (Required / Mandatory) User profile: legal name, username, country, and verification status. No photo is shared. ### Grants access to - `/userinfo` ### Response fields | Field | Description | | --- | --- | | `sub` | Unique user identifier | | `first_name` | User's first name | | `last_name` | User's last name | | `full_name` | User's full name | | `country` | User's country | | `valyd_id` | The user's unique Valyd account identifier | | `id_verified` | Whether ID is verified (boolean) | | `created_at` | Account creation timestamp | ### Missing scope error (403 Forbidden) ```json { "success": false, "error": { "code": "insufficient_scope", "message": "The request requires the profile scope" } } ``` --- ## `email` scope (Optional) The user's email address. By default this is a **private relay address** (the user's real email stays hidden); trusted first-party apps receive the real address. Request `email` as its own scope — it is **not** part of `profile`. ### Grants access to - `/userinfo` (and the ID token) ### Response fields | Field | Description | | --- | --- | | `email` | The user's email address (relay address unless your app is a trusted first party) | | `email_verified` | Whether the email/identity is verified (boolean) | ### Missing scope error (403 Forbidden) ```json { "success": false, "error": { "code": "insufficient_scope", "message": "The request requires the email scope" } } ``` --- ## `phone` scope (Optional) The user's phone number. By default this is a **private relay number** (the user's real number stays hidden); trusted first-party apps receive the real number. Request `phone` as its own scope — it is **not** part of `profile`. ### Grants access to - `/userinfo` (and the ID token) ### Response fields | Field | Description | | --- | --- | | `phone_number` | The user's phone number (relay number unless your app is a trusted first party) | | `phone_number_verified` | Always `false` — Valyd does not verify phone numbers | ### Missing scope error (403 Forbidden) ```json { "success": false, "error": { "code": "insufficient_scope", "message": "The request requires the phone scope" } } ``` --- ## `verifications` scope (Optional) Identity verification status: whether the user passed a human (liveness) check, whether they completed identity (KYC) verification, and any professional licenses linked to their Valyd identity. ### Grants access to - `/verifications` ### Response fields | Field | Description | | --- | --- | | `human_verified` | Whether the user passed a liveness / anti-spoof human check | | `id_verified` | Whether the user completed identity (KYC) verification | | `licenses` | Array of linked professional licenses (`license_type`, `verified`, `verified_from`, `expire_at`) | ### Missing scope error (403 Forbidden) ```json { "success": false, "error": { "code": "insufficient_scope", "message": "The request requires the verifications scope" } } ``` --- ## `doctor_license` scope (Optional) Medical/nursing license details for verified healthcare practitioners. Request this scope only when the user is expected to have a verified medical or nursing license (for example, a doctor or nurse account). ### Grants access to - Doctor/nursing license endpoints (retrieved via the SDK helpers `getDoctorLicense`, `getLicenses`, and `getCprLicense`) ### Missing scope error (403 Forbidden) ```json { "success": false, "error": { "code": "insufficient_scope", "message": "The request requires the doctor_license scope" } } ``` --- ## `zkp` scope (Optional) Zero-Knowledge Proof age verification data. Allows age verification without revealing exact birth date. ### Grants access to - ZKP-related endpoints ### Response fields | Field | Description | | --- | --- | | `is_18` | Whether user is 18+ (without revealing age) | | `is_21` | Whether user is 21+ (without revealing age) | | `is_25` | Whether user is 25+ (without revealing age) | ### Missing scope error (403 Forbidden) ```json { "success": false, "error": { "code": "insufficient_scope", "message": "The request requires the zkp scope" } } ``` --- ## `mcp` scope (Optional) Access to Model Context Protocol (MCP) endpoints. Lets AI agents and tools retrieve the user's authorized identity and verification data through the MCP interface, on the user's behalf. ### Grants access to - MCP endpoints ### Response fields | Field | Description | | --- | --- | | `tools` | MCP tools the agent is authorized to call | | `context` | User identity and verification context exposed to the agent | ### Missing scope error (403 Forbidden) ```json { "success": false, "error": { "code": "insufficient_scope", "message": "The request requires the mcp scope" } } ``` ================================================================================ === FILE: https://docs.valyd.vip/docs/sdks.md === ================================================================================ > Source: https://docs.valyd.vip/docs/sdks # SDKs & tools Everything here is real and maintained — no empty language tabs. For stacks without an SDK, use the [raw API quickstarts](/docs/quickstarts) or point [your own OIDC library](/docs/oidc) at the discovery document. ## @valyd/sdk — Node.js (official) The one package for both products: `valyd.auth` (Connect with Valyd — OIDC transaction, PKCE, verified ID tokens) and `valyd.verify` (verification sessions, checks, webhooks). ```bash npm install @valyd/sdk ``` - [Node quickstart](/docs/quickstart/node) · [SDK guide](/verifications/sdk) · npm: `@valyd/sdk` - Server-side only — the client secret and API key never belong in a browser. ## Sign-in button (browser) A two-line drop-in "Sign in with Valyd" button — generates `state`/`nonce`, sets first-party cookies, and starts the standard flow. Environment follows the script host. ```html ``` - [Button flow](/docs/flows/button) ## Starter project A minimal Express app wired with `@valyd/sdk` — clone, fill `.env`, `npm run dev`. It shows the full **Connect with Valyd** (OIDC + PKCE) flow, and — if you set `VALYD_VERIFY_API_KEY` + `VALYD_WORKFLOW_ID` in `.env` — adds a **Test a verification workflow** button that runs one of your Verify workflows against the signed-in account. - GitHub: `valyd-id/valyd-sandbox-starter` · [Download zip](/downloads/valyd-sdk-starter.zip) ## MCP server (AI agents) Remote MCP server exposing verification as agent tools, secured with OAuth 2.1 + PKCE. - [Setup](/ai/mcp-setup) · [Tools](/ai/mcp-tools) · Endpoint: `https://mcp.valyd.vip/verification/mcp` ## Machine-readable - [OpenAPI specs](/docs/api-reference) — the canonical contract - [Postman collection](/valyd-postman-collection.json) - [llms.txt](/llms.txt) — the agent-readable docs index ================================================================================ === FILE: https://docs.valyd.vip/docs/security-disclosure.md === ================================================================================ > Source: https://docs.valyd.vip/docs/security-disclosure # Security disclosure How to report a security vulnerability in Valyd. Report security issues to **security@valyd.id** — we acknowledge valid reports and coordinate disclosure with the reporter. This page is also the home for Valyd's `security.txt`. ## Reporting a vulnerability | Item | Value | | --- | --- | | Security contact | **security@valyd.id** | | Encryption key for reports | Not currently offered — email **security@valyd.id** and we will arrange a secure channel if needed | | Acknowledgement target | We acknowledge valid reports and follow up with the reporter (no fixed public SLA) | | Disclosure policy | Coordinated disclosure — we ask for reasonable time to remediate before public disclosure | | Bug bounty | No public bug-bounty program at this time — report directly to **security@valyd.id** | ## What to include To speed triage, a report should include: - a clear description of the issue and its impact, - reproduction steps or a proof of concept, - the affected endpoint or surface, and - any relevant `X-Request-Id` values (see [Errors & troubleshooting](/docs/errors#contacting-support)). Do **not** include real end-user identity data, API keys, or tokens in a report. ## Recommended: publish a `security.txt` Valyd should publish a machine-readable disclosure policy at the well-known location `/.well-known/security.txt` (RFC 9116) on its primary domains, so researchers can find the right contact without guessing. A minimal file looks like: ```text Contact: mailto:security@valyd.id Expires: Policy: Preferred-Languages: en ``` - Serve it over HTTPS at `/.well-known/security.txt`. - Keep the `Expires` field current (RFC 9116 requires it). - Valyd serves it on its primary product domains; contact **security@valyd.id** if you cannot locate it. ## See also - [Trust Center](/docs/security-trust) - [Operations & SLA](/docs/operations-sla) - [Errors & troubleshooting](/docs/errors) ================================================================================ === FILE: https://docs.valyd.vip/docs/security-trust.md === ================================================================================ > Source: https://docs.valyd.vip/docs/security-trust # Trust Center How Valyd protects the data an integration touches, and where to find the formal assurances your security review will ask for. The controls below are drawn from Valyd's documented [Data & trust](/docs/data-and-trust) policies. Formal compliance evidence (reports, attestations, and a DPA) is shared with enterprise customers on request under NDA — contact your Valyd account team or **security@valyd.id**. ## Encryption - **In transit** — every documented endpoint is HTTPS/TLS. Production apps must register HTTPS redirect URIs. See [Data & trust](/docs/data-and-trust#security-properties). - **At rest** — the personal data a Valyd account holds (legal name, KYC fields) is stored encrypted at rest on Valyd's systems. - **Raw KYC data** — document fields extracted during a hosted KYC stay encrypted and are released to your integration only after the required ID, liveness, and face-match checks pass. - **Consent-released attributes** — vault-only attributes (DOB, document number, gender, nationality) are sealed on the user's device to a key only you hold (libsodium sealed box, X25519); Valyd cannot read the released values in transit. - **Secrets** — API keys and webhook signing secrets are stored encrypted. ## Biometrics are irreversible vectors, never images Valyd does not store or return face images. Enrollment converts a selfie into a one-way biometric vector (template); every later face match compares vectors. The template is never exposed through any API — not to the user, not to integrators. Full detail: [Biometrics: vectors, never images](/docs/data-and-trust#biometrics-vectors-never-images). ## Transient handling of submitted images The photos you submit to a check (ID images and selfies) are processed transiently for that check and are **not retrievable from a Valyd account** afterward. The KYC `portrait` field is extracted from the ID document you submitted in that request and returned in that response only — it is not a stored account photo. See [What data goes where](/docs/data-and-trust#what-data-goes-where). ## Data minimization by design Account-connected verification returns **proofs only** (verified status, license badges, age bands), never raw account KYC. The Account API reads what previous checks already proved and never runs a check itself. This is the recommended integration mode precisely because the sensitive data stays inside Valyd's controls. ## Compliance & assurance The following are standard requests in an enterprise security review. Valyd shares its current status and the underlying evidence with enterprise customers under NDA — contact your Valyd account team or **security@valyd.id** to request them. | Item | Status | | --- | --- | | SOC 2 (Type I / Type II) | Report and current status available under NDA — contact **security@valyd.id** | | ISO / IEC 27001 | Current certification status available under NDA — contact **security@valyd.id** | | GDPR alignment | Posture and controller/processor terms available under NDA — contact **security@valyd.id** | | Data Processing Agreement (DPA) | Available on request — contact your Valyd account team or **security@valyd.id** to execute a DPA | | Penetration testing | Summary results available under NDA — contact **security@valyd.id** | | Vulnerability / breach response | Handled through [Security disclosure](/docs/security-disclosure); incident-response details available under NDA — contact **security@valyd.id** | | Subprocessors & data residency | Available on request — see [Data residency](/docs/data-residency) | | Data retention | See [Data retention](/docs/data-retention) | | Responsible disclosure | See [Security disclosure](/docs/security-disclosure) | ## See also - [Data retention](/docs/data-retention) — what is kept and for how long - [Data residency](/docs/data-residency) — where data is processed - [Operations & SLA](/docs/operations-sla) — uptime, status, incident comms - [API key lifecycle](/docs/api-key-lifecycle) — key rotation and access model - [Security disclosure](/docs/security-disclosure) — how to report a vulnerability ================================================================================ === FILE: https://docs.valyd.vip/docs/support-escalation.md === ================================================================================ > Source: https://docs.valyd.vip/docs/support-escalation # Support & escalation How to get help with Valyd and what to include so it can be triaged fast. Two things are fixed here: security vulnerabilities route through the [Security disclosure](/docs/security-disclosure) page, not normal support; and for an API or integration problem the details to send (and the things to *never* send) are the exact diagnostic contract from [Errors & troubleshooting](/docs/errors#contacting-support). For general help, contact **support@valyd.id**; support tiers, response-time targets, and a status page are available to enterprise customers — contact your Valyd account team. ## Security issues go through disclosure A suspected vulnerability, exposed secret, or any security-sensitive finding does **not** go to normal support — report it through [Security disclosure](/docs/security-disclosure), which is the home for the security contact and the responsible-disclosure policy. Do not include real end-user identity data, API keys, or tokens in a report. ## Reporting an API or integration problem For a failing call or an unexpected result, include the diagnostic details from [Errors & troubleshooting](/docs/errors#contacting-support): - the **`X-Request-Id`** header value from the failing response, - the **session / event id** if the problem involves a verification (`session_id`, or the `event_id` from a webhook), - a **timestamp** (with time zone), and - the **endpoint** you called and the **environment** it ran against (each environment has its own IdP host). **Never send** API keys, tokens, or the person's identity data — support will never ask for them. Before opening a ticket, read the status first: a `4xx` is almost always an integration fix you can make yourself (see the [error catalog](/docs/errors#3-complete-code-catalog)), while a persistent `5xx` or a degraded-dependency signal (`engine_unreachable`, `face_match_unavailable`) is Valyd-side — [retry with backoff](/docs/operations-sla#degraded-dependency-behavior) before escalating. ## Channels & response targets General support runs through **support@valyd.id**; response-time commitments are part of enterprise plans. | Item | Value | | --- | --- | | Support channel | **support@valyd.id** | | Support hours | Available on request — contact **support@valyd.id** | | Support tiers / plans | Available on enterprise plans — contact your Valyd account team | | First-response targets | Committed response targets are part of enterprise plans — contact your Valyd account team | | Escalation path | For a production-impacting incident, contact your Valyd account team | | Status page | Available on request — contact **support@valyd.id** | | Account / billing contact | **support@valyd.id** | The committed availability and SLA figures, once confirmed, live on [Operations & SLA](/docs/operations-sla) — this page is only the *how to reach us* home. ## See also - [Security disclosure](/docs/security-disclosure) — report a vulnerability - [Errors & troubleshooting](/docs/errors) — read the error, then what to send - [Operations & SLA](/docs/operations-sla) — uptime, status, incident comms - [Audit logging](/docs/audit-logging) — the ids you quote when you ask for help ================================================================================ === FILE: https://docs.valyd.vip/docs/testing.md === ================================================================================ > Source: https://docs.valyd.vip/docs/testing # Testing your integration > Valyd verification is always real — there is no fake-result sandbox. The sandbox at > [/sandbox](/sandbox) tests **login** with demo users; verification checks run for real. ## Testing login **Connect with Valyd is always free** — connecting users, the OIDC flow, and reading account data never cost anything, in development or production. Only verification checks are billed. The [Try the APIs](/sandbox) playground runs the full OAuth 2.0 / OIDC flow against **demo users** with shared test credentials — pick a demo user, choose scopes, and walk from authorization code to `userinfo` without writing code. Use it to see exactly what your app will receive at login before you register your own app. For your own app, the [complete example](/docs/quickstart/node) runs on `localhost`: `http://localhost:8080/callback` is a valid redirect URI for development — register it exactly in the [Developer Portal](https://dev.valyd.vip). For production, switch to your exact HTTPS callback URL. ## Every new account starts with $100 ![The Transactions page: wallet balance, welcome credit, and the top-up contact](/images/screenshots/portal-funds.png) New developer accounts receive a **one-time $100 welcome credit** in the wallet, so you can build and test against real verification before adding funds. When you need more, email [javi@valyd.id](mailto:javi@valyd.id) from your account email — top-ups are applied by an administrator and show up in your Transactions ledger. ## Verification checks are real (and billed) There are no simulated verification results. When you call a [Unique Human API](/verifications/unique-human) endpoint, or a workflow runs a license lookup or any other check, the real pipeline runs — against the real face models and the real license registries — and the call is [billed against your app](/verifications/api-reference). That's deliberate: a decision you can trust in production is the same decision you saw in testing. Practical consequences: - **Watch your wallet balance** in the [Developer Portal](https://dev.valyd.vip) — an exhausted balance returns [`402 Payment required`](/docs/errors); top up in the console. - **Use the `Idempotency-Key` header** on every billable `POST /api/v2/*` so a network retry can never double-run or double-charge a check — see [Idempotency](/verifications/unique-human#idempotency). - **Test with real inputs**: your own ID and selfie, or a real license number. A failed check is a real failure worth reading — inspect `check.error` and the [decision breakdown](/verifications/statuses). - **Clean up test faces**: `DELETE /api/v2/face-uniqueness/{valyd_uuid}` removes a face you enrolled during testing from the gallery. ## Separate test and production apps Create **separate apps** in the Developer Portal (e.g. "Test" and "Production") so each has its own `client_id`/`client_secret`, App API key, workflows, and webhook endpoint. Rotate or revoke the test app's key without touching production. This documentation environment talks to `https://idp.valyd.vip`. Each Valyd environment has its own hosts and its own credentials — an app registered in one environment does not exist in another, so keep per-environment values in your `.env` (as the [complete example](/docs/quickstart/node) does with `VALYD_IDP_URL`). ## Testing webhooks ![The Webhook Tester in the app's verification settings](/images/screenshots/portal-webhook-tester.png) Webhooks need a **publicly reachable HTTPS endpoint**. Two tools cover every case: **Webhook Tester (Developer Portal).** In your app's webhook settings, send a simulated decision event — Approved, Declined, In review, or Expired — straight to your registered URL. Test events are signed with your **real signing secret** (so your verification code genuinely runs) and carry `"test": true` at the top level with a `sess_test_…` id — they never create or modify real verifications. Two extra modes exercise your handler's defenses: - **Send duplicate** — the same event id delivered twice; your handler should process it once. - **Send bad signature** — an invalid signature; your handler must reject it (400), never 200. The result (status code, latency, response excerpt) is shown inline in the portal. **Real deliveries.** Run a real verification session to a terminal state and confirm your handler returns 200. Every delivery attempt (payload, headers, receiver response) is recorded under **Recent webhook deliveries**, with a **Resend** button to replay one after you fix your handler. See [Webhooks](/verifications/webhooks). > **Testing principle:** Valyd may simulate *integration* behavior (a test webhook can say > `declined`), but Valyd never fabricates *verification* — a simulated event can never become a > real proof, credential result, or verified identity. ================================================================================ === FILE: https://docs.valyd.vip/docs/tokens.md === ================================================================================ > Source: https://docs.valyd.vip/docs/tokens # Tokens > 🔑 **Minted by:** `POST /api/auth/oidc/token` · 🎫 Three tokens per login: access, ID, refresh · 📏 **Rule of thumb:** access token → call APIs, ID token → establish the login, refresh token → renew quietly Every successful [Authorization Code](/docs/flows/authorization-code) exchange returns three tokens in one top-level JSON. Each has exactly one job — most integration bugs come from using one token for another's job. | Token | Lifetime | Job | | --- | --- | --- | | Access token | ~15 minutes (`expires_in` ≈ 900) | Call Valyd resource APIs as the user | | ID token | Validated once at login (`exp` ≈ 15 min) | Prove *who* logged in, to *your* backend | | Refresh token | 30 days, **rotates on every refresh** | Mint new access tokens without the user | ## Login sessions Together, these three tokens **are** a login session — a signed-in user your backend holds. There is no separate session object to create: your backend keeps the access token (to call APIs, ~15 min) and the rotating refresh token (to renew quietly, 30 days), usually mirrored by your own app session cookie. The login lasts as long as you keep refreshing — up to 30 days per rotating refresh token — and ends at [logout](/docs/flows/refresh#logout--revocation), on refresh-token theft detection, or after 30 days of silence. > 🧭 **One word, two things.** A *login session* (this page) is unrelated to a *verification > session* (one person's run through a check, ending in a decision). "Session expired" from a > resource API means *refresh the access token*; `EXPIRED` from the decision API means *create a new > [verification session](/verifications/session-lifecycle)*. A dead login session never invalidates > a verification result, and a finished verification session never logs anyone in. ## Access token Sent as `Authorization: Bearer …` to `/userinfo`, `/licenses`, `/verifications`, and to the Verification API as `valyd_access_token` for [account-connected checks](/docs/flows/account-connected). It's scope-gated: it can only reach what the user approved on the consent screen. Decoded example payload (illustrative): ```json { "iss": "https://idp.valyd.vip", "sub": "valyd_f895da61d5174b81b8dd6a4e3b417339", "aud": "YOUR_CLIENT_ID", "iat": 1755600000, "exp": 1755600900, "scope": "openid profile verifications" } ``` **Use it for:** calling Valyd APIs on the user's behalf; attaching to a verification session so the proof saves to their account. **Never use it for:** identifying the user in your app (that's the ID token's job), or storing long-term — it dies in ~15 minutes; [refresh](/docs/flows/refresh) instead. > **Treat it as opaque.** Its internal format is Valyd's to change. Don't parse it, don't build > logic on its claims — pass it in the `Authorization` header and let the API validate it. ## ID token An **RS256-signed JWT** — the login receipt. Your backend validates it once at login and uses its claims to create your own session. Decoded example payload (illustrative): ```json { "iss": "https://idp.valyd.vip", "sub": "valyd_f895da61d5174b81b8dd6a4e3b417339", "aud": "YOUR_CLIENT_ID", "iat": 1755600000, "exp": 1755600900, "nonce": "RANDOM_NONCE_FROM_AUTHORIZE", "name": "John Doe", "preferred_username": "john.doe", "id_verified": true } ``` Claim notes: `sub` is the **stable `valyd_…` id — use it as your primary key**; `aud` must equal your `client_id`; `nonce` must equal the value you sent on `/authorize` (replay protection); `id_verified` tells you the account passed identity verification. **Use it for:** establishing the login on your backend, keying the user by `sub`, and later as the `id_token_hint` on [logout](/docs/flows/refresh#logout--revocation). **Never send it to an API.** It is not an access credential — Valyd endpoints will reject it, and an ID token accepted as an API credential anywhere is a security bug. It also never belongs in a URL or in browser storage. > **Always validate before trusting**: signature (RS256/JWKS), `iss`, `aud`, `exp`, `nonce`. An > unvalidated ID token is just attacker-writable JSON. ## Refresh token An opaque string (`rfrsh_…` — not a JWT, nothing to decode) held **only on your backend**: ```json { "refresh_token": "rfrsh_abc123…", "what_you_can_read_from_it": "nothing — it is an opaque credential, not a JWT" } ``` **Use it for:** minting a new access token at the token endpoint with `grant_type: "refresh_token"`, from your backend, with your client credentials. **Never use it for:** calling APIs, or anywhere client-side. It's the longest-lived credential in the system — treat it like a password. > **Rotation is on.** Every refresh revokes the token you sent and returns a new one — persist > the new value every time. Replaying a rotated-away token is treated as **theft** and revokes > the user's entire refresh-token family for your client. Full mechanics: > [Refresh & logout flow](/docs/flows/refresh). ## Validating tokens **Let a library do it.** The `@valyd/sdk` `handleCallback()` / `exchangeCode()` verify the ID token's RS256 signature against discovery/JWKS plus issuer, audience, expiry, and nonce before returning. Any standard OIDC library pointed at `https://idp.valyd.vip/api/.well-known/openid-configuration` does the same. **Validating manually** (no SDK): fetch the signing keys from the JWKS at `https://idp.valyd.vip/api/auth/oidc/jwks.json`, verify the RS256 signature, then check `iss === "https://idp.valyd.vip"`, `aud === your client_id`, `exp` in the future, and `nonce === the value you sent`. Never accept `alg: "none"` or an unexpected algorithm. ## Related - Where tokens come from: [Authorization Code flow](/docs/flows/authorization-code) - Endpoint contract: [API reference — token endpoint](/docs/endpoints#post-apiauthoidctoken--token-exchange--refresh) - Keeping them fresh: [Refresh & logout flow](/docs/flows/refresh) ================================================================================ === FILE: https://docs.valyd.vip/docs/user-token/account.md === ================================================================================ > Source: https://docs.valyd.vip/docs/user-token/account # Read verified data > 🔑 **Auth:** the user's Bearer `valyd_access_token` · 💸 Free — reads never cost a check · 🔒 Proofs and public data only, never PII Three reads cover everything the account already holds: | API | One call gets you | | --- | --- | | [`GET /oidc/userinfo`](/docs/endpoints#get-userinfo--get-user-profile) | Who they are — legal name, username, country, `id_verified` | | [`GET /oidc/licenses`](/docs/endpoints#get-licenses--get-professional-licenses) | Professional licenses already verified on their account | | [`GET /oidc/verifications`](/docs/endpoints#get-verifications--get-identity-verifications) | Every proof and badge — KYC done, age bands, license badges | ```typescript const proofs = await valyd.auth.getVerifications(accessToken); if (proofs.id_verified) { // KYC already done — you're finished. No check, no cost, no PII stored. } ``` **This is the KYC-reuse story:** the user verified once — maybe in another app. You check the status, you don't re-run it, and you never store personal data. Raw identity attributes (DOB, document fields) are separate — the user approves them explicitly via the [consent flow](/docs/request-data). Reads are only half of it — with the same token you can **re-prove who they are, right now**: match their face before a sensitive action, confirm their location, or re-check a live license. See [what the verification APIs let you build](/docs/how-valyd-works#what-the-verification-apis-let-you-build). Something missing? Run a configured **[workflow](/verifications/workflows)** on a [Reusable Verification](/verifications) session with the user's token — face match, liveness, license, age, location, and [KYC](/docs/user-token/kyc) are all available as workflow checks (see the [checks reference](/verifications/types)). The user completes them on Valyd's verification page, and the passed proofs save back to their Valyd ID. ================================================================================ === FILE: https://docs.valyd.vip/docs/user-token.md === ================================================================================ > Source: https://docs.valyd.vip/docs/user-token --- product: valyd-id api_version: oidc auth: oidc-bearer billable: true pii_mode: proofs human_setup_required: true source_of_truth: openapi --- # Reusable Identity > 🔑 **Auth:** the user's Bearer `valyd_access_token` (from [Connect with Valyd](/docs)) · 💾 Passed checks save to the user's Valyd ID · 🔒 PII stays with Valyd The user connected with Valyd — their access token now unlocks everything in this section. The habit that makes Valyd cheap and fast: **read first, verify only what's missing.** | Page | What it does | | --- | --- | | [Read verified data](/docs/user-token/account) | Profile, licenses, proofs — free, instant, the KYC-reuse story | | [KYC / ID verification](/docs/user-token/kyc) | Government-ID identity → `id_verified: true` | | [Checks reference](/verifications/types) | Face match, liveness, license, age, location — every check a workflow can run | | [Workflows](/verifications/workflows) | Configure the checks in the portal, run them all on one page | This is **[Reusable Verification](/verifications)**: you create a verification session with `valydAccessToken` attached, the user completes the configured **workflow** on Valyd's verification page, the passed proofs save to their Valyd ID, and next time [the read](/docs/user-token/account) answers yes. ID/KYC, face match, age, and professional license all run this way — as workflow checks on the connected user's session, never as direct public API calls. (Need only Liveness or Uniqueness, with no login and nothing saved? That's the [Unique Human API](/verifications/unique-human).) ## Run a workflow **Configure the checks in the portal, we run them all on one page** — and send you proofs, public data, and what passed, on your webhook: ![Composing a workflow: pick the checks, in order](/images/screenshots/portal-workflow-wizard.png) ```typescript const session = await verify.sessions.create({ workflowId, // the checks you picked valydAccessToken: accessToken, // ties the run to the user redirectUrl: "https://yourapp.com/verified", }); // → send the user to session.url // → webhook: what passed + proofs + public data — the account updates itself ``` Full walkthrough with every portal step: **[Run a verification →](/verifications/quickstart)** ## Raw identity data is separate Everything above returns proofs and public data — never documents or DOB. If you genuinely need a raw attribute, the user approves it explicitly: [consent flow](/docs/request-data). ## Working as an organization? Apps, workflows, and billing can live in a shared [Organization](/docs/organizations) — and we onboard the workforce for you: add members over the [SDK](/docs/organizations/api), each is notified with a face-activation link, and signs into your apps by face from then on. Fetch member details, assign roles, deactivate/reactivate — all documented there. Member account **recovery** is coming soon. ================================================================================ === FILE: https://docs.valyd.vip/docs/user-token/kyc.md === ================================================================================ > Source: https://docs.valyd.vip/docs/user-token/kyc # KYC / ID verification > 🔑 **Auth:** the user's `valyd_access_token`, on a verification session · ✅ Proves: government-ID identity → `id_verified: true` · 🔒 Valyd's verification page only Account-connected KYC runs **only on Valyd's verification page** — the user's identity data is encrypted with a key that lives on their device and Valyd's own surface, so an API upload from your backend can't fill their vault. Valyd's verification page can; your server never touches the documents: ```typescript const session = await verify.sessions.create({ workflowId, // a workflow containing the KYC check valydAccessToken: accessToken, redirectUrl: "https://yourapp.com/verified", }); // → send the user to session.url — ID scan, liveness, face match all happen there // → webhook: id_verified + proofs — the documents stay with Valyd, encrypted ``` **Simplest handoff.** If all you need is "make this connected user complete KYC," gate on the proof they already hold, then run a workflow session with their token — Valyd hosts the KYC page and brings the user back when they're done: ```typescript // Gate first: only run KYC if it isn't already on the account const verifications = await valyd.auth.getVerifications(accessToken); if (!verifications.id_verified) { const session = await verify.sessions.create({ workflowId, // a workflow that includes the ID / KYC check valydAccessToken: accessToken, // ties the run to the connected user redirectUrl: "https://yourapp.com/verified", }); return res.redirect(session.url); // user completes KYC on Valyd, then returns } ``` > **These handoffs are the only way to establish `id_verified`.** ID/KYC runs exclusively as a > **Reusable Verification** workflow check on Valyd's verification page — there is no direct > public ID/KYC API. The raw document stays encrypted with Valyd; your app receives the reusable > `id_verified` proof, never the raw fields. Steps their account has already passed are skipped automatically. Once done, `id_verified: true` is readable forever via [Read verified data](/docs/user-token/account). Full walkthrough: [Run a verification](/verifications/quickstart) · [Reusable Verification](/verifications). ================================================================================ === FILE: https://docs.valyd.vip/verify/intro.md === ================================================================================ > Source: https://docs.valyd.vip/verify/intro --- product: valyd-verify billable: true pii_mode: proofs human_setup_required: true source_of_truth: sdk --- # Reusable Verification > 🔑 **Auth:** SDK client (App API key) + the connected user's `valyd_access_token` · 💾 **Result:** proofs save to the user's Valyd ID — reusable next time **Reusable Verification** lets a user connect their Valyd identity to your app, share the verified information they already hold, and complete any additional checks your application requires. The developer story is one journey: 1. **[Connect the user with Valyd](/docs/authentication)** — standard OpenID Connect; your backend receives their `valyd_access_token`. (Connect with Valyd can also serve as your app's sign-in.) 2. **[Read what they already have](/docs/user-token/account)** — profile, `id_verified`, verified licenses, badges, and age bands. If the proof you need is there and fresh, you're done — no check, no cost. 3. **Something missing? Run your [workflow](/verifications/workflows)** — the saved bundle of checks you configured for your app (ID/KYC, professional license, face match, liveness, location, …). [Create a session](/verifications/quickstart) with the user's token; Valyd guides the user through the capture. A returning user re-verifies with a **selfie only** (matched against their stored face vector); already-verified KYC and licenses are skipped. 4. **[Read the result](/verifications/statuses)** — the decision arrives on a signed [webhook](/verifications/webhooks) or via `verify.sessions.decision()`. The passed proof lands on the user's Valyd ID — next time, step 2 answers instead of step 3. > **Biometrics are irreversible vectors, never images.** Valyd does not store or return face > images. Enrollment converts a selfie into a one-way biometric vector (template); every later > face match compares vectors. The photos submitted to a check are processed transiently for > that check and are not retrievable from a Valyd account. The template is never exposed through > any API. [Full scoping →](/docs/data-and-trust) ## The mental model - **[Workflow](/verifications/workflows)** — a reusable configuration describing which checks your app requires (configured in the Developer Portal). - **[Session](/verifications/quickstart)** — one user's run through a workflow on Valyd's verification page. - **[Checks](/verifications/types)** — the individual verifications: ID/KYC, liveness, face match, license, age, location, … - **[Decision](/verifications/statuses)** — the authoritative combined outcome: `APPROVED` / `DECLINED` / `IN_REVIEW`. - **Proof** — the durable outcome saved to the user's Valyd ID when a check ran with their token; read it back via the [Account API](/docs/endpoints#resource-api--user-data). The account's `identity` object carries a `verified_at` timestamp so you can judge freshness; a license badge carries the registry's own `status` and `expires_at`. Re-run a check when your policy needs a fresher answer. ## Data-sharing rule (critical) - **Account APIs return proofs only** — a pseudonym, `id_verified`, verified license badges, and age bands. They **never** return raw KYC (legal name, date of birth, document images). In a decision, the `id_verification` check reduces to `{ status, id_verified }`; `identity` is `{ valyd_id, pseudonym, id_verified, age_bands, licenses, verified_at }`. - **Raw account KYC is released only through the consent flow** — you request specific attributes, the user approves in their Valyd app, and the values are returned end-to-end encrypted (X25519 sealed box). See [Consent & data access](/verifications/data-sharing) and [Request data](/docs/request-data). - Documents, selfies, and personal fields stay [encrypted with Valyd](/docs/data-and-trust#security-properties), not in your database — you **read verified status instead of handling identity data**. - Webhooks are sent only to an active URL configured for your app and are signed. ## The session in code Verification runs through the [`@valyd/sdk`](/verifications/sdk) client — you never manage raw endpoints or capture UI. One call ties the run to the connected user: ```typescript const session = await verify.sessions.create({ workflowId, // the checks you picked in the portal valydAccessToken: accessToken, // ← ties the run to the connected user redirectUrl: "https://yourapp.com/verified", }); // → send them to session.url — proofs come back, PII doesn't ``` ## Start here - [Connect with Valyd](/docs/authentication) — the OIDC button and callback. - [Create a workflow](/verifications/setup) — portal setup: app, API key, workflow, webhooks. - [Run a verification](/verifications/quickstart) — first session end to end. - [Checks reference](/verifications/types) — everything a workflow can verify. --- Just need to know whether someone is a live, unique human — no user account involved? That's the **[Unique Human API](/verifications/unique-human)**. ================================================================================ === FILE: https://docs.valyd.vip/verify/api-reference.md === ================================================================================ > Source: https://docs.valyd.vip/verify/api-reference --- title: API Reference description: Verification runs through the Valyd SDK — no raw endpoints to call. --- # API Reference Valyd verification runs **through the SDK**. You don't call raw HTTP endpoints or manage URLs — the SDK creates the verification session, hands you the link to send the user to Valyd's verification page, and returns the decision and reusable proofs. - **Node SDK reference** — every method you need: [Node SDK](/verifications/sdk) - **How the flow works** — [Reusable Verification](/verifications) - **Reading results** — [Results & decisions](/verifications/statuses) and [Webhooks](/verifications/webhooks) For the API-key-only liveness check, see [Unique Human API → Anti-spoof](/verifications/unique-human/antispoof). ================================================================================ === FILE: https://docs.valyd.vip/verify/api.mdx.md === ================================================================================ > Source: https://docs.valyd.vip/verify/api.mdx --- title: Verification SDK description: Verification runs through the Valyd SDK — create a session, send the user to Valyd's verification page, read the result back. --- # Verification SDK Valyd verification runs **through the SDK** — there is no raw endpoint surface to integrate against. Install the SDK, create a verification session, send the user to Valyd's verification page, and read the result back. - **Node SDK reference** — [Node SDK](/verifications/sdk) - **The flow, end to end** — [Reusable Verification](/verifications) - **Results & webhooks** — [Results & decisions](/verifications/statuses) · [Webhooks](/verifications/webhooks) ================================================================================ === FILE: https://docs.valyd.vip/verify/console.md === ================================================================================ > Source: https://docs.valyd.vip/verify/console # The Developer Portal ## Apps One app in the Developer Portal carries both identities: the OAuth `client_id` / `client_secret` for **Connect with Valyd**, and a Verify **API key** (`vrf_…`) for the verification APIs. The API key is shown **once** at creation. Create multiple apps such as Test and Production. - The API key is shown **once** at creation. Copy it immediately and store it server-side. - If lost, rotate the key in the Console to generate a new one. ## Workflows Bundle checks (ID, liveness, face match, and the other Verify checks) into a reusable Workflow. Each Workflow has a `workflow_id` used when creating verification sessions. - The `workflow_id` is required as the `workflow_id` field when creating a verification session (see https://docs.valyd.vip/verifications/quickstart). ## Webhooks Configure a per-app endpoint URL and signing secret (rotatable). Valyd POSTs signed events to this URL when a session reaches a terminal state. - Set both the endpoint URL and the signing secret per App. - Verify the signature on incoming events using the signing secret before trusting them. ## SSO The console uses Valyd SSO. Your developer account is separate from end-users you verify. ## Human-only checklist (in the Console) ```text IF you do not yet have an App API key: → Sign in at https://dev.valyd.vip with Valyd SSO (one sign-in covers OAuth apps, Verify apps and workflows) → Open your App and copy the API key shown once at creation; store it server-side IF you lost the API key: → In the Console, rotate the App API key to generate a new one IF you will use Reusable Verification: → Create a Workflow bundling the checks you need and copy its workflow_id → Under Webhooks, set the endpoint URL and copy the signing secret IF you will use the Unique Human API only: → You only need the App API key; Workflows and Webhooks are not required ``` ================================================================================ === FILE: https://docs.valyd.vip/verify/data-sharing.md === ================================================================================ > Source: https://docs.valyd.vip/verify/data-sharing # Consent & data access > 🔑 **Auth:** SDK client (App API key), plus the user's `valyd_access_token` for Reusable Verification The two products share very different data with your application: - **[Reusable Verification](/verifications)** — the user connects with Valyd and you run a verification session with their `valyd_access_token`. You receive the **decision plus reusable proofs** (a pseudonym, `id_verified`, verified license badges, age bands) — raw identity data is shared only with the user's explicit consent. The raw identity data (documents, DOB, face images) stays **encrypted with Valyd** under the user's per-user key. - **[Unique Human API](/verifications/unique-human)** — no login, no Valyd account. This is the one place where check data flows **to you**: you supply the input (the images for an anti-spoof check), Valyd processes them, and the result is returned to your system. Nothing is written to a Valyd account, and there is no proof to reuse later; the data, and the duty of care that comes with it, are yours. The rest of this page covers the **Unique Human API** — the product where you hold the result. (In Reusable Verification, Valyd holds the raw data and you only ever receive proofs.) ## Who holds what (Unique Human API) | | You (the integrator) | Valyd | | --- | --- | --- | | **Inputs** (the selfie / face images for an anti-spoof check) | You capture and submit them | Processed **transiently** for that check, then discarded — not retrievable afterwards | | **Results** (check status, scores, the per-check `data`) | Returned in the HTTP response — yours to store | A session record of the check outcome (status, scores, billing/audit metadata), not the raw images | | **Biometrics** | Never receive a face template | Face vectors are irreversible templates, **never images** — and are not returned to you | | **Storage, protection, deletion** | **Your responsibility** — encrypt at rest, restrict access, delete per your retention policy and local law | Retention limited to the transient processing and session record above | > **Biometrics are irreversible vectors, never images.** Valyd does not store or return face > images. The photos you submit to a check are processed transiently for that check and are not > retrievable afterwards. Where a gallery feature (face uniqueness, anti-spoof + identity) > enrolls a face, it stores a one-way biometric vector (template) — never the photo — and the > template is never exposed through any API. [Full scoping →](/docs/data-and-trust) ## Your responsibilities - **Store securely** — the images you submit and the check `data` you get back can be sensitive; encrypt them at rest and keep them out of logs. - **Limit access** — treat check results with the same access controls as any biometric record. - **Delete on schedule** — you own the retention clock; delete when your policy or the user's request requires it. - **Keep the key server-side** — the App API key never belongs in a browser. ## Need ID/KYC, face match, age, license, or location? Those checks are not part of the Unique Human API — they run only as workflow checks in **[Reusable Verification](/verifications)**: the user connects with Valyd and you run a verification session with their `valyd_access_token`. The passed proof saves to *their* Valyd ID, the raw identity data stays encrypted with Valyd, and your app reads verified status instead of storing documents. ================================================================================ === FILE: https://docs.valyd.vip/verify/quickstart.md === ================================================================================ > Source: https://docs.valyd.vip/verify/quickstart --- product: valyd-verify billable: true pii_mode: proofs human_setup_required: true source_of_truth: sdk --- # Run a verification > 🔑 **Auth:** SDK client (App API key) + a `workflowId` · 👤 Include the connected user's `valyd_access_token` so the proof saves to their Valyd ID Start a verification session for your [workflow](/verifications/workflows), send the user to Valyd's verification page — Valyd handles the capture UI, camera, retries, and security — and read one combined decision when they're done. There is no capture UI to build. The flow at a glance: 1. Create a session on your server with a `workflowId` (and the user's `valyd_access_token`). 2. Redirect the user's browser to the returned `url`. 3. Valyd captures everything and redirects back to your `redirectUrl`. 4. Receive a signed webhook, then fetch the authoritative result with `verify.sessions.decision(id)`. ### Prerequisites All from [Create a workflow](/verifications/setup) — developer setup, not part of your end-user flow: - The **App API key**, copied at app creation (shown once). Store it server-side only. - A **`workflowId`** from a workflow you created in the [Developer Portal](https://dev.valyd.vip). - A **webhook URL + signing secret** configured under Webhooks in the portal. - The connected user's `valyd_access_token` from [Connect with Valyd](/docs/authentication), so the passed proofs save to their Valyd ID and already-proven steps are skipped. Install and initialize the SDK: ```bash npm i @valyd/sdk ``` ```javascript import { VerifyClient } from "@valyd/sdk"; const verify = new VerifyClient({ apiKey: process.env.VALYD_API_KEY, webhookSecret: process.env.VALYD_WEBHOOK_SECRET, // used by verify.webhooks.constructEvent }); ``` ## Create a session Call `verify.sessions.create` from your **backend**. The response includes the `url` you send the user to: ```javascript const session = await verify.sessions.create({ workflowId: process.env.VALYD_WORKFLOW_ID, // the checks you picked in the portal valydAccessToken: accessToken, // ties the run to the connected user redirectUrl: "https://app.example.com/verify/callback", callback: "https://api.example.com/webhooks/valyd", vendorData: "user_123", // your internal ref — echoed back on the webhook ttlSeconds: 900, }); // session.url → redirect the user's browser here // session.sessionId, session.sessionToken, session.expiresAt ``` The returned `session` carries the verification-page `url` plus `sessionId`, `sessionToken`, `features`, `redirectUrl`, and `expiresAt`: ```json { "sessionId": "ses_…", "status": "NOT_STARTED", "url": "https://idp.valyd.vip/s/…", "sessionToken": "stk_…", "features": ["id_verification","liveness","face_match","credential"], "redirectUrl": "https://app.example.com/verify/callback", "expiresAt": "2026-06-11T12:00:00Z" } ``` > Keep your API key **server-side only** — never create a session from the browser. Only the > session `url` and the `sessionToken` are safe to send to the client. `VALYD_WORKFLOW_ID` is the > `workflowId` from the Developer Portal (https://dev.valyd.vip → Workflows). Then redirect the user's browser to `session.url` (e.g. `res.redirect(session.url)`). Valyd renders the whole capture and verification UI; the steps auto-adapt to the workflow's checks. With the user's token on the session, the run pre-fills from their account, skips already-proven steps, and saves passed proofs to their Valyd ID. ## After the user returns Valyd sends the user's browser back to your `redirectUrl` with `?session_id=…&status=…`. **Treat `status` as a hint only** — never grant access on that query param. Fetch the authoritative outcome with `verify.sessions.decision(id)` (or wait for the signed webhook): ```javascript const decision = await verify.sessions.decision(sessionId); // decision.status, decision.checks[] ``` - [Session lifecycle](/verifications/session-lifecycle) — the full state machine and what to do at each stage. - [Results & decisions](/verifications/statuses) — every status value, the per-check statuses, and reading the decision payload. - [Webhooks](/verifications/webhooks) — the signed terminal-state callback and how to verify it. ## Other session helpers ```javascript const session = await verify.sessions.retrieve(sessionId); const page = await verify.sessions.list({ status: "APPROVED", vendorData: "user_123", limit: 50 }); await verify.sessions.updateStatus(sessionId, "APPROVED"); // or "DECLINED" — manual review decision (IN_REVIEW sessions only) ``` ## SDK surface used in this flow | SDK method | Purpose | | --- | --- | | `verify.sessions.create({ workflowId, valydAccessToken?, redirectUrl, callback?, vendorData?, ttlSeconds? })` | Create a verification session. | | `verify.sessions.decision(id)` | Read the authoritative decision and per-check breakdown. | | `verify.sessions.retrieve(id)` | Retrieve a session. | | `verify.sessions.list({ status?, vendorData?, limit? })` | List sessions. | | `verify.sessions.updateStatus(id, "APPROVED" \| "DECLINED")` | Manually decide an `IN_REVIEW` session (approval still requires passed ID, liveness, and face-match checks). | | `verify.webhooks.constructEvent(rawBody, headers)` | Verify and parse the signed webhook Valyd POSTs to your `callback`. | > **Workflow CRUD is not in the SDK** — compose workflows in the > [Developer Portal](https://dev.valyd.vip) and pass the resulting `workflowId` to > `verify.sessions.create({ workflowId, … })`. ## Full implementation example The complete Express integration — create the session, handle the redirect back (status is a hint only), verify the signed webhook, then pull the authoritative decision: ```javascript import express from "express"; import { VerifyClient, ValydVerifyError } from "@valyd/sdk"; const app = express(); const verify = new VerifyClient({ apiKey: process.env.VALYD_API_KEY, webhookSecret: process.env.VALYD_WEBHOOK_SECRET, }); // 1) Start verification app.post("/start-verification", express.json(), async (req, res) => { const session = await verify.sessions.create({ workflowId: process.env.VALYD_WORKFLOW_ID, // license-only OR kyc+license redirectUrl: `${process.env.APP_URL}/verify/callback`, callback: `${process.env.APP_URL}/webhooks/valyd`, vendorData: req.body.userId, }); res.json({ url: session.url, sessionId: session.sessionId }); }); // 2) Redirect-back (status is a hint only) app.get("/verify/callback", (req, res) => { res.redirect(`/verify/pending?s=${req.query.session_id}`); }); // 3) Signed webhook — MUST use raw body app.post( "/webhooks/valyd", express.raw({ type: "application/json" }), async (req, res) => { try { const event = verify.webhooks.constructEvent(req.body, req.headers); // 4) Pull the full decision (webhook is a notification) const decision = await verify.sessions.decision(event.sessionId); await persist(event.vendorData, decision); res.json({ ok: true }); } catch (err) { if (err instanceof ValydVerifyError && err.code === "invalid_signature") { return res.status(400).send("bad signature"); } throw err; } } ); app.listen(3000); ``` ### Common errors 1. **`API_KEY_INVALID`** — missing, wrong, or rotated API key, or it was used client-side. Re-copy the key from the portal (or rotate it), set `VALYD_API_KEY` server-side, and pass it to `new VerifyClient({ apiKey })`. Never expose it in browser code. 2. **`VALIDATION_ERROR` on `verify.sessions.create`** — missing or invalid `workflowId`. Create a workflow in the portal, set `VALYD_WORKFLOW_ID`, and verify `echo $VALYD_WORKFLOW_ID` is non-empty. 3. **No webhook received** — webhook URL/signing secret not configured, or your endpoint is not publicly reachable. Set them in Portal → Webhooks, ensure the URL is publicly reachable, and verify the signature via `verify.webhooks.constructEvent` before trusting the event. ### Next steps - **Compose the checks** — [Workflows](/verifications/workflows) · [Checks reference](/verifications/types). - **Track the run** — [Session lifecycle](/verifications/session-lifecycle). - **Read the result** — [Results & decisions](/verifications/statuses) and [Webhooks](/verifications/webhooks). ================================================================================ === FILE: https://docs.valyd.vip/verify/sdk.md === ================================================================================ > Source: https://docs.valyd.vip/verify/sdk # Node SDK ## Recipe ### Prerequisites - Node 18+ installed. Check with: ```bash node --version ``` **Expected output:** `v18.x.x` or higher. If lower, upgrade Node before continuing. - Credentials from the Valyd Developer Portal (https://dev.valyd.vip): - `VALYD_API_KEY` — the App API key the SDK client authenticates with on every request. - `VALYD_WEBHOOK_SECRET` — needed to verify webhook signatures (Reusable Verification). - `VALYD_WORKFLOW_ID` — needed when creating verification sessions. ```text IF you are building Reusable Verification (send the user to Valyd's verification page): → you need VALYD_API_KEY, VALYD_WEBHOOK_SECRET, and VALYD_WORKFLOW_ID IF you are using the Unique Human API (a no-account session for a liveness/uniqueness workflow): → you need VALYD_API_KEY and VALYD_WORKFLOW_ID IF unsure which credentials you have: → log in to https://dev.valyd.vip and check your app's API keys / webhooks / workflows ``` ### Steps 1. **Install the SDK.** ```bash npm i @valyd/sdk ``` **Expected output:** npm adds `@valyd/sdk` at its latest published version to `dependencies` in `package.json`, so a fresh install always pulls the newest release. 2. **Set environment variables** (e.g. in a `.env` file or your process environment). Get each value from the Valyd Developer Portal: https://dev.valyd.vip. ```bash VALYD_API_KEY=your_api_key_here # App API key for every request VALYD_WEBHOOK_SECRET=your_webhook_secret # required for webhook handling VALYD_WORKFLOW_ID=your_workflow_id # required to create verification sessions ``` **Expected output:** no output; these are read at runtime via `process.env.*`. 3. **Initialise the client** in your server code. ```javascript import { VerifyClient } from "@valyd/sdk"; const verify = new VerifyClient({ apiKey: process.env.VALYD_API_KEY!, }); ``` **Expected output:** a `VerifyClient` instance. No network call is made on construction. If `apiKey` is missing, a later call throws `ValydVerifyError` with code `config_error`. #### Constructor options | Option | Type | Default | Description | | --- | --- | --- | --- | | `apiKey` | string | — | Required. The App API key the client authenticates with on every request. | | `baseUrl` | string | `https://idp.valyd.vip` | API base URL. Override only for staging/self-hosted. | | `webhookSecret` | string | — | Optional. When set, `webhooks.constructEvent` / `verify` can be called without passing the secret explicitly. | | `timeoutMs` | number | `15000` | Per-request timeout. **Credential lookups (`credentialVerification`, `kycCredential`) automatically use at least 60s** — set a higher value here only if you want a bigger floor for all calls. | | `fetch` | typeof fetch | — | Custom fetch implementation (proxies, instrumentation, tests). | ### Authentication Every Verification API call is authenticated by your **App API key** — the `apiKey` you pass to the constructor. This is the credential that matters for the SDK; get it from the Developer Portal → your project → Credentials. - **`apiKey` (`vrf_…`)** — authenticates all `verify.*` calls (sessions, the Unique Human API, workflows). This is the only credential the SDK needs to make requests. - **`webhookSecret` (`whsec_…`)** — NOT an auth credential for outbound calls. It is used only to verify the HMAC signature on **incoming** webhooks (`verify.webhooks.constructEvent`). - **`client_id` / `client_secret`** — these belong to **Connect with Valyd** (standard OAuth 2.0 / OIDC), the sign-in step of Reusable Verification. They do **not** authenticate verification calls; use the App API key for that. (You hold both credentials, used independently.) So: integrations that only call the Verification API need just the `apiKey`. There is no constructor form that authenticates verify calls without it. ### Resources After initialising `verify`, use these resource namespaces. #### `verify.sessions` - `create(params): Promise` — Create a verification session. Returns `.url` and `.sessionId` — see [Run a verification](/verifications/quickstart). - `retrieve(id): Promise` — Fetch a session by id. - `list({ status?, vendorData?, limit? }): Promise` — List sessions, filterable by status / vendor_data. - `decision(id): Promise` — Authoritative result with `.checks[]` — call this after the webhook. - `updateStatus(id, "APPROVED" | "DECLINED"): Promise` — Manual override (e.g. after agent review). > **Workflows** are composed in the [Developer Portal](https://dev.valyd.vip) — the Node SDK does > not expose workflow CRUD. You pass the resulting `workflowId` to `sessions.create(...)`. #### The Unique Human API The **Unique Human API** is the same `verify.sessions` surface with **no user token**: create a session for a workflow containing the anti-spoof and/or face-uniqueness checks, redirect the person to `session.url`, and read the verdict from `verify.sessions.decision()`. No account is involved, the result returns to you, and nothing is saved to one. See the [Unique Human API](/verifications/unique-human) reference. - `faceUniquenessUnlink(valydUuid)` — GDPR: forget this project's link to a face id (deletes the face entirely when no remaining project or Valyd account knows it). > **ID/KYC, face match, age, professional license, and location run only as workflow checks in > [Reusable Verification](/verifications)** (a verification session for a user who connected with > Valyd), never as their own public APIs. The SDK's other low-level `verify.standalone.*` methods > remain for compatibility and are not part of the public products. Every billable check also accepts an optional `idempotencyKey` — sent as the `Idempotency-Key` header so a network retry can never double-charge or double-run. #### `verify.credentials` - `states(): Promise` — List supported states. - `providers(state): Promise` — List providers (license types) in a state, with `required_fields`. #### `verify.webhooks` - `constructEvent(rawBody, headers, secret?, { toleranceSeconds? }): WebhookEvent` — Verifies the HMAC signature and returns the parsed event. Throws `ValydVerifyError` with code `invalid_signature` on mismatch. - `verify(rawBody, headers, secret?, { toleranceSeconds? }): boolean` — Boolean check, no parse, no throw. Also exported as top-level `constructEvent` / `verify`. When `webhookSecret` is set on the client, the `secret` arg is optional. ### Helpers & types `readImage` and `ImageInput` cover all the ways an image can be supplied: ```typescript import { readImage, type ImageInput } from "@valyd/sdk"; // ImageInput accepted everywhere an image is required: // Buffer | Uint8Array | base64 string | data-URL string const fromFile: ImageInput = readImage("./id_front.jpg"); // reads to base64 const fromBuf: ImageInput = await fs.promises.readFile("./selfie.jpg"); const fromDataUrl: ImageInput = "data:image/jpeg;base64,/9j/4AAQ..."; ``` Every response is strongly typed. Public API uses `camelCase`; wire payloads stay `snake_case`. ```typescript import type { Session, SessionSummary, Decision, Check, CheckEnvelope, KycCredentialResult, Workflow, CredentialState, CredentialProvider, WebhookEvent, } from "@valyd/sdk"; ``` ### Error handling Every failure throws `ValydVerifyError` with `{ code, status?, data? }`. The `code` is either an API code (e.g. `API_KEY_INVALID`, `VALIDATION_ERROR`) or an SDK code: - `network_error` — DNS/socket failure. - `timeout` — exceeded `timeoutMs`. - `invalid_signature` — webhook HMAC mismatch or stale timestamp. - `config_error` — missing `apiKey` / `webhookSecret`. ```javascript import { VerifyClient, ValydVerifyError } from "@valyd/sdk"; const verify = new VerifyClient({ apiKey: process.env.VALYD_API_KEY! }); try { const session = await verify.sessions.create({ workflowId: process.env.VALYD_WORKFLOW_ID!, redirectUrl: "https://yourapp.com/checked", }); } catch (err) { if (err instanceof ValydVerifyError) { console.error(err.code, err.status, err.message, err.data); if (err.code === "API_KEY_INVALID") { /* rotate / refetch */ } } else { throw err; } } ``` ### Quickstarts #### Reusable Verification quickstart ```javascript import { VerifyClient } from "@valyd/sdk"; const verify = new VerifyClient({ apiKey: process.env.VALYD_API_KEY!, webhookSecret: process.env.VALYD_WEBHOOK_SECRET!, }); // 1) Create a session and redirect the user const session = await verify.sessions.create({ workflowId: process.env.VALYD_WORKFLOW_ID!, redirectUrl: "https://app.example.com/verify/callback", callback: "https://api.example.com/webhooks/valyd", vendorData: "user_123", }); // res.redirect(session.url) // 2) In your webhook handler: const event = verify.webhooks.constructEvent(rawBody, headers); // throws on bad signature // 3) Pull the authoritative decision const decision = await verify.sessions.decision(event.sessionId); // decision.status, decision.checks[] ``` **Expected output:** `verify.sessions.create(...)` resolves to a `Session` with `.url` (redirect the user here) and `.sessionId`. After the user finishes, your webhook fires; `constructEvent` returns the parsed `WebhookEvent`, and `verify.sessions.decision(...)` resolves to a `Decision` with `.status` and `.checks[]`. #### Unique Human API quickstart The Unique Human API is API-key-only — a **no-account session** for a workflow containing the liveness and/or uniqueness checks; the person is redirected to Valyd's verification page and nothing is saved to an account. (ID/KYC, face match, age, license, and location run as workflow checks in [Reusable Verification](/verifications) instead.) ```javascript import { VerifyClient } from "@valyd/sdk"; const verify = new VerifyClient({ apiKey: process.env.VALYD_API_KEY }); const session = await verify.sessions.create({ workflowId: process.env.VALYD_WORKFLOW_ID, // liveness and/or uniqueness redirectUrl: "https://yourapp.com/checked", }); // → redirect the person to session.url, then: const decision = await verify.sessions.decision(session.sessionId); // antispoof check data → { human_score, assurance: "captured", ... } // face_uniqueness check data → { valyd_uuid, registered: "new" | "existing" } ``` **Expected output:** the decision's `status` is `"APPROVED"` on a live, unique capture, with the per-check data on `decision.checks[]`. ### Express webhook Use `express.raw()` so the body bytes match what Valyd signed. ```javascript import express from "express"; import { VerifyClient, ValydVerifyError } from "@valyd/sdk"; const app = express(); const verify = new VerifyClient({ apiKey: process.env.VALYD_API_KEY!, webhookSecret: process.env.VALYD_WEBHOOK_SECRET!, }); app.post( "/webhooks/valyd", express.raw({ type: "application/json" }), async (req, res) => { try { const event = verify.webhooks.constructEvent(req.body, req.headers); const decision = await verify.sessions.decision(event.sessionId); await persist(event.vendorData, decision); res.json({ ok: true }); } catch (err) { if (err instanceof ValydVerifyError && err.code === "invalid_signature") { return res.status(400).send("bad signature"); } throw err; } } ); ``` **Expected output:** on a valid signature the handler responds `200` with `{ "ok": true }`; on a bad signature it responds `400` with body `bad signature`. ### Verification - Confirm the SDK is installed: ```bash npm ls @valyd/sdk ``` **Expected output:** `@valyd/sdk` at its latest published version. - Confirm credentials are wired (only needs `VALYD_API_KEY`): ```javascript import { VerifyClient } from "@valyd/sdk"; const verify = new VerifyClient({ apiKey: process.env.VALYD_API_KEY! }); const { states } = await verify.credentials.states(); console.log(states.length); // > 0 means the API key works ``` **Expected output:** a number greater than 0. If it throws `ValydVerifyError` with code `API_KEY_INVALID`, the key is wrong or missing. ### Common errors 1. **`ValydVerifyError` code `config_error`** - **Cause:** `apiKey` (or `webhookSecret` for webhook calls) was not provided to `VerifyClient`. - **Fix:** Set `VALYD_API_KEY` (and `VALYD_WEBHOOK_SECRET` for webhooks) in the environment and pass them to the constructor: `new VerifyClient({ apiKey: process.env.VALYD_API_KEY!, webhookSecret: process.env.VALYD_WEBHOOK_SECRET! })`. 2. **`ValydVerifyError` code `invalid_signature` in the webhook handler** - **Cause:** webhook HMAC mismatch or stale timestamp — most often because the request body was parsed/re-serialized before signature verification, so the bytes no longer match what Valyd signed. - **Fix:** Mount the webhook route with `express.raw({ type: "application/json" })` so `req.body` is the exact raw bytes, and make sure the `webhookSecret` matches the one in the dashboard. 3. **`ValydVerifyError` code `timeout`** - **Cause:** the request exceeded `timeoutMs` (default `15000`). Credential lookups can be slow. - **Fix:** Increase the per-request timeout for credential lookups (10–60s), e.g. `new VerifyClient({ apiKey: process.env.VALYD_API_KEY!, timeoutMs: 90_000 })`. ================================================================================ === FILE: https://docs.valyd.vip/verify/session-lifecycle.md === ================================================================================ > Source: https://docs.valyd.vip/verify/session-lifecycle # Session lifecycle A verification session is one person's single run through a [workflow](/verifications/workflows)'s checks on Valyd's verification page. It moves through a small state machine that always ends in a terminal decision. This page is the lifecycle over time; the exact status values and how to act on each live in [Decisions & statuses](/verifications/statuses), and the end-to-end integration is in [Run a verification](/verifications/quickstart). ## The states over time ```mermaid stateDiagram-v2 [*] --> NOT_STARTED: create session NOT_STARTED --> IN_PROGRESS: user opens the verification page IN_PROGRESS --> IN_REVIEW: needs human / async review IN_PROGRESS --> APPROVED IN_PROGRESS --> DECLINED IN_PROGRESS --> ABANDONED: user left IN_PROGRESS --> EXPIRED: TTL elapsed IN_REVIEW --> APPROVED IN_REVIEW --> DECLINED APPROVED --> [*] DECLINED --> [*] ABANDONED --> [*] EXPIRED --> [*] ``` 1. **Created (`NOT_STARTED`).** Your backend calls `verify.sessions.create` with a `workflowId`. The response carries the verification page `url`, a `sessionToken`, and `expiresAt`. Tag it with `vendorData` (your internal user ref, echoed back on the webhook) and bound its lifetime with `ttlSeconds`. 2. **In progress (`IN_PROGRESS` → optionally `IN_REVIEW`).** The user opens the `url` and completes the workflow's checks. A session that needs manual or async review passes through `IN_REVIEW` first; otherwise it goes straight to a terminal state. 3. **Terminal decision (`APPROVED` · `DECLINED` · `ABANDONED` · `EXPIRED`).** The lifecycle ends. Terminal is terminal: an abandoned or expired session is **never** resumed — create a new one. ## Two signals, one authority When the user finishes, Valyd sends two things — treat only one as authoritative: - **The redirect `?status=` is a hint.** The user's browser returns to your `redirect_url` with `?session_id=…&status=…`. Never grant access on that query param — it is a UI hint only. - **The decision call is authoritative.** Read the real outcome from `verify.sessions.decision(id)`, which carries the session status plus the per-check breakdown. See [Run a verification, step 4](/verifications/quickstart). - **A signed webhook fires on the terminal state.** Valyd POSTs to your `callback` with the event and decision. The webhook is a notification — still call the decision API for the full per-check detail. See [Webhooks](/verifications/webhooks). ## What to do at each stage ```text IF status is NOT_STARTED or IN_PROGRESS: → wait; the run is not finished. Keep the session pending. IF status is IN_REVIEW: → wait for the review outcome; do not grant access yet. IF status is APPROVED: → fetch the decision, then grant access / complete onboarding. IF status is DECLINED: → fetch the decision to see which checks failed; deny and offer a retry if your policy allows. IF status is ABANDONED or EXPIRED: → treat as not verified; create a new session if the user still needs to verify. ``` ## Related - [Decisions & statuses](/verifications/statuses) — every status value and the per-check statuses. - [Run a verification](/verifications/quickstart) — creating the session and reading the decision. - [Webhooks](/verifications/webhooks) — the signed terminal-state notification. ================================================================================ === FILE: https://docs.valyd.vip/verify/setup.md === ================================================================================ > Source: https://docs.valyd.vip/verify/setup # Create a workflow > 🔑 **What you leave with:** an App **API key** (shown once), a `workflowId`, and a webhook secret Portal setup for [Reusable Verification](/verifications): where verification lives in the [Developer Portal](https://dev.valyd.vip), and the three things to grab before your first session. Don't have an account yet? [Create one first](/docs/create-account) — it takes a minute and comes with a $100 welcome credit. ## Where verification lives in the portal Verification is configured **inside your app** — the same app whose OIDC credentials power [Connect with Valyd](/docs/authentication). Open the app → its **Verification** tab: ![The Verification tab inside an app in the Developer Portal](/images/screenshots/portal-app-verification.png) (Building only with the [Unique Human API](/verifications/unique-human)? No project needed — every organization has a built-in **Unique Human API** key on the dashboard for no-account anti-spoof checks; see that page's setup section.) ## Get your API key (shown once) When you open the app's **Verification** tab, the portal shows the **API key exactly once** in a show-once panel. Copy it immediately and store it server-side — it cannot be retrieved again. If you lose it, rotate it in the portal to mint a new one. Never put the key in browser code: it belongs in your backend's environment, passed to the SDK client on server-to-server calls only. ## Build the workflow A [workflow](/verifications/workflows) is the saved bundle of checks your app requires — KYC, professional license, face match, liveness, location, and more. Build one with the workflow wizard and copy its `workflowId`: ![The workflow creation wizard in the Developer Portal](/images/screenshots/portal-workflow-wizard.png) ## Configure webhooks Set your endpoint URL and copy the signing secret under **Webhooks**. Valyd POSTs signed events there when a session finishes — [Webhooks](/verifications/webhooks) covers verification of the signature. The portal includes a **webhook tester** so you can fire test events at your endpoint before going live: ![The webhook tester in the Developer Portal](/images/screenshots/portal-webhook-tester.png) ## Next - [Run a verification](/verifications/quickstart) — first session with the key and `workflowId` you just copied. - [Workflows](/verifications/workflows) — presets, bundling checks, and changing a workflow. - [Checks reference](/verifications/types) — everything a workflow can verify. ================================================================================ === FILE: https://docs.valyd.vip/verify/statuses.md === ================================================================================ > Source: https://docs.valyd.vip/verify/statuses # Results & decisions **The decision is the authoritative outcome — never trust redirect query params as final.** The `?status=` your `redirect_url` receives is a hint only; confirm every result via the signed [webhook](/verifications/webhooks) or `verify.sessions.decision(id)` ([Run a verification, step 4](/verifications/quickstart)). ## Session status Lifecycle transitions: ```mermaid stateDiagram-v2 [*] --> NOT_STARTED NOT_STARTED --> IN_PROGRESS IN_PROGRESS --> IN_REVIEW IN_PROGRESS --> APPROVED IN_PROGRESS --> DECLINED IN_PROGRESS --> ABANDONED IN_PROGRESS --> EXPIRED IN_REVIEW --> APPROVED IN_REVIEW --> DECLINED ``` `IN_REVIEW` is optional: a session may go directly from `IN_PROGRESS` to a terminal state, or pass through `IN_REVIEW` first. | Status | Meaning | |---------------|------------------------------------------------------| | `NOT_STARTED` | Session created, user not yet on the verification page | | `IN_PROGRESS` | User is interacting with the flow | | `IN_REVIEW` | Awaiting human / async review | | `APPROVED` | All checks passed (or manually approved) | | `DECLINED` | Checks failed (or manually declined) | | `ABANDONED` | User left before completing | | `EXPIRED` | TTL elapsed before completion | Terminal states (a webhook is sent and the lifecycle ends): `APPROVED`, `DECLINED`, `ABANDONED`, `EXPIRED`. Non-terminal states (still in flight): `NOT_STARTED`, `IN_PROGRESS`, `IN_REVIEW`. **Unknown values:** new statuses may be added over time ([versioning](/verifications/versioning)) — treat unknown verification states as **not approved** until your application explicitly supports them. ### Manual review decisions An `IN_REVIEW` session can be resolved by a reviewer on your side via `verify.sessions.updateStatus(id, "APPROVED" | "DECLINED")`. This records your **business decision** on the session — it does not change what the individual checks proved; the per-check results are preserved in the decision. Only `IN_REVIEW` sessions can be manually decided, and a manual `APPROVED` still requires the session's ID verification, liveness, and face-match checks to have passed. ### Decision tree — how to act on a session status ```text IF status == NOT_STARTED: → do nothing yet; wait for the user to open the verification page. Keep the session pending. IF status == IN_PROGRESS: → do nothing yet; the user is mid-flow. Keep the session pending. IF status == IN_REVIEW: → do nothing yet; await the review outcome. The session will move to APPROVED or DECLINED. Do not grant access. IF status == APPROVED: → call verify.sessions.decision(id) for the full extracted data, then grant access / complete onboarding. IF status == DECLINED: → call verify.sessions.decision(id) to see which checks failed; deny access and surface a retry path if your policy allows. IF status == ABANDONED: → treat as not verified; prompt the user to restart verification (create a new session). IF status == EXPIRED: → treat as not verified; the session TTL elapsed. Create a new session if the user still needs to verify. IF unsure of current state: → call verify.sessions.retrieve(id) to read the current status. ``` ## Check status Each individual check within a session reports one of three values: | Check status | Meaning | |--------------|------------------------------------------| | `passed` | check succeeded | | `failed` | check failed | | `review` | inconclusive; needs human or async review| ### Decision tree — how to act on a check status ```text IF check == passed: → this check is satisfied. If all checks are passed, the session moves toward APPROVED. IF check == failed: → this check did not succeed. It typically drives the session toward DECLINED; inspect the decision for the failure reason. IF check == review: → inconclusive. The session typically sits in IN_REVIEW until a human or async process resolves it. Do not grant access on this check yet. IF unsure: → call verify.sessions.decision(id) to read per-check statuses. ``` Relationship between check status and session status: - All checks `passed` → session typically `APPROVED`. - Any check `failed` → session typically `DECLINED`. - Any check `review` (and none failed) → session typically `IN_REVIEW` until resolved. ## Reading the decision The webhook is a notification; the **decision call** holds the authoritative result and the full per-check breakdown. Pull it with `verify.sessions.decision(id)` ([Run a verification](/verifications/quickstart); [webhooks](/verifications/webhooks)): ```text IF you received a webhook: → it carries event.status and event.decision; still call verify.sessions.decision(id) for the full check breakdown. IF you want to pull the result yourself: → call verify.sessions.decision(sessionId). Then read d.status: IF d.status == "APPROVED": → verification succeeded (see the per-check rule below for KYC + License). IF d.status == "DECLINED": → verification failed; inspect d.checks for the failing check's error. IF d.status == "IN_REVIEW": → a human/manual review is pending; wait for a terminal webhook or poll again. Per check in d.checks (each: { type, status, score, data, error }): IF check.status == "passed": → this check succeeded. IF check.status == "failed": → read check.error?.message (e.g. "License belongs to a different name"). IF check.status == "review": → this check is awaiting manual review. IF check.status == "pending" or "running": → not finished yet. ``` ```javascript const d = await verify.sessions.decision(sessionId); // d.status → session progress: "APPROVED" | "DECLINED" | "IN_REVIEW" (may still be pending) // d.decision → final business outcome, resolved only: "APPROVED" | "DECLINED" (never IN_REVIEW) // d.checks → [{ type, status, score, data, error }] // d.decided_at → ISO timestamp const credential = d.checks.find(c => c.type === "credential"); if (credential.status === "failed") { console.log(credential.error?.message); // e.g. "License belongs to a different name" } ``` **Mode note:** the decision below illustrates a [Reusable Verification](/verifications) session (created **with** a `valyd_access_token`). With the user's token the decision carries what passed, proofs, and public data — the raw PII stays encrypted on their account and never reaches your server. ```json { "status": "APPROVED", "decision": "APPROVED", "decided_at": "2026-06-11T12:05:00Z", "checks": [ { "type": "id_verification", "status": "passed", "score": 0.97, "data": { /* fields, portrait, … */ } }, { "type": "liveness", "status": "passed", "score": 1.00, "data": { "live_score": 1 } }, { "type": "face_match", "status": "passed", "score": 0.97, "data": { "similarity": 0.97, "threshold": 0.95 } }, { "type": "credential", "status": "passed", "score": 1.00, "data": { "match": true, "license": { "status": "active", "expires_at": "2027-01-01" } } } ] } ``` APPROVED for KYC + License means ALL of: - The ID was verified (OCR + authenticity). - The selfie was live. - The selfie matches the ID portrait. - The license exists AND belongs to the person on the ID. ================================================================================ === FILE: https://docs.valyd.vip/verify/types.md === ================================================================================ > Source: https://docs.valyd.vip/verify/types # Checks reference > 🔑 **Auth:** SDK client (App API key) · 💾 Run in a workflow with the user's `valyd_access_token` and the passed proof saves to their Valyd ID Every check Valyd can run, in one place: what it verifies, what the user provides, what comes back, and where it's available — inside a **[Reusable Verification](/verifications)** [workflow](/verifications/workflows) (a connected user's session, where every check runs) and/or as a direct **[Unique Human API](/verifications/unique-human)** call (API key only, no user login). ## ID verification — `id_verification` Verifies a government ID: OCR of the document fields plus an authenticity score. The user provides the front (and optionally back) image of the ID. You get back a proof that the document was verified; the raw extracted fields stay encrypted on the account. Use it whenever you need to know who a document says someone is. **Available:** [Reusable Verification](/verifications) workflow ## Liveness — `liveness` Passive liveness on a single selfie: is this a real, live capture? Use it as the cheap first gate before a face match. **Available:** [Reusable Verification](/verifications) workflow ## Anti-spoof — `antispoof` Stronger "is this a live human capture?" answer with a vendor-neutral `human_score` (0–100). The user provides a single image (score capped at 85) or a 3–8 frame burst captured over ~2s, which adds motion and same-person consistency analysis. A workflow session captures a live camera burst with a random on-screen action for the strongest assurance (`assurance: "captured"`). Use it when presentation attacks (photos of photos, replays) are a real threat. **Available:** [Unique Human API — Liveness](/verifications/unique-human/antispoof) (no-account workflow session) · [Reusable Verification](/verifications) workflow ## Anti-spoof + identity — `antispoof/identity` Runs the identical anti-spoof pipeline and, only if it passes, resolves the proven-live face to a stable `valyd_` uuid from the global face gallery. You get the `human_score` plus `identity: { valyd_uuid, registered }` — the same face resolves to the same uuid whenever the gallery match clears its similarity threshold. Use it for duplicate-account / sybil detection with liveness assurance built in. **Available:** [Unique Human API](/verifications/unique-human/face-uniqueness) (no-account workflow session) · [Reusable Verification](/verifications) workflow ## Face uniqueness — `face-uniqueness` One face = one Valyd uuid. Enrolls or matches a selfie against the global gallery and returns the stable `valyd_uuid` plus whether it was newly registered. Use it to stop one person opening many accounts. **Available:** [Unique Human API — Uniqueness](/verifications/unique-human/face-uniqueness) (no-account workflow session) · [Reusable Verification](/verifications) workflow ## Face match — `face_match` 1:1 comparison of two face images — typically the ID portrait against a fresh selfie. You get `similarity` and the pass `threshold` (default ~0.95). Use it to bind a live person to a verified document. **Available:** [Reusable Verification](/verifications) workflow ## Age verification — `age` Computes age bands from a verified date of birth. With a `valyd_access_token` the bands are computed from the account's KYC-verified DOB and returned as a proof. Use it for age-gated products. **Available:** [Reusable Verification](/verifications) workflow ## License / credential verification — `credential` Looks up a professional license in the provider registry and matches it to a name. In a workflow session the name comes from the verified ID (never client-supplied); you get `match` and the registry's `license` record (status, expiry, specialty). Registry lookups can take 10–60 s. Use it to verify doctors, nurses, and other licensed professionals. **Available:** [Reusable Verification](/verifications) workflow ## KYC + credential — combined ID verification + liveness + face match + license lookup in one workflow session. The license is matched against the name OCR'd from the ID — never a client-supplied name — so a caller cannot substitute an arbitrary name to claim someone else's license. You get a per-check breakdown plus the verified `identity` proof. `status` is `"passed"` only when every check passes. **Available:** [Reusable Verification](/verifications) workflow ## Location — `location` Records and validates a geolocation fix for a session. Used by workflows like EVV (electronic visit verification) to prove where a check happened. **Available:** [Reusable Verification](/verifications) workflow --- > **Biometrics are irreversible vectors, never images.** Valyd does not store or return face > images. Enrollment converts a selfie into a one-way biometric vector (template); every later > face match compares vectors. The photos you submit to a check are processed transiently for > that check and are not retrievable from a Valyd account. The template is never exposed through > any API, and the ID `portrait` in a KYC result is extracted from the document you submitted in > that request — not a stored account photo. [Full scoping →](/docs/data-and-trust) Next: bundle checks into a [workflow](/verifications/workflows), or — for the direct liveness and uniqueness calls — see the [Unique Human API](/verifications/unique-human). ================================================================================ === FILE: https://docs.valyd.vip/verify/unique-human/antispoof.mdx.md === ================================================================================ > Source: https://docs.valyd.vip/verify/unique-human/antispoof.mdx --- product: valyd-verify billable: true pii_mode: standalone human_setup_required: true source_of_truth: sdk --- # Liveness Detects presentation and spoof attacks (printouts, screens, replays, masks) — the [Unique Human API](/verifications/unique-human)'s anti-spoof check. > 🔑 **Auth:** App API key + a workflow with the anti-spoof check · 👤 **User account:** none · 💾 **Result:** returned to your system, nothing saved ## How it works You create a session for a workflow containing the anti-spoof check and redirect the person to Valyd's verification page. The page captures a **live camera burst with a random on-screen action** — the strongest assurance level (`assurance: "captured"`), with per-frame voting, motion analysis, and same-person consistency. The check answers "is this a live human capture?" with a vendor-neutral `human_score` (0–100) and a pass/fail verdict. No account is involved and nothing is stored. ## Run it ```javascript import { VerifyClient } from "@valyd/sdk"; const verify = new VerifyClient({ apiKey: process.env.VALYD_API_KEY }); const session = await verify.sessions.create({ workflowId: process.env.VALYD_WORKFLOW_ID, // a workflow with the anti-spoof check redirectUrl: "https://yourapp.com/checked", vendorData: "user-123", }); // → redirect the person's browser to session.url ``` ## Result Read the decision (or receive it on a signed [webhook](/verifications/webhooks)): ```javascript const decision = await verify.sessions.decision(session.sessionId); // decision.status: "APPROVED" | "DECLINED" | "IN_REVIEW" // the antispoof check's data: // { // assurance: "captured", // live camera burst with a random on-screen action // frames_analyzed: 5, // frames_genuine: 5, // frames_spoof: 0, // motion: "natural", // face_consistency: "consistent", // human_score: 100 // } ``` On a failure the check data carries a `signal` field, one of: `no_face`, `face_unreadable`, `spoof_detected`, `low_confidence`, `duplicate_frames`, `static_capture`, `discontinuous_motion`, `different_faces`. Nothing is written to any Valyd account — the verdict is yours to act on. ## Next - **[Uniqueness](/verifications/unique-human/face-uniqueness)** — add "have we seen this face before?" to the same workflow. - Liveness bound to a connected user's account runs inside a [Reusable Verification](/verifications) workflow — see the [checks reference](/verifications/types). ================================================================================ === FILE: https://docs.valyd.vip/verify/unique-human/errors.md === ================================================================================ > Source: https://docs.valyd.vip/verify/unique-human/errors # Common errors These are the errors you hit on the **Unique Human API**. The SDK throws a `ValydVerifyError` carrying `{ code, status, message }`; catch it and read those fields: ```javascript import { VerifyClient, ValydVerifyError } from "@valyd/sdk"; const verify = new VerifyClient({ apiKey: process.env.VALYD_API_KEY }); try { const session = await verify.sessions.create({ workflowId: process.env.VALYD_WORKFLOW_ID, // liveness and/or uniqueness redirectUrl: "https://yourapp.com/checked", }); } catch (err) { if (err instanceof ValydVerifyError) { console.error(err.status, err.code, err.message); } else { throw err; } } ``` ## Frequent failures and fixes 1. **Invalid or missing API key.** - Cause: the `apiKey` passed to `VerifyClient` is absent, wrong, or revoked. - Fix: set `apiKey` on the client from a valid key. Obtain one from the Developer Portal — open a project's **Verification** tab, or use your organization's built-in **Unique Human API** key on the dashboard: https://dev.valyd.vip 2. **Missing or invalid `workflowId`.** - Cause: the session was created without a real workflow id (`VALIDATION_ERROR`). - Fix: build a workflow with the liveness / uniqueness checks in the Developer Portal and pass its `workflowId`. 3. **Rate limited.** - Cause: too many calls in a short window. - Fix: back off and retry with jitter. A spoof verdict is not an error — the session completes and the decision's antispoof check data carries the failure `signal`. See [Liveness](/verifications/unique-human/antispoof) for the signals. --- **Need ID/KYC, face match, age, professional license, or location?** Those run as workflow checks in [Reusable Verification](/verifications) on a connected user's verification session, where the decision returns with a status (`APPROVED` / `DECLINED` / `IN_REVIEW`) rather than throwing. See also [Run a verification](/verifications/quickstart) · [Consent & data access](/verifications/data-sharing). ================================================================================ === FILE: https://docs.valyd.vip/verify/unique-human/face-uniqueness.mdx.md === ================================================================================ > Source: https://docs.valyd.vip/verify/unique-human/face-uniqueness.mdx --- product: valyd-verify billable: true pii_mode: standalone human_setup_required: true source_of_truth: sdk --- # Uniqueness One face = one identity — matches the captured face against the Valyd face registry for sybil and duplicate-account detection, as part of the [Unique Human API](/verifications/unique-human). > 🔑 **Auth:** App API key + a workflow with the face-uniqueness check · 👤 **User account:** none · 💾 **Result:** returned to your system; the face maps to a stable `valyd_uuid` ## How it works You create a session for a workflow containing the face-uniqueness check and redirect the person to Valyd's verification page. The page captures a live burst, enrolls or matches the face against the registry, and the decision returns a stable `valyd_uuid` plus whether it was newly registered. The same face resolves to the same uuid on every run, so you can detect a person opening a second account. The biometric is stored as an irreversible vector encrypted with Valyd — never as an image — and is never exposed through any API. ## Run it ```javascript import { VerifyClient } from "@valyd/sdk"; const verify = new VerifyClient({ apiKey: process.env.VALYD_API_KEY }); const session = await verify.sessions.create({ workflowId: process.env.VALYD_WORKFLOW_ID, // a workflow with the face-uniqueness check redirectUrl: "https://yourapp.com/checked", vendorData: "user-123", }); // → redirect the person's browser to session.url ``` Pair it with [Liveness](/verifications/unique-human/antispoof) in the same workflow so only a proven-live face is matched against the registry. ## Result ```javascript const decision = await verify.sessions.decision(session.sessionId); // decision.status: "APPROVED" | "DECLINED" | "IN_REVIEW" // the face_uniqueness check's data: // { valyd_uuid: "valyd_8f2…", registered: "existing" } // registered: "new" the first time a face is seen, "existing" afterwards ``` Store the `valyd_uuid` against your own user record. A later session that resolves to an already seen uuid is the same human — your policy decides what happens next. ## Deleting a face (GDPR) To unlink a face your project no longer needs (right to erasure), remove it from the unique-face registry by its `valyd_` uuid from the project's **Verification** settings in the [Developer Portal](https://dev.valyd.vip). The face is unlinked from your project, and fully deleted once no remaining project — and no Valyd account — still knows it. ## Next - **[Liveness](/verifications/unique-human/antispoof)** — the anti-spoof half of the Unique Human API. - Uniqueness bound to a connected user's account runs inside a [Reusable Verification](/verifications) workflow — see the [checks reference](/verifications/types). ================================================================================ === FILE: https://docs.valyd.vip/verify/unique-human.md === ================================================================================ > Source: https://docs.valyd.vip/verify/unique-human --- product: valyd-verify billable: true pii_mode: standalone human_setup_required: true source_of_truth: sdk --- # Unique Human API > 🔑 **Auth:** App API key (server-side) + a `workflowId` · 👤 **User account:** none · 💾 **Result:** returned to your system, nothing saved to an account Determine whether you're interacting with a **live, unique human** using your Valyd API credentials. No user account and no OIDC: you create a session for a workflow that contains the **Liveness** and/or **Uniqueness** checks, redirect the person to **Valyd's verification page** (Valyd handles the camera, capture, retries, and security), and read the verdict when they're done. The result returns to your system — nothing is saved to any Valyd account. ## The flow 1. In the [Developer Portal](https://dev.valyd.vip) create a **project** and open its **Verification** tab. Build a **workflow** with the checks you need — [Liveness](/verifications/unique-human/antispoof), [Uniqueness](/verifications/unique-human/face-uniqueness), or both — then copy the project's API key (shown once) and the `workflowId`. (For a quick, no-account anti-spoof check, every organization also has a built-in **Unique Human API** key on the dashboard.) 2. Create a session from your backend — **no user token**: ```javascript import { VerifyClient } from "@valyd/sdk"; const verify = new VerifyClient({ apiKey: process.env.VALYD_API_KEY }); const session = await verify.sessions.create({ workflowId: process.env.VALYD_WORKFLOW_ID, // liveness and/or uniqueness redirectUrl: "https://yourapp.com/checked", vendorData: "user-123", // your internal ref }); // → redirect the person's browser to session.url ``` 3. Valyd's page captures a live camera burst (with a random on-screen action for the strongest assurance) and sends the person back to your `redirectUrl`. 4. Read the verdict — signed [webhook](/verifications/webhooks) or the decision call: ```javascript const decision = await verify.sessions.decision(session.sessionId); // decision.status: "APPROVED" | "DECLINED" | "IN_REVIEW" // decision.checks[] — per-check data: // antispoof → { human_score: 100, assurance: "captured", ... } // face_uniqueness → { valyd_uuid: "valyd_8f2…", registered: "new" | "existing" } ``` Because the session carries no user token, nothing is saved to a Valyd account — the verdict is yours to act on. ## The two checks - **[Liveness](/verifications/unique-human/antispoof)** — is this a live human in front of the camera, not a printout, screen replay, or mask? - **[Uniqueness](/verifications/unique-human/face-uniqueness)** — one face = one identity: the same face always resolves to the same stable `valyd_uuid`, so a person opening a second account is caught. ## Setup Portal sign-in works with an **email magic link** as well as a Valyd ID. Create a **project** from the dashboard, open its **Verification** tab, copy its API key into `VALYD_API_KEY` (server-side only — never in browser code), build the workflow, and copy its `workflowId`. --- Need reusable identity or verified credentials — KYC, licenses, face match bound to a user? See **[Reusable Verification](/verifications)**. ================================================================================ === FILE: https://docs.valyd.vip/verify/versioning.md === ================================================================================ > Source: https://docs.valyd.vip/verify/versioning # Versioning & deprecation Verification is consumed through the `@valyd/sdk` package — you pin the **SDK version**, and the SDK tracks the current verification contract for you. ## What is NOT a breaking change (may ship any time) These are **additive** and safe — your integration must tolerate them: - Adding a new SDK method, a new optional parameter, or a new workflow feature/check. - Adding a new field to a response object (e.g. a new key inside `check.data`). - Adding a new enum value (a new `check.status`, a new failure `signal`, a new event `type`). - Making a previously-required field optional. - Bug fixes and performance changes that don't alter the documented contract. **Build defensively:** ignore unknown response fields, and don't hard-fail on an unrecognized enum value — treat unknown verification states as **not approved** until your application explicitly supports them. ## What IS a breaking change (gets a new major SDK version) - Removing or renaming an SDK method, parameter, or response field. - Changing a field's type or the shape of a response. - Making an optional parameter required, or tightening validation. - Removing an enum value, or changing the meaning of an existing one. - Changing authentication or error semantics. Breaking changes are **never** made to a released SDK major in place. They ship under a new major version; the pin you already have keeps working. ## Deprecation policy When a method or field is deprecated: 1. It is announced in the [Changelog](/docs/changelog) and marked deprecated in this documentation. 2. It keeps working for a **minimum 6-month** migration window after the announcement. 3. The SDK surfaces a deprecation notice pointing at the replacement. You never have to migrate on our schedule inside a major version — pin the SDK (see below) and you are stable until we announce that major's deprecation with the window above. ## Recommended practices - Pin the SDK with a caret range — `npm i @valyd/sdk` — so you get compatible patch and minor updates without an unexpected major bump. - Subscribe to the [Changelog](/docs/changelog) for additive changes and any deprecation notices. - Handle unknown enum values and extra fields gracefully (see "build defensively" above). ================================================================================ === FILE: https://docs.valyd.vip/verify/webhooks.md === ================================================================================ > Source: https://docs.valyd.vip/verify/webhooks # Webhooks > **Try it without a real session:** the Developer Portal's **Webhook Tester** (app → webhook > settings) sends simulated decision events — signed with your real secret, marked > `"test": true` — plus duplicate-event and bad-signature modes to prove your handler's > defenses. See [Testing](/docs/testing#testing-webhooks). ## Recipe: register a callback and verify webhook signatures ### Prerequisites - A publicly reachable HTTPS URL for your webhook handler. - A webhook signing secret. Configure the callback URL and obtain the signing secret in the Valyd Developer Portal (https://dev.valyd.vip). Store it as `VALYD_WEBHOOK_SECRET`. - An App API key stored as `VALYD_API_KEY`, used to fetch the full decision after a webhook arrives. ### Steps 1. **Register your callback URL.** In the Developer Portal (https://dev.valyd.vip) set the app-level callback URL, or pass a per-session callback URL when creating the session. Note the webhook signing secret shown there. **Expected output:** The console shows your callback URL saved and a signing secret. There is no HTTP response to capture for this step — it is a portal action. 2. **Store the secrets in your environment.** ```bash # .env VALYD_WEBHOOK_SECRET=whsec_... # the webhook signing secret from the Developer Portal (https://dev.valyd.vip) VALYD_API_KEY=... # your App API key from the Developer Portal (https://dev.valyd.vip) ``` **Expected output:** Both variables are available to your process via `process.env`. 3. **Receive the POST and verify the signature using the RAW body.** Valyd sends these headers on every webhook request: - `X-Valyd-Timestamp` — unix seconds when the event was sent - `X-Valyd-Event-Id` — unique event id (use for idempotency) - `X-Valyd-Signature` — lowercase hex HMAC-SHA256 Compute `HMAC_SHA256("{timestamp}.{rawBody}", webhookSigningSecret)` and compare it in constant time against the `X-Valyd-Signature` header. Reject the request on any mismatch. ```javascript import crypto from "crypto"; import express from "express"; const app = express(); // IMPORTANT: capture the RAW body for HMAC verification app.post( "/api/valyd-webhook", express.raw({ type: "application/json" }), (req, res) => { const ts = req.header("X-Valyd-Timestamp"); const sig = req.header("X-Valyd-Signature") || ""; const raw = req.body; // Buffer const expected = crypto .createHmac("sha256", process.env.VALYD_WEBHOOK_SECRET) .update(`${ts}.${raw.toString("utf8")}`) .digest("hex"); const ok = sig.length === expected.length && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig)); if (!ok) return res.status(400).send("bad signature"); // Replay protection: reject deliveries older than 5 minutes. `X-Valyd-Timestamp` // is signed into the HMAC above, so it can't be tampered with. if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) { return res.status(400).send("stale timestamp"); } const event = JSON.parse(raw.toString("utf8")); // event = { event_id, type: "verification.approved" | ..., session_id, status, vendor_data, decision, occurred_at } // Respond fast; then fetch the full decision asynchronously. res.status(200).json({ ok: true }); } ); ``` **Expected output:** On a valid signature your handler returns HTTP 200. On a bad signature it returns HTTP 400 with body `bad signature`. **Verify with the Node SDK (instead of hand-rolling HMAC).** `@valyd/sdk` does the raw-body signature check, the staleness check, and the JSON parse for you — pass the raw body and headers to `verify.webhooks.constructEvent`, which throws `ValydVerifyError` with `code === "invalid_signature"` on a bad signature. Still use the raw body (`express.raw`): ```javascript import express from "express"; import { VerifyClient, ValydVerifyError } from "@valyd/sdk"; const verify = new VerifyClient({ apiKey: process.env.VALYD_API_KEY, webhookSecret: process.env.VALYD_WEBHOOK_SECRET, }); app.post( "/api/valyd-webhook", express.raw({ type: "application/json" }), async (req, res) => { try { const event = verify.webhooks.constructEvent(req.body, req.headers); // event.sessionId, event.type, event.status, event.decision, event.vendorData await persistDecision(event); res.json({ ok: true }); } catch (err) { if (err instanceof ValydVerifyError && err.code === "invalid_signature") { return res.status(400).send("bad signature"); } throw err; } } ); ``` 4. **Fetch the full decision after acknowledging the webhook.** The webhook body is a notification; `verify.sessions.decision(id)` (session id from the event) returns the full per-check breakdown — proofs + public data on sessions created with the user's `valyd_access_token`. ```javascript const decision = await verify.sessions.decision(event.sessionId); // decision.status, decision.checks[] ``` **Expected output:** a `Decision` with the full per-check data for that session. ### Event body The decoded JSON event has this shape: ```json { "event_id": "evt_...", "type": "verification.approved", "session_id": "ses_...", "status": "APPROVED", "vendor_data": "user-123", "decision": "approved", "occurred_at": "2025-06-05T11:42:13Z" } ``` Field notes: - `event_id` — matches the `X-Valyd-Event-Id` header; use it to deduplicate retried deliveries **and manual resends** (both carry the same id). - `type` — the event type. The full set is: - `verification.approved` — terminal, passed. - `verification.declined` — terminal, failed. - `verification.in_review` — a manual/agent review is pending (not yet terminal). - `verification.abandoned` — the user left the verification page without finishing. - `verification.expired` — the session's TTL elapsed before completion. - `session_id` — the session that triggered the event; pass it to `verify.sessions.decision(id)`. - `status` — the session status (e.g. `APPROVED`). See [Results & decisions](/verifications/statuses) for every possible value. - `decision` — the final business outcome string (`approved` / `declined`). ### Delivery and retries - Your endpoint must return a 2xx status and respond fast — defer heavy work to a background queue. - Non-2xx (or timed-out) deliveries are **retried automatically**. Because the same event may arrive more than once, treat delivery as **at-least-once** and **deduplicate on `X-Valyd-Event-Id`**. - Verify the HMAC signature and reject stale timestamps (> 5 minutes) on every delivery. #### Retry schedule A delivery is attempted up to **10 times**. After a failed attempt Valyd waits, then retries: | After attempt | Wait before next try | |---|---| | 1 | 5 seconds | | 2 | 30 seconds | | 3 | 2 minutes | | 4 | 10 minutes | | 5 and later | 30 minutes | So a persistently failing endpoint is retried across roughly **2.5 hours** before Valyd stops. Every retry carries the **same** `X-Valyd-Event-Id`, so deduplicating on that header collapses all retries of one event into a single unit of work. #### Delivery log and manual resend (Developer Portal) Every attempt — successful or failed — is recorded in the **Developer Portal** (https://dev.valyd.vip), on your application's **Verification** page under **Recent webhook deliveries**. For each attempt you can see the destination URL, the exact payload and headers we sent, the receiver's HTTP status and response body, and any transport error. Use the **Resend** button next to a delivery to re-queue that webhook — for example after you've fixed a bug in your handler, or if an endpoint was down during the automatic retry window. A manual resend carries the **same** `X-Valyd-Event-Id` as the original, so an idempotent handler treats it as the same event rather than a new one. > If a webhook is ever missed entirely, `verify.sessions.decision(id)` is the authoritative source > of the result — call it any time. ### Verification - Send a test webhook (or trigger a real terminal session) and confirm your handler logs a valid signature and returns HTTP 200. - Manually corrupt the secret and confirm your handler returns HTTP 400 `bad signature` — this proves verification is actually running. ### Common errors 1. **Signature always mismatches.** - **Cause:** The framework parsed and re-serialized the JSON body, so the bytes used for HMAC differ from what Valyd signed. - **Fix:** Capture the raw body (e.g. `express.raw({ type: "application/json" })`) and run HMAC over the exact raw bytes before any JSON parsing. 2. **Signature mismatch despite using the raw body.** - **Cause:** Wrong signing input or secret — the signed string must be `"{timestamp}.{rawBody}"` (timestamp, a literal dot, then the raw body), and the secret must be the webhook signing secret, not the App API key. - **Fix:** Build the HMAC input as `${ts}.${raw.toString("utf8")}` and use `VALYD_WEBHOOK_SECRET`. Compare in constant time against the lowercase hex `X-Valyd-Signature`. 3. **Duplicate processing / webhook retried.** - **Cause:** Your endpoint returned non-2xx (or timed out), so Valyd retried with exponential backoff. - **Fix:** Return 2xx immediately and process asynchronously; deduplicate on `X-Valyd-Event-Id`. ================================================================================ === FILE: https://docs.valyd.vip/verify/workflows.md === ================================================================================ > Source: https://docs.valyd.vip/verify/workflows # Workflows > 🔑 **Configured in:** the [Developer Portal](https://dev.valyd.vip) · 🧩 **Used by:** verification sessions (`workflowId`) A **workflow** defines what Valyd needs to verify for your application — a reusable bundle of checks. You define it once in the Developer Portal and reference its `workflowId` every time you create a session through the SDK. Valyd's verification page auto-adapts its steps to the workflow's checks, and all results come back together in one decision. ## Example: the "KYC + License" workflow The workflow's `features` are `[id_verification, liveness, face_match, credential]`, and the user's session walks the chain: Because the license is matched against the name OCR'd from the verified ID, the user never types a name and cannot present someone else's license. ![The workflow wizard in the Developer Portal](/images/screenshots/portal-workflow-wizard.png) ## Creating a workflow Workflows are created and edited **in the Developer Portal** (https://dev.valyd.vip) → **Workflows** — there is no workflow-CRUD SDK method. Create a workflow from a preset, then copy its `workflowId`. There are two presets, and both use the **same integration code** — only the `workflowId` differs: #### License Verification — *Credential only* - Checks: `[credential]` - Flow: State → license type → name + license number → verify. - Fastest path to verify a professional license. No ID scan required. #### KYC + License — *Identity + Credential* - Checks: `[id_verification, liveness, face_match, credential]` - Flow: Scan ID + selfie (OCR + liveness + 1:1 face match), then state + license type + license number. - The name is taken from the verified ID automatically (the user doesn't type it), so a license belonging to a different person is rejected. ```text IF you only need to verify a professional license (no ID scan): → use the "License Verification" workflowId IF you need identity + credential (ID scan + selfie + license): → use the "KYC + License" workflowId IF unsure which workflowId to use: → open the Developer Portal (https://dev.valyd.vip) → Workflows, and copy the workflowId of the preset you created ``` ## Using the workflow ID Pass the `workflowId` when [creating a session](/verifications/quickstart) through the SDK: ```javascript const session = await verify.sessions.create({ workflowId: process.env.VALYD_WORKFLOW_ID, redirectUrl: "https://app.example.com/verify/callback", }); ``` The session's `features` array in the response echoes the workflow's checks. Both presets use the **same integration code** — only the `workflowId` differs, so switching from license-only to full KYC + license is a one-variable change. ## Example: bundling several checks in one session One session can run **several checks back to back** — the person completes them all on one page, and you get **one combined decision**. A common shape is an **EVV / home-health** onboarding where a caregiver must, in a single sitting, prove **who they are, that they're licensed, and where they are**: - **ID / KYC** — scan a government ID (OCR + authenticity) with a **live** selfie and 1:1 face match. - **Professional license** — verify their nursing/clinical license against the name on the ID. - **Location verification** — confirm they're at the visit location. You don't wire these together in code. In the Portal's workflow builder you pick the checks (and their order) once, and Valyd hands you a single `workflowId`. Then the **same one call** you already use runs the whole flow — the person selects nothing technical, they just complete each step in turn: ```javascript // The workflow already bundles [id_verification, liveness, face_match, credential, location]. const session = await verify.sessions.create({ workflowId: process.env.VALYD_EVV_WORKFLOW_ID, // the EVV workflow you built in the portal redirectUrl: `${process.env.APP_URL}/visits/verified`, callback: `${process.env.APP_URL}/webhooks/valyd`, vendorData: visit.caregiverId, // your internal ref — echoed back on the webhook }); // → res.redirect(session.url) — one page, ID → license → location, in order ``` When the caregiver finishes, one signed webhook fires with **one decision** covering every check, and the decision's `checks` array carries the per-check breakdown (ID, liveness, face match, license, location) — read it as in [Decisions & statuses](/verifications/statuses). ## Changing a workflow Update a workflow's name or features in the Portal. A session runs the checks of the workflow it was created with — create a new session to pick up changes. Keep separate workflows (and apps) for test and production rather than mutating one in place. ## Reuse on connected sessions When a session is created with the connected user's `valyd_access_token` ([Reusable Verification](/verifications)), the flow **skips steps the account has already completed** — an already-KYC'd user isn't asked to rescan their ID; a returning user re-verifies with a selfie matched against their stored face vector. A session created without a user's token always runs every check in the workflow. Next: [Run a verification](/verifications/quickstart) for the full session flow, or the [checks reference](/verifications/types) for what each check does.