# 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 provides two developer products: **Valyd ID** (OAuth2 / OpenID Connect third-party SSO, > internally "TPSSO" — verified user profiles, professional licenses, and identity claims) and > **Valyd Verify** (hosted and server-to-server identity verification: ID checks, liveness, face > match, age, and credential verification). 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). ## 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: `https://idp.valyd.work` - Valyd Verify API: `https://idp.valyd.work` - Developer Portal (Valyd ID): `https://dev.valyd.work` - Developer Portal: `https://dev.valyd.work` - **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 (Valyd Verify). - Auth headers: Valyd ID uses OAuth2 Bearer access tokens; Valyd Verify uses `X-API-Key: ` (Bearer also accepted). - Token exchange and webhook signature verification MUST run server-side (never expose the client secret or webhook secret to a browser). ## Valyd ID — Getting started - [Overview](https://docs.valyd.work/docs/overview.md): What Valyd ID is, the 6-step OAuth2 SSO integration flow, and security notes. - [Create a Project & Get Credentials](https://docs.valyd.work/docs/create-project.md): Dev-portal setup — sign up, create a project, set redirect/scopes, copy client_id + client_secret. (Human-only.) - [Quick start — Login with Valyd](https://docs.valyd.work/docs/quick-start.md): Install valyd-idp-sdk and wire the full TPSSO/OAuth login flow end to end. - [Login sessions (CSRF)](https://docs.valyd.work/docs/login-sessions.md): Why OAuth `state` comparison fails for TPSSO and how to use createLoginSession/verifyLoginSession. ## Valyd ID — Authentication & integration - [Authentication (OAuth2 / TPSSO flow)](https://docs.valyd.work/docs/authentication.md): Authorization URL, login-session CSRF, callback handling, code→token exchange. - [OpenID Connect (OIDC) Integration](https://docs.valyd.work/docs/oidc.md): Discovery endpoint, manual config, Mendix setup, claim/user mapping, testing, troubleshooting, security. ## Valyd ID — Reference - [API Reference (endpoints)](https://docs.valyd.work/docs/endpoints.md): POST /token, GET /userinfo, GET /licenses, GET /verifications, POST /refresh — base URL https://idp.valyd.work/api/auth/tpsso. - [OAuth2 Scopes](https://docs.valyd.work/docs/scopes.md): profile, verifications, doctor_license, zkp, mcp — what data each unlocks and how to request them. - [Errors & troubleshooting](https://docs.valyd.work/docs/errors.md): Every error code with cause and fix, plus the SDK "Invalid login session" failure. - [Changelog](https://docs.valyd.work/docs/changelog.md): valyd-idp-sdk release history (v0.2.0 login sessions, v0.1.0 initial client). ## Valyd Verify — Guides - [Introduction](https://docs.valyd.work/verify/intro.md): What Valyd Verify is, Hosted vs Core APIs modes, services, base URL, response envelope. - [Quickstart](https://docs.valyd.work/verify/quickstart.md): Get an API key, create a workflow/webhook, make a first Core API call and a hosted session. - [Developer Portal](https://docs.valyd.work/verify/console.md): Apps, API keys, workflows, webhooks, SSO. (Human-only UI steps.) - [Hosted vs Core APIs](https://docs.valyd.work/verify/modes.md): Decision tree and comparison to choose an integration mode. - [Account (Managed by Valyd)](https://docs.valyd.work/verify/managed.md): Account vs Non-account model, the proofs-only data rule, hosted + core account APIs, reuse, and the consent Core API for raw KYC. - [Hosted Verification](https://docs.valyd.work/verify/hosted.md): Create session, redirect, signed webhooks, decision API, statuses. Base URL https://idp.valyd.work. - [Core APIs](https://docs.valyd.work/verify/standalone.md): Server-to-server checks — ID, liveness, face match, age, credential, KYC+credential. - [Node SDK](https://docs.valyd.work/verify/sdk.md): Install/init valyd-verify-sdk, resources, types, error handling, Express webhook handler. - [Webhooks](https://docs.valyd.work/verify/webhooks.md): Register a callback URL and verify HMAC-SHA256 signatures over the raw request body. - [Statuses & decisioning](https://docs.valyd.work/verify/statuses.md): Every session/check status, what it means, and how to act on it. ## Valyd Verify — API Reference - [API Reference](https://docs.valyd.work/verify/api-reference.md): Sessions, Workflows, Core checks, Decision, and Errors & rate limits. All calls use `X-API-Key`. ## Downloads & tooling - [Postman collection](https://docs.valyd.work/valyd-postman-collection.json): Importable collection of the Valyd ID API requests. - [Node SDK starter](https://docs.valyd.work/downloads/valyd-sdk-starter.zip): Starter scaffold for integrating Valyd. ## 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.work (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.work) 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.work/docs/overview.md === ================================================================================ > Source: https://docs.valyd.work/docs/overview > Part of: Valyd ID API documentation — static copy generated for AI agents > Generated from repo component: OverviewSection.tsx # Valyd Third-Party SSO API ## Agent Quick-Start - Source URL: https://docs.valyd.work/docs/overview - Credentials / env vars needed: client_id, client_secret (obtain from the Developer Portal — see Prerequisites) - Files an integrator edits: none — reference / orientation page only - Estimated steps: 6 (the end-to-end OAuth2 SSO flow described below) - Can complete without human input: NO — obtaining credentials requires signing up at https://dev.valyd.work and creating a project (a human-only web step) - Prerequisites: - A basic Valyd account (sign up at https://dev.valyd.work — no KYC verification required) - A registered project in the Developer Portal that provides your `client_id` and `client_secret` - A backend capable of making server-side HTTPS requests (to keep `client_secret` secret) Integrate secure identity verification and authentication into your application using Valyd's OAuth2-based Single Sign-On system. Get access to verified user profiles, professional licenses, and identity verification data. ## Base URL ```text https://idp.valyd.work/api/auth/tpsso ``` ## Integration Flow Access to the Developer Portal requires a basic Valyd account. No KYC verification needed — just sign up at https://dev.valyd.work to get your API credentials. The end-to-end OAuth2 SSO flow has six steps: 1. **Create Project** — Register your application at https://dev.valyd.work to get your client credentials (`client_id` and `client_secret`). 2. **Redirect to Authorization** — When a user clicks "Login with Valyd", redirect them to the authorization URL with your `client_id` and requested `scope`s. 3. **User Consent** — The user sees the consent screen with the requested permissions and approves access. 4. **Receive Authorization Code** — After approval, the user is redirected to your callback URL with a one-time code (valid for 5 minutes). 5. **Exchange Code for Tokens** — Your backend exchanges the code for an `access_token` and `refresh_token` using your `client_secret`. 6. **Access Protected Resources** — Use the `access_token` to call `/userinfo`, `/licenses`, and `/verifications` endpoints. ### Authorization URL shape The authorization URL is built from the IdP base host `https://idp.valyd.work`, the `/auth` path, and query parameters. `scope` is a space-separated list, URL-encoded. ```text https://idp.valyd.work/auth?client_id=YOUR_CLIENT_ID&redirect_url=YOUR_REDIRECT_URI&scope=profile%20verifications ``` - `YOUR_CLIENT_ID` — get this from the Developer Portal → your project → Credentials: https://dev.valyd.work - `YOUR_REDIRECT_URI` — the Redirect URL you registered for the project in the Developer Portal (must NOT end with a trailing slash) - `scope` — space-separated scope list (e.g. `profile verifications zkp`), URL-encoded so the space becomes `%20` ## Security Notes - Keep your `client_secret` server-side only — never expose it in frontend code. - `access_token` is short-lived (15 minutes); `refresh_token` is longer-lived. Note: the component lists `access_token` as "short-lived (15 minutes)" in the Security Notes, while the Integration Flow text describes it generically as "short-lived". 15 minutes is the explicit value given. - Always use HTTPS for all API calls. - Store tokens securely and never log them in production. ## Developer Tools The live documentation page includes a Postman Collection generator widget for exploring these endpoints interactively. It is a UI convenience and produces a Postman collection for the same endpoints described here; there is no additional API surface beyond the endpoints listed above. ================================================================================ === FILE: https://docs.valyd.work/docs/create-project.md === ================================================================================ > Source: https://docs.valyd.work/docs/create-project > Part of: Valyd ID API documentation — static copy generated for AI agents > Generated from repo component: GettingStartedSection.tsx # Create a Project & Get Your Credentials ## Agent Quick-Start - Source URL: https://docs.valyd.work/docs/create-project - Credentials / env vars needed: produces `client_id` and `client_secret` (these are the OUTPUT of this page, not a prerequisite) - Files an integrator edits: your backend environment file (e.g. `.env`) to store `client_secret` after you obtain it - Estimated steps: 3 - Can complete without human input: NO — every step is a manual action in the Developer Portal web UI (sign up, create project, copy the one-time-shown secret). An automated agent cannot perform these; a human must do them and then hand the agent the resulting `client_id` and `client_secret`. - Prerequisites: - A basic Valyd account (sign up at https://dev.valyd.work — no KYC verification required) - Access to a browser to use the Developer Portal at https://dev.valyd.work - The exact production domain(s) your app sends requests from (for Allowed Web Origins) - Your callback/redirect URL, with NO trailing slash (e.g. `https://myapp.com/callback`) Before integrating with Valyd SSO, you need to register your application in the Developer Portal to obtain your client credentials. Access to the Developer Portal requires a basic Valyd account; no KYC verification is needed — just sign up at https://dev.valyd.work. ## Prerequisites - A Valyd account. Sign up at https://dev.valyd.work 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.work — 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.work ``` **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` — User's profile photo - `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. ### Step 3 — Save your credentials 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. Store the secret in your backend environment file, for example: ```bash # .env (server-side only — never commit or expose in frontend code) VALYD_CLIENT_ID=9357c59bc1794b4c9efe8823e5878147 VALYD_CLIENT_SECRET=sk_live_a1b2c3d4e5f6g7h8i9j0... ``` **Expected output:** You have copied and stored a `client_id` (32-hex-character string) and a `client_secret` (prefixed `sk_live_...`). The secret is now saved server-side. ## You're Ready — Next Steps With your Client ID and Client Secret you can now integrate Valyd SSO into your application: 1. Store your `client_secret` securely in your backend environment variables. 2. Implement the "Login with Valyd" button that redirects to the authorization URL (`https://idp.valyd.work/auth?client_id=...&redirect_url=...&scope=...`). 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 user data. ## 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.work → 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.work/docs/quick-start.md === ================================================================================ > Source: https://docs.valyd.work/docs/quick-start > Part of: Valyd ID API documentation — static copy generated for AI agents > Generated from repo component: QuickStartSection.tsx # Quick start — Login with Valyd ## Agent Quick-Start - Source URL: https://docs.valyd.work/docs/quick-start - Credentials / env vars needed: VALYD_CLIENT_ID, VALYD_CLIENT_SECRET, VALYD_REDIRECT_URI - Files an integrator edits: .env, server route handlers (e.g. server.ts: /login and /callback) - Estimated steps: 6 - Can complete without human input: NO — the client ID, client secret, and registered redirect URI must be created/copied by a human from the Valyd developer portal, and scopes must be enabled there. - Prerequisites: - Node.js 18+ installed. - A registered app in the Valyd developer portal with a client ID and client secret. - The redirect URI (e.g. http://localhost:8080/callback) registered in the portal, matching exactly (no trailing slash). - The scopes you intend to request (profile, verifications, doctor_license, zkp, mcp) enabled in the portal. ### Prerequisites - Node.js 18+ (the SDK requires it). - `valyd-idp-sdk` version `^0.2.0` or later (older versions lack the login-session helpers used here). - A Valyd app with credentials. Obtain `VALYD_CLIENT_ID` and `VALYD_CLIENT_SECRET` from the Valyd developer portal (get these from the developer portal → your project → Credentials). `VALYD_CLIENT_SECRET` is server-side only and must never reach a browser. - A redirect URI registered in the portal that exactly matches `VALYD_REDIRECT_URI` (no trailing slash). - Scopes enabled in the portal: any of `profile`, `verifications`, `doctor_license`, `zkp`, `mcp`. ### Steps 1. **Install the SDK.** Run the install command (the official `valyd-idp-sdk` handles the full TPSSO/OAuth2 flow, login sessions for CSRF protection, and typed resource calls). ```bash npm install valyd-idp-sdk@^0.2.0 ``` **Expected output:** npm adds `valyd-idp-sdk` (a `0.2.x` or newer release) to `dependencies` in `package.json` and reports the package was added with no error exit code. 2. **Create the `.env` file.** Use `KEY=value` with no spaces around `=`. The values shown are examples — substitute your own. ```bash # .env — no spaces around = VALYD_CLIENT_ID=9357c59bc1794b4c9efe8823e5878147 VALYD_CLIENT_SECRET=sk_live_a1b2c3d4e5f6... VALYD_REDIRECT_URI=http://localhost:8080/callback ``` - `VALYD_CLIENT_ID` — get this from the Valyd developer portal → your project → Credentials. - `VALYD_CLIENT_SECRET` — get this from the Valyd developer portal → your project → Credentials. Server-side only; never bundle into the browser. - `VALYD_REDIRECT_URI` — must match the value registered in the portal exactly (no trailing slash). For local dev use e.g. `http://localhost:8080/callback` and also register it in the portal. **Expected output:** A `.env` file on disk with the three keys set. No command output; verify the file contents are correct. 3. **Initialize the client.** Construct a `ValydClient` from your environment variables. `baseUrl` defaults to `https://idp.valyd.work`. ```typescript // server.ts import { ValydClient } from "valyd-idp-sdk"; const valyd = new ValydClient({ clientId: process.env.VALYD_CLIENT_ID!, clientSecret: process.env.VALYD_CLIENT_SECRET!, redirectUri: process.env.VALYD_REDIRECT_URI!, // e.g. http://localhost:8080/callback // baseUrl defaults to https://idp.valyd.work }); ``` **Expected output:** A configured `valyd` client instance. No network call is made at construction. 4. **Start a login session and redirect to Valyd.** Before redirecting the user, call `createLoginSession()`. This issues an HMAC-signed marker that you must store server-side (here, an `httpOnly` cookie). Then build the authorize URL with `getAuthorizationUrl()` and redirect. ```typescript // 1. Start a login session before redirecting the user. // This issues an HMAC-signed marker you must store server-side. app.get("/login", async (req, res) => { const session = await valyd.createLoginSession(); // Persist the marker (httpOnly cookie or server session). res.cookie("valyd_login", session.marker, { httpOnly: true, sameSite: "lax", secure: process.env.NODE_ENV === "production", maxAge: 10 * 60 * 1000, // 10 minutes }); // 2. Build the authorize URL and redirect. const url = valyd.getAuthorizationUrl({ state: session.authorizeState, scope: ["profile", "verifications"], productName: "My App", }); res.redirect(url); }); ``` **Expected output:** `createLoginSession()` returns an object containing `authorizeState` and `marker`. The browser is redirected (HTTP 302) to the Valyd authorize URL, and the `valyd_login` cookie is set with the marker. 5. **Handle the callback and verify the login session (CSRF check).** On the callback route, parse the query, then verify the stored marker — NOT the callback `state`. ```typescript // 3. Handle the callback. app.get("/callback", async (req, res) => { const { code, error } = valyd.parseCallback(req.url); if (error || !code) return res.status(400).send(error ?? "missing code"); // 4. CSRF check — verify the marker we stored, NOT the callback state. const marker = req.cookies.valyd_login; const check = await valyd.verifyLoginSession(marker); if (!check.valid) return res.status(400).send("Invalid login session"); }); ``` **Expected output:** `parseCallback(req.url)` returns `{ code, error }`. On a valid login, `verifyLoginSession(marker)` returns `{ valid: true }`. If the marker is expired, missing, or tampered, it returns `{ valid: false }` and the route responds HTTP 400 `"Invalid login session"`. IF the callback contains an `error` or no `code`: → respond HTTP 400 with the error message (or `"missing code"`) and stop. IF `verifyLoginSession(marker)` returns `{ valid: false }`: → respond HTTP 400 `"Invalid login session"` and stop; do NOT exchange the code. IF you are tempted to compare the sent `state` to the callback `state`: → do NOT. Valyd does not echo your `state`; the callback `state` is Valyd's own session id. Use `verifyLoginSession(marker)` instead. See login-sessions.md. 6. **Exchange the code for tokens and call resource endpoints.** After the CSRF check passes, exchange the authorization code and use the access token to fetch user data. ```typescript // 5. Exchange the code for tokens. const tokens = await valyd.exchangeCode(code); // 6. Call resource endpoints with the access token. const profile = await valyd.getUserInfo(tokens.accessToken); const verifications = await valyd.getVerifications(tokens.accessToken); res.clearCookie("valyd_login"); // ...set your own app session and redirect the user. ``` **Expected output:** `exchangeCode(code)` returns a tokens object containing `accessToken`. `getUserInfo(tokens.accessToken)` returns the user's profile and `getVerifications(tokens.accessToken)` returns their verifications. The `valyd_login` cookie is cleared. You then set your own app session and redirect the user. ### The flow at a glance | Step | What | SDK method | | --- | --- | --- | | 1 | Start login, store marker | `createLoginSession()` | | 2 | Redirect to Valyd | `getAuthorizationUrl()` | | 3 | Read callback query | `parseCallback()` | | 4 | CSRF check | `verifyLoginSession(marker)` | | 5 | Get tokens | `exchangeCode(code)` | | 6 | User data | `getUserInfo()`, `getVerifications()`, … | ### Environment & app setup | Item | Rule | | --- | --- | | `VALYD_CLIENT_ID` | From the dev portal. | | `VALYD_CLIENT_SECRET` | Server-side only. Never bundle into the browser. | | `VALYD_REDIRECT_URI` | Must match the portal value exactly (no trailing slash). | | Local dev | e.g. `http://localhost:8080/callback` — also register in the portal. | | Scopes | Enable in the portal: `profile`, `verifications`, `doctor_license`, `zkp`, `mcp`. | ### Verification - Confirm the SDK installed at the required version: ```bash npm ls valyd-idp-sdk ``` **Expected output:** `valyd-idp-sdk@0.2.x` (or newer) listed. If it shows below `0.2.0`, the login-session helpers (`createLoginSession`, `verifyLoginSession`) will be missing. - Run the full round trip: start the server, visit the `/login` route in a browser, complete login at Valyd, and confirm the `/callback` route reaches step 6. A successful run sets the `valyd_login` cookie on `/login`, passes `verifyLoginSession` on the callback, and returns a populated `profile` from `getUserInfo`. ### Common errors - **Comparing OAuth `state` for CSRF (the most common mistake).** - **Cause:** Valyd (TPSSO) does not echo the `state` you send on `authorize`; the `state` on the callback is Valyd's own session id, so `sentState !== callbackState` is always "broken" and rejects valid logins. - **Fix:** Do not compare states. Use `createLoginSession()` before redirect and `verifyLoginSession(marker)` on the callback for CSRF. See login-sessions.md. - **"Invalid login session" on the callback.** - **Cause:** The marker cookie is missing (cookie not set, blocked, or expired beyond the 10-minute TTL) or tampered, so `verifyLoginSession(marker)` returns `{ valid: false }`. - **Fix:** Ensure the `valyd_login` cookie is set on `/login` with `maxAge: 10 * 60 * 1000` and `httpOnly: true`, that the browser sends it back on `/callback`, and that the user completes login within 10 minutes. - **Redirect URI mismatch / scope errors at the authorize step.** - **Cause:** `VALYD_REDIRECT_URI` does not exactly match the value registered in the portal (e.g. a trailing slash), or a requested scope is not enabled in the portal. - **Fix:** Make `VALYD_REDIRECT_URI` identical to the registered value (no trailing slash), and enable each requested scope (`profile`, `verifications`, `doctor_license`, `zkp`, `mcp`) in the portal before requesting it. ================================================================================ === FILE: https://docs.valyd.work/docs/login-sessions.md === ================================================================================ > Source: https://docs.valyd.work/docs/login-sessions > Part of: Valyd ID API documentation — static copy generated for AI agents > Generated from repo component: LoginSessionsSection.tsx # Login sessions (CSRF protection) ## Agent Quick-Start - Source URL: https://docs.valyd.work/docs/login-sessions - Credentials / env vars needed: none directly (relies on the configured `valyd` client, which uses your client ID/secret — get these from the Valyd developer portal → your project → Credentials) - Files an integrator edits: server route handlers (the login redirect route and the callback route) - Estimated steps: 2 - Can complete without human input: YES — this is a code-only CSRF mechanism using SDK methods; no portal action is required for the mechanism itself (though the `valyd` client must already be configured with credentials). - Prerequisites: - A configured `valyd` client (`new ValydClient({...})`) from `valyd-idp-sdk` version `^0.2.0` or later. - Server-side storage available for the marker (httpOnly cookie, encrypted session, or KV store). The classic OAuth CSRF check — generate a random `state`, then compare what the IdP echoes — does NOT work for Valyd TPSSO, because Valyd returns its own session id on the callback. The SDK ships with a purpose-built mechanism: **login sessions**. **The problem:** Comparing the callback `state` against the value you sent will always fail — Valyd substitutes its own opaque session id on the redirect back. **The solution:** Call `createLoginSession()` before the redirect and store the `marker` server-side. On the callback, call `verifyLoginSession(marker)`. ### Prerequisites - A configured `valyd` client instance from `valyd-idp-sdk` (`^0.2.0`+), constructed with your `clientId`, `clientSecret`, and `redirectUri` (get the client ID and secret from the Valyd developer portal → your project → Credentials). - Server-side storage for the marker: an `httpOnly` cookie, an encrypted server session, or a KV store. The marker must never be exposed to client-side JavaScript. ### Steps 1. **Before redirecting the user to Valyd, create a login session and store the marker.** `createLoginSession()` returns an `authorizeState` and an HMAC-signed `marker`. Store the marker server-side (here, an `httpOnly` cookie with a 10-minute lifetime to match the marker TTL), then redirect to the authorize URL using `session.authorizeState` as `state`. ```typescript // 1. Before redirecting the user to Valyd const session = await valyd.createLoginSession(); // → { authorizeState: "...", marker: "v1.." } res.cookie("valyd_login", session.marker, { httpOnly: true, sameSite: "lax", secure: true, maxAge: 10 * 60 * 1000, // 10 minutes (matches marker TTL) }); res.redirect(valyd.getAuthorizationUrl({ state: session.authorizeState, scope: ["profile", "verifications"], })); ``` **Expected output:** `createLoginSession()` resolves to `{ authorizeState: "...", marker: "v1.." }`. The `valyd_login` cookie is set with the marker, and the browser is redirected (HTTP 302) to the Valyd authorize URL. 2. **On the callback, verify the stored marker.** Read the marker from your server-side storage and pass it to `verifyLoginSession(marker)`. It returns `{ valid: boolean }` and never throws on an invalid marker. ```typescript // 2. On the callback const marker = req.cookies.valyd_login; const { valid } = await valyd.verifyLoginSession(marker); if (!valid) { // Expired login, missing cookie, or tampered marker. return res.status(400).send("Invalid login session"); } ``` **Expected output:** On a legitimate login within the TTL, `verifyLoginSession(marker)` returns `{ valid: true }`. Otherwise it returns `{ valid: false }` and this route responds HTTP 400 with body `"Invalid login session"`. IF the marker cookie is present and within its 10-minute TTL and untampered: → `{ valid: true }`; proceed to `exchangeCode()`. IF the marker is expired (older than 10 minutes), the cookie is missing, or the marker was tampered with: → `{ valid: false }`; respond HTTP 400 `"Invalid login session"` and stop. Do NOT call `exchangeCode()`. IF you are unsure whether the marker is valid: → call `await valyd.verifyLoginSession(marker)` and branch on the returned `valid` boolean (it never throws). ### Marker / login-session properties | Property | Details | | --- | --- | | Marker format | HMAC-signed string. Signed with your client secret on Valyd's side. | | TTL | 10 minutes. After that, `verifyLoginSession` returns `{ valid: false }`. | | Storage | Server only — `httpOnly` cookie, encrypted session, or KV. Never expose to JS. | | Return value | `verifyLoginSession` returns `{ valid: boolean }`. It never throws on an invalid marker. | | When to verify | On the callback, before `exchangeCode()`. | A full Express example is available in the SDK repo: https://github.com/valyd/idp-sdk/blob/HEAD/examples/express-login.ts ### Verification - Happy path: complete a real login. On the callback, `verifyLoginSession(marker)` should return `{ valid: true }` and the request should proceed to `exchangeCode()`. - Negative path: clear or wait out the `valyd_login` cookie (or modify a character in the marker), then hit the callback. `verifyLoginSession(marker)` should return `{ valid: false }` and the route should respond HTTP 400 `"Invalid login session"`. ### Common errors - **Using OAuth `state` comparison for CSRF.** - **Cause:** Valyd substitutes its own opaque session id for the `state` on the callback, so comparing it to the value you sent always fails (and offers no protection). - **Fix:** Use the login-session mechanism: `createLoginSession()` before the redirect, store the `marker` server-side, and `verifyLoginSession(marker)` on the callback. - **`verifyLoginSession` returns `{ valid: false }` for a legitimate user.** - **Cause:** The marker expired (more than 10 minutes elapsed before the callback), the cookie was not sent back, or the marker was altered in transit/storage. - **Fix:** Set the marker cookie with `maxAge: 10 * 60 * 1000` to match the TTL, ensure it is `httpOnly` and returned on the callback, and have the user complete login within 10 minutes. Store the marker verbatim — do not re-encode or truncate it. - **Exposing the marker to client-side JavaScript.** - **Cause:** Storing the marker somewhere readable by the browser (non-`httpOnly` cookie, localStorage), which undermines the CSRF protection. - **Fix:** Store the marker server-side only — an `httpOnly` cookie, an encrypted server session, or a KV store. Never expose it to JS. ================================================================================ === FILE: https://docs.valyd.work/docs/authentication.md === ================================================================================ > Source: https://docs.valyd.work/docs/authentication > Part of: Valyd ID API documentation — static copy generated for AI agents > Generated from repo component: AuthenticationSection.tsx # Authentication (OAuth2 / TPSSO Flow) ## Agent Quick-Start - Source URL: https://docs.valyd.work/docs/authentication - Credentials / env vars needed: VALYD_CLIENT_ID, VALYD_CLIENT_SECRET, VALYD_REDIRECT_URI - Files an integrator edits: .env (credentials), server route handler for `/callback`, login/redirect route handler - Estimated steps: 6 - Can complete without human input: NO — a human must register the app and obtain a Client ID + Client Secret from the Developer Portal (https://dev.valyd.work), and register the exact redirect/callback URL. - Prerequisites: - A Valyd application registered in the Developer Portal (https://dev.valyd.work) with a Client ID and Client Secret. - The exact callback/redirect URL (e.g. `https://yourapp.com/callback`) registered on that application — it must match the `redirect_url` you send. - A server-side environment that can keep `VALYD_CLIENT_SECRET` private (token exchange MUST happen server-side). - (Recommended) Node project with the official SDK: `npm install valyd-idp-sdk@^0.2.0` --- Valyd uses the OAuth2 Authorization Code flow (referred to internally as TPSSO). This is a server-side integration: you redirect the user to Valyd, the user consents, Valyd redirects back to your callback with a one-time `code`, and your server exchanges that code for tokens. CRITICAL behavioral difference from standard OAuth2: Valyd does NOT echo your `state` back on the callback. The `state` returned to your callback is Valyd's own opaque session id. Do NOT use it for CSRF protection by comparing it to a value you sent. Instead, create a login session up front, store its marker, and verify the marker on the callback. ### Prerequisites - Client ID and Client Secret (get these from the Developer Portal → your project → Credentials: https://dev.valyd.work). - A registered redirect/callback URL matching what you send as `redirect_url`. - Environment variables set on your server: - `VALYD_CLIENT_ID` (get from Developer Portal: https://dev.valyd.work) - `VALYD_CLIENT_SECRET` (get from Developer Portal: https://dev.valyd.work) - `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 Valyd authorization endpoint with your client credentials and requested scopes. The exact URL format is: ```text https://idp.valyd.work/auth?client_id={client_id}&redirect_url={redirect_url}&scope={scopes} ``` Parameters: | Parameter | Required | Description | | --- | --- | --- | | `client_id` | Yes | Your application's Client ID from the Developer Portal (https://dev.valyd.work). | | `redirect_url` | Yes | The URL to redirect to after authentication. Must match the URL registered on your application. | | `scope` | Yes | Space-separated list of scopes, URL-encoded. Example: `profile%20verifications`. | | `state` | Optional | Pass `session.authorizeState` from `createLoginSession()`. NOT echoed on the callback for TPSSO — do not use it for CSRF on its own. | **Expected output:** A fully-formed URL string. Example with encoded scopes `profile verifications`: ```text https://idp.valyd.work/auth?client_id=YOUR_CLIENT_ID&redirect_url=https://yourapp.com/callback&scope=profile%20verifications ``` 2. **Issue a login session and redirect (recommended: use the SDK).** On your login route, create a login session, store its marker (httpOnly cookie or server session), build the authorization URL, and redirect the user: ```javascript import { ValydClient } from "valyd-idp-sdk"; const valyd = new ValydClient({ clientId: process.env.VALYD_CLIENT_ID!, clientSecret: process.env.VALYD_CLIENT_SECRET!, redirectUri: "https://yourapp.com/callback", }); // Issue a login session and redirect — never compare state on the callback. const session = await valyd.createLoginSession(); // store session.marker in an httpOnly cookie or server session const url = valyd.getAuthorizationUrl({ state: session.authorizeState, scope: ["profile", "verifications", "zkp"], productName: "My App", }); res.redirect(url); ``` **Expected output:** HTTP 302 redirect sending the user's browser to `https://idp.valyd.work/auth?...`. The login session marker is now stored on the user's side (cookie/session) for the CSRF check in step 5. 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`. **Expected output:** Valyd redirects the user's browser to your callback URL with the code attached. The `code` is valid for only 5 minutes. 4. **Receive the callback on your server.** The user is redirected to your registered callback URL with the authorization code as a query parameter. Format: ```text https://yourapp.com/callback?code=AUTH_CODE_HERE ``` Callback query parameters: | Parameter | Description | | --- | --- | | `code` | The one-time authorization code (valid for 5 minutes). | | `state` | Valyd's own session id (opaque). Do NOT compare it to the value you sent on `authorize`. Use `verifyLoginSession(marker)` for CSRF. | **Expected output:** Your `/callback` route is invoked with `code` (and possibly `error`) present in the query string. 5. **CSRF check via the login session.** Verify the marker you stored in step 2. This is how you protect against CSRF — NOT by comparing the callback `state`. ```javascript const marker = req.cookies.valyd_login; const { valid } = await valyd.verifyLoginSession(marker); if (!valid) return res.status(400).send("Invalid login session"); ``` **Expected output:** `verifyLoginSession(marker)` returns `{ valid: true }` for a legitimate flow. If `valid` is `false`, reject the request (HTTP 400). 6. **Exchange the code for tokens, then fetch the user (server-side).** Immediately exchange the `code` (it expires in 5 minutes). Full Node/Express + SDK handler: ```javascript // Recommended: use the official SDK. // npm install valyd-idp-sdk@^0.2.0 import { ValydClient } from "valyd-idp-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) => { // 1. Pull code + state out of the redirect. const { code, error } = valyd.parseCallback(req.url); if (error || !code) return res.status(400).send(error ?? "missing code"); // 2. CSRF — verify the marker we stored on the way out. // Do NOT compare req.query.state to anything you sent. const marker = req.cookies.valyd_login; const { valid } = await valyd.verifyLoginSession(marker); if (!valid) return res.status(400).send("Invalid login session"); // 3. Exchange the code for tokens (server-side). const tokens = await valyd.exchangeCode(code); // 4. Fetch user data. const user = await valyd.getUserInfo(tokens.accessToken); res.clearCookie("valyd_login"); // ...set your own app session, then redirect to /dashboard }); ``` **Expected output:** `exchangeCode(code)` returns tokens (with `accessToken`); `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.work/api/auth/tpsso/token` with a JSON body. The response wraps the tokens under a `data` key (i.e. `response.data.access_token`). Request shape: ```http POST /api/auth/tpsso/token HTTP/1.1 Host: idp.valyd.work Content-Type: application/json { "grant_type": "authorization_code", "client_id": "YOUR_CLIENT_ID", "client_secret": "YOUR_CLIENT_SECRET", "code": "AUTH_CODE_HERE" } ``` **Expected output:** HTTP 200 with a JSON body whose `data` object contains `access_token` (read it as `response.data.access_token`). Python (Flask): ```python from flask import Flask, request, redirect, session import requests app = Flask(__name__) @app.route("/callback") def callback(): code = request.args.get("code") if not code: return "missing code", 400 response = requests.post( "https://idp.valyd.work/api/auth/tpsso/token", json={ "grant_type": "authorization_code", "client_id": "YOUR_CLIENT_ID", "client_secret": "YOUR_CLIENT_SECRET", "code": code, }, ) tokens = response.json()["data"] session["access_token"] = tokens["access_token"] return redirect("/dashboard") ``` PHP: ```php 'https://idp.valyd.work/api/auth/tpsso/token', CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode([ 'grant_type' => 'authorization_code', 'client_id' => 'YOUR_CLIENT_ID', 'client_secret' => 'YOUR_CLIENT_SECRET', 'code' => $code ]), CURLOPT_HTTPHEADER => ['Content-Type: application/json'], CURLOPT_RETURNTRANSFER => true, ]); $data = json_decode(curl_exec($ch), true); session_start(); $_SESSION['access_token'] = $data['data']['access_token']; header('Location: /dashboard'); ``` Java (Spring): ```java @GetMapping("/callback") public ResponseEntity callback(@RequestParam String code) { RestTemplate rt = new RestTemplate(); Map body = Map.of( "grant_type", "authorization_code", "client_id", "YOUR_CLIENT_ID", "client_secret", "YOUR_CLIENT_SECRET", "code", code ); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); rt.postForEntity("https://idp.valyd.work/api/auth/tpsso/token", new HttpEntity<>(body, headers), String.class); return ResponseEntity.status(302).header("Location", "/dashboard").build(); } ``` In all raw-HTTP examples, replace `YOUR_CLIENT_ID` and `YOUR_CLIENT_SECRET` with your real values (get these from the Developer Portal → your project → Credentials: https://dev.valyd.work). 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: → call valyd.createLoginSession(), store session.marker (httpOnly cookie / server session), build the URL with valyd.getAuthorizationUrl({ state: session.authorizeState, scope, productName }), and res.redirect(url). IF the request hit your /callback route: → parse it: const { code, error } = valyd.parseCallback(req.url) 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): → const { valid } = await valyd.verifyLoginSession(req.cookies.valyd_login) IF valid === false: → return HTTP 400 "Invalid login session". STOP. IF valid === true: → proceed to token exchange. DO NOT compare the callback `state` to anything you sent — for TPSSO it is Valyd's opaque session id, not your value. Token exchange: → const tokens = await valyd.exchangeCode(code) // must run server-side, within 5 minutes of issue IF more than 5 minutes elapsed since the code was issued: → the code is expired; restart the flow from the login route. → const user = await valyd.getUserInfo(tokens.accessToken) → res.clearCookie("valyd_login"), set your own app session, redirect to /dashboard. IF you are not using Node / the SDK: → POST https://idp.valyd.work/api/auth/tpsso/token with JSON { grant_type: "authorization_code", client_id, client_secret, code } and read the token from response.data.access_token. ``` ### Verification - Confirm the redirect: opening your login route returns HTTP 302 with a `Location` header beginning `https://idp.valyd.work/auth?client_id=...&redirect_url=...&scope=...`. - After consenting, confirm your `/callback` route receives a `code` query parameter. - Confirm the token exchange succeeds: ```bash curl -i -X POST https://idp.valyd.work/api/auth/tpsso/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"}' ``` Expected: HTTP 200 and a JSON body containing `data.access_token`. ### Common errors 1. **Comparing the callback `state` to the value you sent (flow always rejected).** - Cause: Valyd's TPSSO does not echo your `state`; the `state` on the callback is Valyd's own opaque session id, so any equality check against your sent value fails. - Fix: Remove the `state` comparison. Use `valyd.createLoginSession()` + store `session.marker`, then verify on the callback with `valyd.verifyLoginSession(marker)` expecting `{ valid: true }`. 2. **"missing code" / expired code (HTTP 400 on callback or 4xx from /token).** - Cause: No `code` in the callback (user denied or an `error` was returned), or the code was used more than 5 minutes after issue. - Fix: When `parseCallback` returns `error` or no `code`, return HTTP 400 and restart the flow. Exchange the code immediately on the server — it is valid for only 5 minutes. 3. **`redirect_url` mismatch (authorization rejected by Valyd).** - Cause: The `redirect_url` 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.work), matching scheme, host, and path. ================================================================================ === FILE: https://docs.valyd.work/docs/oidc.md === ================================================================================ > Source: https://docs.valyd.work/docs/oidc > Part of: Valyd ID API documentation — static copy generated for AI agents > Generated from repo component: OIDCSection.tsx # OpenID Connect (OIDC) Integration Guide ## Agent Quick-Start - Source URL: https://docs.valyd.work/docs/oidc - Credentials / env vars needed: OIDC_CLIENT_ID, OIDC_CLIENT_SECRET (get both from the Developer Portal → your project → Credentials: https://dev.valyd.work), OIDC_REDIRECT_URI (the callback URL you register, e.g. https://your-app.mendixcloud.com/oidc/callback) - Files an integrator edits: your platform's SSO/OIDC config (Mendix Studio Pro module config, Mendix Cloud Portal Environment → Security, or a custom OIDC module's constants file) - Estimated steps: 8 (register client → configure issuer/endpoints → set scopes → enter credentials → set redirect URI → test login → exchange code → map user) - Can complete without human input: NO — a human must sign in to the Developer Portal at https://dev.valyd.work, create the OIDC client, and copy the client_secret (shown only once). After credentials exist, the rest can be automated. - Prerequisites: - A Mendix account, or admin access to your target OIDC-compatible platform's console. - Access to the Developer Portal at https://dev.valyd.work (used to register the client). - A registered OIDC client (created as a project in the Developer Portal — yields client_id and client_secret). - A redirect URL that matches EXACTLY what you configure in your platform (no trailing slashes, correct protocol). --- ## What is OpenID Connect? OpenID Connect (OIDC) is an identity layer built on top of OAuth 2.0. It allows applications to verify user identity and obtain basic profile information in a standardized, secure way. Valyd implements OIDC to provide a secure, standards-compliant authentication mechanism that works seamlessly with enterprise platforms like Mendix, and any OIDC-compatible application. ### When to Use OIDC vs Regular OAuth ```text USE OIDC WHEN: - Integrating with enterprise platforms (Mendix, Salesforce, etc.) - Your framework requires OIDC discovery endpoints - You need ID tokens with signed claims (JWT) - You need automatic discovery of endpoints USE REGULAR OAUTH WHEN: - Building a custom web/mobile application - You only need access tokens for API calls - Simpler integration requirements ``` ### OIDC Authentication Flow ```text User → Your App → Authorization → Token → Userinfo/JWKS 1. Click Login 2. Redirect to Valyd 3. User authenticates 4. Exchange code for tokens 5. Fetch user info ``` --- ### Prerequisites Before integrating OIDC, ensure you have the following: 1. **Mendix account** — Or access to your target platform's admin console. 2. **Access to Developer Portal** — Visit https://dev.valyd.work to register. 3. **Registered OIDC client** — Create a project in the Developer Portal. 4. **Correct redirect URL** — Must match exactly what you configure in your platform. --- ### Steps #### Step 1 — Log into the Developer Portal (human required) Open the Developer Portal and sign in with your account. ```text Open: https://dev.valyd.work ``` **Expected output:** You are signed in and can see your projects dashboard. #### Step 2 — Register a new OIDC client (human required) Click "Create Project" and fill in the required fields: | Field | Description | Required | | --------------- | --------------------------------------------- | -------- | | `client_id` | Auto-generated unique identifier | Auto | | `client_secret` | Auto-generated secret (shown only once!) | Auto | | `redirect_uri` | Must match EXACTLY — no trailing slashes | Required | Example redirect URI: ```text https://your-app.mendixcloud.com/oidc/callback ``` **⚠️ Critical — Save your client_secret:** The `client_secret` is displayed only once during registration. Copy it immediately and store it securely. If lost, you must regenerate it. **Expected output:** A project with a `client_id` and a one-time-displayed `client_secret`, plus your registered `redirect_uri`. #### Step 3 — Get OIDC endpoints (auto-discovery or manual) ```text IF your platform supports OIDC auto-discovery: → Point it at the discovery URL and let it auto-configure all endpoints: GET https://idp.valyd.work/api/.well-known/openid-configuration IF your platform does NOT support auto-discovery: → Enter the endpoint values manually (see "Manual OIDC Configuration" below). IF unsure whether discovery works: → Run: curl -s https://idp.valyd.work/api/.well-known/openid-configuration A 200 response with a JSON body containing "issuer" means discovery works. ``` **Discovery endpoint** (method + full URL): ```http GET https://idp.valyd.work/api/.well-known/openid-configuration ``` The discovery endpoint provides all the information needed to configure your OIDC client automatically. Most platforms can use this URL to auto-configure all endpoints. **Expected output:** HTTP 200 with this JSON body: ```json { "issuer": "https://idp.valyd.work", "authorization_endpoint": "https://idp.valyd.work/api/auth/oidc/authorize", "token_endpoint": "https://idp.valyd.work/api/auth/oidc/token", "userinfo_endpoint": "https://idp.valyd.work/api/auth/oidc/userinfo", "jwks_uri": "https://idp.valyd.work/api/auth/oidc/jwks.json", "response_types_supported": ["code", "id_token", "token id_token"], "grant_types_supported": ["authorization_code", "refresh_token"], "subject_types_supported": ["public"], "id_token_signing_alg_values_supported": ["RS256"], "scopes_supported": ["openid", "profile", "email", "verifications", "zkp"], "token_endpoint_auth_methods_supported": ["client_secret_post", "client_secret_basic"], "claims_supported": [ "sub", "iss", "aud", "exp", "iat", "name", "email", "preferred_username", "first_name", "last_name" ] } ``` **Discovery response fields:** | Field | Description | | ---------------------------- | --------------------------------------------------------------- | | `issuer` | The base URL of the identity provider | | `authorization_endpoint` | URL to redirect users for authentication | | `token_endpoint` | URL to exchange authorization code for tokens | | `userinfo_endpoint` | URL to fetch user profile information | | `jwks_uri` | URL to fetch public keys for JWT validation | | `response_types_supported` | Supported OAuth response types (code, id_token, etc.) | | `grant_types_supported` | Supported grant types (authorization_code, refresh_token) | | `scopes_supported` | Available scopes: openid, profile, email, verifications, zkp | #### Step 4 — Manual OIDC configuration (only if auto-discovery is unsupported) If your platform doesn't support auto-discovery, use these values to configure manually: | Setting | Value | | ---------------------- | ------------------------------------------------ | | Issuer | `https://idp.valyd.work` | | Authorization Endpoint | `https://idp.valyd.work/api/auth/oidc/authorize` | | Token Endpoint | `https://idp.valyd.work/api/auth/oidc/token` | | Userinfo Endpoint | `https://idp.valyd.work/api/auth/oidc/userinfo` | | JWKS URI | `https://idp.valyd.work/api/auth/oidc/jwks.json` | | Auth Method | `client_secret_post` / `client_secret_basic` | | ID Token Algorithm | `RS256` | **Expected output:** Your platform's OIDC client config now has the issuer, all four endpoints, the JWKS URI, an auth method, and the signing algorithm set. #### Step 5 — Configure your platform (Mendix or custom) Choose the path that matches your deployment: ```text IF you build/configure the app in Mendix Studio Pro: → Follow "Method 1 — Mendix Studio Pro" below. IF your app is already deployed to Mendix Cloud: → Follow "Method 2 — Mendix Cloud Portal" below. IF you use a custom OIDC module (any platform): → Follow "Method 3 — Custom Module Configuration" below. IF your platform is not Mendix but is OIDC-compatible: → Use the discovery URL (Step 3) or the manual values (Step 4) in that platform's OIDC/SSO settings; map claims per "User Mapping Guide" below. ``` ##### Method 1 — Mendix Studio Pro Configure OIDC directly in Mendix Studio Pro: 1. **Add Issuer URL** — In your SSO configuration, set: ```text https://idp.valyd.work ``` 2. **Configure Endpoints:** - Authorization: `/api/auth/oidc/authorize` - Token: `/api/auth/oidc/token` - Userinfo: `/api/auth/oidc/userinfo` 3. **Set Scopes:** ```text openid profile email ``` 4. **Enter Credentials** — Add your `client_id` and `client_secret` from the Developer Portal (https://dev.valyd.work → your project → Credentials). 5. **Set Redirect URI** — Format: ```text https://your-app.mendixcloud.com/oidc/callback ``` **Expected output:** The Mendix SSO module shows a fully configured OpenID Connect provider with issuer, endpoints, scopes, credentials, and redirect URI. ##### Method 2 — Mendix Cloud Portal For apps deployed to Mendix Cloud, configure via the Cloud Portal: 1. Go to your app in the Mendix Cloud Portal. 2. Navigate to Environment → Security. 3. Enable SSO and select "OpenID Connect". 4. Enter the issuer URL: `https://idp.valyd.work` 5. Add your client credentials (`client_id` and `client_secret` from https://dev.valyd.work). 6. Save and restart your application. **Expected output:** SSO is enabled for the environment; after restart the login page offers OpenID Connect sign-in. ##### Method 3 — Custom Module Configuration For custom OIDC module implementations, use these constants. Replace the placeholder values: - `your-client-id` → your `client_id` (from https://dev.valyd.work → your project → Credentials) - `your-client-secret` → your `client_secret` (from the same place; shown only once at registration) - `OIDC_REDIRECT_URI` → the callback URL you registered ```javascript // OIDC Configuration Constants const OIDC_ISSUER = "https://idp.valyd.work"; const OIDC_CLIENT_ID = "your-client-id"; const OIDC_CLIENT_SECRET = "your-client-secret"; const OIDC_REDIRECT_URI = "https://your-app.mendixcloud.com/oidc/callback"; const OIDC_SCOPES = "openid profile email"; ``` **Expected output:** Your module holds the issuer, credentials, redirect URI, and scope string needed to start the authorization-code flow. #### Step 6 — Map OIDC claims to user fields Map OIDC claims to your platform's user fields: | Mendix Field | OIDC Claim | | ------------ | --------------------------- | | Username | `preferred_username` or `sub` | | Email | `email` | | Name | `name` | | First Name | `first_name` | | Last Name | `last_name` | **Sample userinfo response** (returned by `GET https://idp.valyd.work/api/auth/oidc/userinfo` with a valid access token): ```json { "sub": "user_12345", "preferred_username": "john.doe", "email": "john.doe@example.com", "email_verified": true, "name": "John Doe", "first_name": "John", "last_name": "Doe", "picture": "https://example.com/avatar.jpg" } ``` **Expected output:** Each platform user field is populated from the corresponding claim above after a successful login. #### Step 7 — Complete example: Mendix SSO JSON configuration Complete configuration example for Mendix SSO. Replace `mendix-app-123` with your `client_id` and `your-secret-here` with your `client_secret` (both from https://dev.valyd.work), and adjust `redirect_uri` to your app's callback URL: ```json { "sso": { "enabled": true, "provider": "openid_connect", "config": { "issuer": "https://idp.valyd.work", "client_id": "mendix-app-123", "client_secret": "your-secret-here", "authorization_endpoint": "https://idp.valyd.work/api/auth/oidc/authorize", "token_endpoint": "https://idp.valyd.work/api/auth/oidc/token", "userinfo_endpoint": "https://idp.valyd.work/api/auth/oidc/userinfo", "jwks_uri": "https://idp.valyd.work/api/auth/oidc/jwks.json", "scopes": ["openid", "email", "profile"], "redirect_uri": "https://your-app.mendixcloud.com/oidc/callback", "token_endpoint_auth_method": "client_secret_post", "id_token_signing_alg": "RS256", "user_mapping": { "username": "preferred_username", "email": "email", "name": "name", "first_name": "first_name", "last_name": "last_name" } } } } ``` **Expected output:** A valid Mendix SSO config block your platform can load to enable OpenID Connect sign-in with Valyd. #### Step 8 — Test the full login flow Walk through the flow end to end: 1. **Open your application** — Navigate to your deployed app's login page. 2. **Click Login** — Click the SSO or "Login with Valyd" button. 3. **Redirect to authorization** — You should be redirected to `https://idp.valyd.work/api/auth/oidc/authorize`. 4. **Authenticate** — Log in with your Valyd credentials. 5. **Consent screen** — Approve the requested scopes if prompted. 6. **Callback redirect** — You're redirected back to your app with an authorization code. 7. **Token exchange** — Your app exchanges the code for `access_token`, `id_token`, and `refresh_token`. 8. **User logged in** — User session is created with mapped profile data. **Expected output:** You land back in your app authenticated, with a session whose profile fields are populated from the OIDC claims. --- ### Verification ```text 1. Verify discovery is reachable: curl -s https://idp.valyd.work/api/.well-known/openid-configuration → Expect HTTP 200 and a JSON body whose "issuer" is "https://idp.valyd.work". 2. Verify JWKS is reachable (needed for ID token signature validation): curl -s https://idp.valyd.work/api/auth/oidc/jwks.json → Expect HTTP 200 and a JSON body containing a "keys" array. 3. Verify the end-to-end login (Step 8): → A click on Login redirects to https://idp.valyd.work/api/auth/oidc/authorize, and after authenticating you return to your app logged in with mapped fields. 4. Verify userinfo returns mapped claims: → With a valid access token, GET https://idp.valyd.work/api/auth/oidc/userinfo returns 200 with sub, preferred_username, email, name, first_name, last_name. ``` --- ### Common errors **Invalid redirect_uri** - **Cause:** The redirect URI in your request doesn't match what's registered. - **Fix:** Ensure the `redirect_uri` matches EXACTLY (no trailing slashes, correct protocol) the value registered in the Developer Portal at https://dev.valyd.work. **Invalid client credentials** - **Cause:** Wrong `client_id` or `client_secret`. - **Fix:** Verify credentials in the Developer Portal (https://dev.valyd.work). If the secret is lost, regenerate it (it is shown only once). **User mapping failed** - **Cause:** Expected claims are missing from the ID token or userinfo. - **Fix:** Check your scope configuration includes `profile` and `email`. **ID token validation failed** - **Cause:** Token signature verification failed or token is expired. - **Fix:** Ensure your server time is synchronized. Verify the JWKS endpoint (https://idp.valyd.work/api/auth/oidc/jwks.json) is accessible. **Discovery endpoint failed** - **Cause:** Cannot reach the `.well-known/openid-configuration` URL. - **Fix:** Check network connectivity to https://idp.valyd.work. Ensure no firewall blocks the request. --- ## Security Best Practices - **Never expose client_secret** — Keep it server-side only. Never include it in frontend code or version control. - **Always use HTTPS** — All OAuth/OIDC traffic must be encrypted. Never use HTTP. - **Validate redirect URIs strictly** — Register exact URIs. Don't use wildcards in production. - **Rotate secrets regularly** — Implement a secret rotation schedule (every 90 days recommended). ### Token Expiry Information | Token | Expiry | | ------------- | ---------- | | ID Token | 15 minutes | | Access Token | 1 hour | | Refresh Token | 30 days | ================================================================================ === FILE: https://docs.valyd.work/docs/endpoints.md === ================================================================================ > Source: https://docs.valyd.work/docs/endpoints > Part of: Valyd ID API documentation — static copy generated for AI agents > Generated from repo component: EndpointsSection.tsx # API Reference ## Agent Quick-Start - Source URL: https://docs.valyd.work/docs/endpoints - Credentials / env vars needed: CLIENT_ID, CLIENT_SECRET, ACCESS_TOKEN, REFRESH_TOKEN, AUTH_CODE - Files an integrator edits: none — reference only (consumed by your backend route handlers) - Estimated steps: N/A (reference) - Can complete without human input: NO — CLIENT_ID and CLIENT_SECRET must be obtained by a human from the Developer Portal (https://dev.valyd.work) - Prerequisites: - A registered application with a Client ID and Client Secret (get these from the Developer Portal: https://dev.valyd.work) - All requests must be made over HTTPS - Authenticated endpoints require a Bearer access token in the `Authorization` header - The base URL for every endpoint below is `https://idp.valyd.work/api/auth/tpsso` ## General notes - 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 (`createLoginSession()`, `verifyLoginSession(marker)`, `getAuthorizationUrl()`, `exchangeCode`) — they call these endpoints for you. - Base URL (used by every endpoint below): `https://idp.valyd.work/api/auth/tpsso` ## SDK methods (v0.2.0) ### `valyd.createLoginSession()` (New in 0.2.0) Issues a one-time login session. Call **before** redirecting the user to Valyd. Returns `{ authorizeState, marker }`. - **authorizeState** — pass as `state` in `getAuthorizationUrl()`. - **marker** — HMAC-signed string. Store server-side (httpOnly cookie or session). Never expose to the browser JS. - **TTL** — 10 minutes. ### `valyd.verifyLoginSession(marker)` (New in 0.2.0) Validates the marker on the callback, **before** `exchangeCode`. Returns `{ valid: boolean }`. Never throws on an invalid marker. - Use this as your CSRF check. Do **not** compare callback `state` to anything. - Returns `{ valid: false }` for expired, missing, or tampered markers. --- ## POST /token — Exchange Code for Tokens - **Method:** POST - **Full URL:** `https://idp.valyd.work/api/auth/tpsso/token` - **Base URL:** `https://idp.valyd.work/api/auth/tpsso` - **Path:** `/token` - **Auth / required scope:** None (authenticated via `client_id` + `client_secret` in the request body) - **Required headers:** - `Content-Type: application/json` - `Accept: application/json` Exchange the one-time authorization code you received on your callback URL for access and refresh tokens. This should be called from your backend server. ### Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `grant_type` | string | Yes | Must be "authorization_code" | | `client_id` | string | Yes | Your assigned Client ID (get this from the Developer Portal → your project → Credentials: https://dev.valyd.work) | | `client_secret` | string | Yes | Your Client Secret (server-side only!) (get this from the Developer Portal → your project → Credentials: https://dev.valyd.work) | | `code` | string | Yes | The authorization code from callback | ### Request body (application/json) ```json { "grant_type": "authorization_code", "client_id": "YOUR_CLIENT_ID", "client_secret": "YOUR_CLIENT_SECRET", "code": "AUTH_CODE_FROM_CALLBACK" } ``` `YOUR_CLIENT_ID` / `YOUR_CLIENT_SECRET`: get these from the Developer Portal → your project → Credentials: https://dev.valyd.work `AUTH_CODE_FROM_CALLBACK`: the one-time authorization code delivered to your registered callback URL after the user authenticates. ### Code examples ```bash curl -X POST "https://idp.valyd.work/api/auth/tpsso/token" \ -H "Content-Type: application/json" \ -H "Accept: application/json" \ -d '{ "grant_type": "authorization_code", "client_id": "YOUR_CLIENT_ID", "client_secret": "YOUR_CLIENT_SECRET", "code": "AUTH_CODE_FROM_CALLBACK" }' ``` ```javascript const response = await fetch("https://idp.valyd.work/api/auth/tpsso/token", { method: "POST", headers: { "Content-Type": "application/json", "Accept": "application/json" }, body: JSON.stringify({ grant_type: "authorization_code", client_id: "YOUR_CLIENT_ID", client_secret: "YOUR_CLIENT_SECRET", code: authCode }) }); const data = await response.json(); const { access_token, refresh_token } = data.data; ``` ```python import requests response = requests.post( "https://idp.valyd.work/api/auth/tpsso/token", json={ "grant_type": "authorization_code", "client_id": "YOUR_CLIENT_ID", "client_secret": "YOUR_CLIENT_SECRET", "code": auth_code }, headers={ "Content-Type": "application/json", "Accept": "application/json" } ) data = response.json() access_token = data["data"]["access_token"] refresh_token = data["data"]["refresh_token"] ``` ```php "https://idp.valyd.work/api/auth/tpsso/token", CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode([ "grant_type" => "authorization_code", "client_id" => "YOUR_CLIENT_ID", "client_secret" => "YOUR_CLIENT_SECRET", "code" => $authCode ]), CURLOPT_HTTPHEADER => [ "Content-Type: application/json", "Accept: application/json" ], CURLOPT_RETURNTRANSFER => true ]); $response = curl_exec($ch); $data = json_decode($response, true); $accessToken = $data["data"]["access_token"]; ?> ``` ```java HttpClient client = HttpClient.newHttpClient(); String body = """ { "grant_type": "authorization_code", "client_id": "YOUR_CLIENT_ID", "client_secret": "YOUR_CLIENT_SECRET", "code": "%s" } """.formatted(authCode); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://idp.valyd.work/api/auth/tpsso/token")) .header("Content-Type", "application/json") .header("Accept", "application/json") .POST(HttpRequest.BodyPublishers.ofString(body)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); // Parse response with your preferred JSON library ``` ### Expected output **Response — 200 OK** (Success Response): ```json { "success": true, "data": { "access_token": "eyJhbGciOi...", "token_type": "Bearer", "expires_in": 900, "refresh_token": "rfrsh_abc123...", "user": { "id": 123, "email": "user@example.com", "username": "john_doe", "name": "John Doe", "valyd_id": "valyd_225c7f2ac450496f97bbbc57354a5898", "avatar_url": null, "created_at": "2025-09-11T10:15:00Z" } } } ``` **Response — Error** (Error Response): ```json { "success": false, "error": { "code": "invalid_client", "message": "client_id/client_secret invalid" } } ``` --- ## GET /userinfo — Get User Profile - **Method:** GET - **Full URL:** `https://idp.valyd.work/api/auth/tpsso/userinfo` - **Base URL:** `https://idp.valyd.work/api/auth/tpsso` - **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 /token` (or `POST /refresh`). ### Code examples ```bash curl -X GET "https://idp.valyd.work/api/auth/tpsso/userinfo" \ -H "Accept: application/json" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ```javascript const response = await fetch("https://idp.valyd.work/api/auth/tpsso/userinfo", { method: "GET", headers: { "Accept": "application/json", "Authorization": `Bearer ${accessToken}` } }); const data = await response.json(); const user = data.data; console.log(user.full_name, user.email); ``` ```python import requests response = requests.get( "https://idp.valyd.work/api/auth/tpsso/userinfo", headers={ "Accept": "application/json", "Authorization": f"Bearer {access_token}" } ) user = response.json()["data"] print(user["full_name"], user["email"]) ``` ```php "https://idp.valyd.work/api/auth/tpsso/userinfo", CURLOPT_HTTPHEADER => [ "Accept: application/json", "Authorization: Bearer " . $accessToken ], CURLOPT_RETURNTRANSFER => true ]); $response = curl_exec($ch); $user = json_decode($response, true)["data"]; echo $user["full_name"]; ?> ``` ```java HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://idp.valyd.work/api/auth/tpsso/userinfo")) .header("Accept", "application/json") .header("Authorization", "Bearer " + accessToken) .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); ``` ### Expected output **Response — 200 OK** (Success Response): ```json { "success": true, "data": { "sub": "valyd_225c7f2ac450496f97bbbc57354a5898", "email": "user@example.com", "first_name": "John", "last_name": "Doe", "full_name": "John Doe", "valyd_id": "valyd_225c7f2ac450496f97bbbc57354a5898", "id_verified": true, "created_at": "2025-09-10T12:00:00Z" } } ``` **Response — Error** (Error Response): ```json { "success": false, "error": { "code": "insufficient_scope", "message": "The request requires the profile scope" } } ``` --- ## GET /licenses — Get Professional Licenses - **Method:** GET - **Full URL:** `https://idp.valyd.work/api/auth/tpsso/licenses` - **Base URL:** `https://idp.valyd.work/api/auth/tpsso` - **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 /token` (or `POST /refresh`). ### Code examples ```bash curl -X GET "https://idp.valyd.work/api/auth/tpsso/licenses" \ -H "Accept: application/json" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ```javascript const response = await fetch("https://idp.valyd.work/api/auth/tpsso/licenses", { method: "GET", headers: { "Accept": "application/json", "Authorization": `Bearer ${accessToken}` } }); const data = await response.json(); const licenses = data.data.licenses; licenses.forEach(license => { console.log(`${license.type}: ${license.status}`); }); ``` ```python import requests response = requests.get( "https://idp.valyd.work/api/auth/tpsso/licenses", headers={ "Accept": "application/json", "Authorization": f"Bearer {access_token}" } ) licenses = response.json()["data"]["licenses"] for license in licenses: print(f"{license['type']}: {license['status']}") ``` ```php "https://idp.valyd.work/api/auth/tpsso/licenses", CURLOPT_HTTPHEADER => [ "Accept: application/json", "Authorization: Bearer " . $accessToken ], CURLOPT_RETURNTRANSFER => true ]); $response = curl_exec($ch); $licenses = json_decode($response, true)["data"]["licenses"]; foreach ($licenses as $license) { echo $license["type"] . ": " . $license["status"] . "\n"; } ?> ``` ```java HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://idp.valyd.work/api/auth/tpsso/licenses")) .header("Accept", "application/json") .header("Authorization", "Bearer " + accessToken) .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); // Parse licenses from response ``` ### Expected output **Response — 200 OK** (Success 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" } ] } } ``` **Response — Error** (Error Response): ```json { "success": false, "error": { "code": "invalid_token", "message": "token invalid/expired" } } ``` --- ## GET /verifications — Get Identity Verifications - **Method:** GET - **Full URL:** `https://idp.valyd.work/api/auth/tpsso/verifications` - **Base URL:** `https://idp.valyd.work/api/auth/tpsso` - **Path:** `/verifications` - **Auth / required scope:** Bearer access token required; required scope: `verifications` - **Required headers:** - `Accept: application/json` - `Authorization: Bearer YOUR_ACCESS_TOKEN` Returns identity and verification results including ID verification status, face match confidence, and last verification timestamp. Use alongside `/userinfo` for a complete user picture. `YOUR_ACCESS_TOKEN`: the `access_token` returned by `POST /token` (or `POST /refresh`). ### Code examples ```bash curl -X GET "https://idp.valyd.work/api/auth/tpsso/verifications" \ -H "Accept: application/json" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ```javascript const response = await fetch("https://idp.valyd.work/api/auth/tpsso/verifications", { method: "GET", headers: { "Accept": "application/json", "Authorization": `Bearer ${accessToken}` } }); const data = await response.json(); const { id_verified, face_match } = data.data.verifications; if (id_verified && face_match > 0.9) { console.log("User is fully verified!"); } ``` ```python import requests response = requests.get( "https://idp.valyd.work/api/auth/tpsso/verifications", headers={ "Accept": "application/json", "Authorization": f"Bearer {access_token}" } ) verifications = response.json()["data"]["verifications"] if verifications["id_verified"] and verifications["face_match"] > 0.9: print("User is fully verified!") ``` ```php "https://idp.valyd.work/api/auth/tpsso/verifications", CURLOPT_HTTPHEADER => [ "Accept: application/json", "Authorization: Bearer " . $accessToken ], CURLOPT_RETURNTRANSFER => true ]); $response = curl_exec($ch); $verifications = json_decode($response, true)["data"]["verifications"]; if ($verifications["id_verified"] && $verifications["face_match"] > 0.9) { echo "User is fully verified!"; } ?> ``` ```java HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://idp.valyd.work/api/auth/tpsso/verifications")) .header("Accept", "application/json") .header("Authorization", "Bearer " + accessToken) .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); // Parse verifications from response ``` ### Expected output **Response — 200 OK** (Success Response): ```json { "success": true, "data": { "verifications": { "id_verified": true, "face_match": 0.98, "last_checked": "2025-09-11T12:00:00Z" } } } ``` **Response — Error** (Error Response): ```json { "success": false, "error": { "code": "insufficient_scope", "message": "The request requires the verifications scope" } } ``` --- ## POST /refresh — Refresh Access Token - **Method:** POST - **Full URL:** `https://idp.valyd.work/api/auth/tpsso/refresh` - **Base URL:** `https://idp.valyd.work/api/auth/tpsso` - **Path:** `/refresh` - **Auth / required scope:** None (authenticated via the `refresh_token` in the request body) - **Required headers:** - `Content-Type: application/json` - `Accept: application/json` Use your `refresh_token` to obtain a new `access_token` when it expires. Optionally rotate the refresh token for enhanced security. ### Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `refresh_token` | string | Yes | Your current refresh token | | `rotate_refresh` | boolean | No | Set to true to also get a new refresh token | ### Request body (application/json) ```json { "refresh_token": "rfrsh_abc123...", "rotate_refresh": true } ``` `refresh_token`: the `refresh_token` previously returned by `POST /token` (or by a prior `POST /refresh` when `rotate_refresh` was true). ### Code examples ```bash curl -X POST "https://idp.valyd.work/api/auth/tpsso/refresh" \ -H "Content-Type: application/json" \ -H "Accept: application/json" \ -d '{ "refresh_token": "rfrsh_abc123...", "rotate_refresh": true }' ``` ```javascript const response = await fetch("https://idp.valyd.work/api/auth/tpsso/refresh", { method: "POST", headers: { "Content-Type": "application/json", "Accept": "application/json" }, body: JSON.stringify({ refresh_token: refreshToken, rotate_refresh: true // Recommended for security }) }); const data = await response.json(); const { access_token, refresh_token } = data.data.tokens; // Update stored tokens localStorage.setItem("access_token", access_token); localStorage.setItem("refresh_token", refresh_token); ``` ```python import requests response = requests.post( "https://idp.valyd.work/api/auth/tpsso/refresh", json={ "refresh_token": refresh_token, "rotate_refresh": True # Recommended for security }, headers={ "Content-Type": "application/json", "Accept": "application/json" } ) tokens = response.json()["data"]["tokens"] access_token = tokens["access_token"] refresh_token = tokens["refresh_token"] ``` ```php "https://idp.valyd.work/api/auth/tpsso/refresh", CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode([ "refresh_token" => $refreshToken, "rotate_refresh" => true ]), CURLOPT_HTTPHEADER => [ "Content-Type: application/json", "Accept: application/json" ], CURLOPT_RETURNTRANSFER => true ]); $response = curl_exec($ch); $tokens = json_decode($response, true)["data"]["tokens"]; $accessToken = $tokens["access_token"]; $refreshToken = $tokens["refresh_token"]; ?> ``` ```java HttpClient client = HttpClient.newHttpClient(); String body = """ { "refresh_token": "%s", "rotate_refresh": true } """.formatted(refreshToken); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://idp.valyd.work/api/auth/tpsso/refresh")) .header("Content-Type", "application/json") .header("Accept", "application/json") .POST(HttpRequest.BodyPublishers.ofString(body)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); ``` ### Expected output **Response — 200 OK** (Success Response): ```json { "success": true, "data": { "tokens": { "access_token": "eyJhbGciOi...", "token_type": "Bearer", "expires_in": 900, "refresh_token": "rfrsh_new456..." } } } ``` **Response — Error** (Error Response): ```json { "success": false, "error": { "code": "invalid_grant", "message": "refresh_token is invalid or expired" } } ``` ================================================================================ === FILE: https://docs.valyd.work/docs/scopes.md === ================================================================================ > Source: https://docs.valyd.work/docs/scopes > Part of: Valyd ID API documentation — static copy generated for AI agents > Generated from repo component: ScopesSection.tsx # OAuth2 Scopes ## Agent Quick-Start - Source URL: https://docs.valyd.work/docs/scopes - Credentials / env vars needed: CLIENT_ID, REDIRECT_URI (access token also required to call scoped endpoints) - Files an integrator edits: the code that builds your authorization URL (e.g. server route handler or auth helper) - Estimated steps: 2 (choose scopes, add them space-separated and URL-encoded to the authorization URL) - Can complete without human input: NO — each requested scope must first be enabled for your project in the Developer Portal (https://dev.valyd.work) by a human. - Prerequisites: - A registered project in the Developer Portal: https://dev.valyd.work - The scopes you intend to request are enabled for that project in the portal - A `client_id` and registered `redirect_url` for the project Valyd uses OAuth2 scope-based access control. When initiating the authorization flow, you specify which permissions your application needs. Users see these scopes on the consent screen before granting access. ## Scopes summary | Scope | Required | Description | Grants access to | | --- | --- | --- | --- | | `profile` | Yes (Mandatory) | User profile information including name, photo, and age verification status | `/userinfo` | | `verifications` | Optional | Identity verification data including ID verification status, face match confidence, and verification timestamps | `/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 Include the `scope` parameter in your authorization URL. Multiple scopes must be **space-separated** and URL-encoded. ```javascript const clientId = "YOUR_CLIENT_ID"; // get this from the Developer Portal → your project → Credentials: https://dev.valyd.work const redirectUrl = "YOUR_REDIRECT_URI"; // must match a redirect URL registered for your project in the Developer Portal: https://dev.valyd.work const scopes = "profile verifications zkp"; // Space-separated const authURL = `https://idp.valyd.work/auth?client_id=${clientId}&redirect_url=${redirectUrl}&scope=${encodeURIComponent(scopes)}`; // Result: scope=profile%20verifications%20zkp ``` Expected output: a fully-formed authorization URL string. Redirecting the user to it shows the Valyd consent screen listing exactly the scopes you requested. ## Scope enforcement - Scopes are verified against your project settings in the Developer Portal. - If you request a scope not enabled for your project, 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 project. Enable it in the Developer Portal → your project → Scopes: https://dev.valyd.work 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 information including name, photo, and age verification status. ### Grants access to - `/userinfo` ### Response fields | Field | Description | | --- | --- | | `sub` | Unique user identifier | | `email` | User's email address | | `first_name` | User's first name | | `last_name` | User's last name | | `full_name` | User's full name | | `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" } } ``` --- ## `verifications` scope (Optional) Identity verification data including ID verification status, face match confidence, and verification timestamps. ### Grants access to - `/verifications` ### Response fields | Field | Description | | --- | --- | | `id_verified` | Whether the user's ID is verified | | `face_match` | Face match confidence score (0-1) | | `last_checked` | Timestamp of last verification check | ### 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.work/docs/errors.md === ================================================================================ > Source: https://docs.valyd.work/docs/errors > Part of: Valyd ID API documentation — static copy generated for AI agents > Generated from repo component: ErrorsSection.tsx # Error Codes Reference ## Agent Quick-Start - Source URL: https://docs.valyd.work/docs/errors - Credentials / env vars needed: none (reference page; fixes may require CLIENT_ID, CLIENT_SECRET, REDIRECT_URI) - Files an integrator edits: none — reference only (apply fixes in your auth/route-handler code as indicated per error) - Estimated steps: 0 (look up the error code, apply the listed fix) - Can complete without human input: YES — match the error `code` you received to a section below and apply its Fix. - Prerequisites: none All error responses follow a consistent format: a `success: false` flag and an `error` object containing a `code` and a human-readable `message`. ```json { "success": false, "error": { "code": "", "message": "" } } ``` To pick the right section, match on the `error.code` value (or the symptom for the SDK-level error): ```text IF code == "invalid_client": → go to "invalid_client" below (401) IF code == "invalid_token": → go to "invalid_token" below (401) IF code == "insufficient_scope": → go to "insufficient_scope" below (403) IF code == "invalid_grant": → go to "invalid_grant" below (400) IF code == "invalid_request": → go to "invalid_request" below (400) IF code == "access_denied": → go to "access_denied" below (403) IF verifyLoginSession returns valid:false (no thrown error): → go to "Invalid login session" below (SDK / app-level) ``` --- ## `invalid_client` - **Status:** 401 Unauthorized - **Cause:** The `client_id` or `client_secret` is invalid or doesn't match. - **Fix:** Verify your credentials in the Developer Portal (https://dev.valyd.work → your project → Credentials) and ensure you're using the correct values. The `client_id` comes from the portal; the `client_secret` is generated in the portal by a human. Example response: ```json { "success": false, "error": { "code": "invalid_client", "message": "client_id/client_secret invalid" } } ``` --- ## `invalid_token` - **Status:** 401 Unauthorized - **Cause:** The access token is invalid, malformed, or has expired. - **Fix:** Use the `/refresh` endpoint to get a new access token, or re-authenticate the user. Example response: ```json { "success": false, "error": { "code": "invalid_token", "message": "token invalid/expired" } } ``` --- ## `insufficient_scope` - **Status:** 403 Forbidden - **Cause:** The access token doesn't have the required scope for this endpoint. - **Fix:** Request the missing scope in your authorization URL (space-separated, URL-encoded `scope` parameter) and have the user re-authenticate. The scope must also be enabled for your project in the Developer Portal: https://dev.valyd.work Example response: ```json { "success": false, "error": { "code": "insufficient_scope", "message": "The request requires the profile scope" } } ``` --- ## `invalid_grant` - **Status:** 400 Bad Request - **Cause:** The authorization code or refresh token is invalid, expired, already used, or the `redirect_uri` doesn't match the one registered in the portal. - **Fix:** Authorization codes expire after 5 minutes and can only be used once. Verify your redirect URI matches exactly the one registered in the Developer Portal (https://dev.valyd.work). For refresh tokens, have the user re-authenticate. Example response: ```json { "success": false, "error": { "code": "invalid_grant", "message": "refresh_token is invalid or expired" } } ``` --- ## `invalid_request` - **Status:** 400 Bad Request - **Cause:** The request is missing required parameters or has invalid parameter values. - **Fix:** Check the API documentation for required parameters and ensure all values are properly formatted. Example response: ```json { "success": false, "error": { "code": "invalid_request", "message": "Missing required parameter: code" } } ``` --- ## `access_denied` - **Status:** 403 Forbidden - **Cause:** The user denied the authorization request on the consent screen. - **Fix:** The user chose not to grant access. You may prompt them to try again or explain why the permissions are needed. Example response: ```json { "success": false, "error": { "code": "access_denied", "message": "User denied the authorization request" } } ``` --- ## Invalid login session - **Status:** SDK / app-level (no HTTP error code; the SDK call returns `{ valid: false }` and never throws on invalid markers) - **Cause:** `valyd.verifyLoginSession(marker)` returned `{ valid: false }`. Possible reasons: - The login session expired (10-minute TTL). - The marker cookie is missing. - The marker was tampered with. - You're verifying with a different `clientSecret` than the one that created the session. - **Fix:** Send the user back through `/login` to issue a fresh login session. Make sure the marker is stored server-side (httpOnly cookie or session) and survives the full redirect round-trip. Do NOT try to compare the callback `state` — that is Valyd's session id, not your CSRF token. Example (SDK call): ```ts // SDK call const { valid } = await valyd.verifyLoginSession(marker); // { valid: false } — never throws on invalid markers ``` ================================================================================ === FILE: https://docs.valyd.work/docs/changelog.md === ================================================================================ > Source: https://docs.valyd.work/docs/changelog > Part of: Valyd ID API documentation — static copy generated for AI agents > Generated from repo component: ChangelogSection.tsx # Changelog ## Agent Quick-Start - Source URL: https://docs.valyd.work/docs/changelog - Credentials / env vars needed: none - Files an integrator edits: none — reference only - Estimated steps: 0 (read to confirm which SDK version introduces the helper you need) - Can complete without human input: YES — this is a read-only version history. - Prerequisites: none This page tracks releases of the Valyd SDK. Tags: **Added** (new functionality), **Docs** (documentation-only changes), **Breaking (docs)** (a documented pattern was removed or changed). --- ## v0.2.0 — Login sessions for TPSSO - **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.work/verify/intro.md === ================================================================================ > Source: https://docs.valyd.work/verify#intro > Part of: Valyd Verify API documentation — static copy generated for AI agents > Generated from repo component: IntroSection.tsx # Valyd Verify ## Agent Quick-Start - Source URL: https://docs.valyd.work/verify#intro - Credentials / env vars needed: none (this is an overview; you need an App API key from the Developer Portal before making calls — get it at https://dev.valyd.work) - Files an integrator edits: none — reference only - Estimated steps: 0 (conceptual overview) - Can complete without human input: YES — reading/reference only, no actions required - Prerequisites: none Valyd Verify is an identity-verification platform with two integration modes. Pick Hosted if you want Valyd to handle the camera capture and UI, or Core APIs if you want to call REST endpoints server-to-server with your own UI and data. ## Integration modes ### Hosted Redirect your user to Valyd's hosted page for ID + selfie capture. Receive the result via a signed webhook and a decision API call. No UI to build. ### Core APIs Call per-capability REST endpoints server-to-server with your own UI and data. Synchronous JSON result returned on the same request. To choose between them, see https://docs.valyd.work/verify#modes (modes.md). ## Services | Service | Description | | --- | --- | | `id_verification` | KYC / OCR from a government ID | | `liveness` | Passive liveness check | | `face_match` | Selfie vs ID portrait | | `age` | Age band checks from DOB | | `credential` | Professional license lookup | ## Base URL ```text https://idp.valyd.work ``` ## Response envelope Every API response uses this envelope. `success` is always present; `error` is only present when `success` is `false`. ```json { "success": true, "data": {}, "error": { "code": "string", "message": "string" } } ``` Note: the `error` object is only included in the response when `success` is `false`. On success, only `success` and `data` are present. ================================================================================ === FILE: https://docs.valyd.work/verify/quickstart.md === ================================================================================ > Source: https://docs.valyd.work/verify#quickstart > Part of: Valyd Verify API documentation — static copy generated for AI agents > Generated from repo component: QuickstartSection.tsx # Valyd Verify Quickstart ## Agent Quick-Start - Source URL: https://docs.valyd.work/verify#quickstart - Credentials / env vars needed: VALYD_API_KEY, VALYD_WORKFLOW_ID (Hosted only) - Files an integrator edits: .env (to store VALYD_API_KEY and VALYD_WORKFLOW_ID), server route handler (to make the API call and, for Hosted, handle the webhook) - Estimated steps: 6 - Can complete without human input: NO — steps 1-4 require a human to sign in with Valyd SSO, copy the one-time API key, create a Workflow, and configure the webhook in the Developer Portal (https://dev.valyd.work). The API call itself (steps 5-6) can be automated once credentials exist. - Prerequisites: - A Valyd SSO account able to sign in to the Developer Portal - An App API key copied from the Console (shown once at creation) - For Hosted: a `workflow_id` from a created Workflow - For Hosted: a publicly reachable webhook URL and signing secret ### Prerequisites - Access to the Developer Portal at https://dev.valyd.work (sign in with Valyd SSO). - The App API key, copied at App creation (shown once). Store it server-side only. - For the Hosted snippet: a `workflow_id` from a Workflow you created in the Console. ### Steps 1. Sign in to the Developer Portal with Valyd SSO. The Developer Portal is the ONE console: the same sign-in issues your OAuth app (client_id / client_secret), your Verify app (API key) and your workflows. (Human-only step.) ```text Open https://dev.valyd.work and sign in with Valyd SSO. ``` **Expected output:** You are signed in and a default App is visible in the Console. 2. Copy the App API key (shown once at creation). Keep it server-side only. (Human-only step.) Then store it in your environment. ```bash export VALYD_API_KEY="paste-the-one-time-app-api-key-here" ``` (Get the API key from the Developer Portal → your App → it is shown once at creation: https://dev.valyd.work) **Expected output:** `VALYD_API_KEY` is set in your shell/`.env`. The key cannot be retrieved again after creation — rotate it in the Console if lost. 3. (Hosted only) Create a Workflow and copy its `workflow_id`. (Human-only step.) Then store it. ```bash export VALYD_WORKFLOW_ID="paste-your-workflow-id-here" ``` (Get the `workflow_id` from the Developer Portal → Workflows → your Workflow: https://dev.valyd.work) **Expected output:** `VALYD_WORKFLOW_ID` is set. Required only for the Hosted session call in step 6. 4. (Hosted only) Set your webhook URL and signing secret under Webhooks in the Console. (Human-only step.) ```text In the Console → Webhooks: set the endpoint URL (e.g. https://your-app.com/api/valyd-webhook) and copy the signing secret. ``` **Expected output:** Valyd will POST signed events to your URL when a session reaches a terminal state. 5. Run your first Core APIs call (age verification) to confirm your API key works. ```bash curl -X POST https://idp.valyd.work/api/v2/age-verification \ -H "X-API-Key: $VALYD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "dob": "1995-06-01", "bands": ["is_18_plus"] }' ``` **Expected output:** HTTP `200` with the standard envelope, e.g. `{ "success": true, "data": { ... } }`. On a bad/missing key expect a `4xx` with `{ "success": false, "error": { "code": "...", "message": "..." } }`. 6. (Hosted only) Create a Hosted session from your server, then redirect the user's browser to the returned URL. ```javascript const res = await fetch("https://idp.valyd.work/api/v2/session", { method: "POST", headers: { "X-API-Key": process.env.VALYD_API_KEY, "Content-Type": "application/json", }, body: JSON.stringify({ workflow_id: process.env.VALYD_WORKFLOW_ID, redirect_url: "https://your-app.com/verify/result", callback: "https://your-app.com/api/valyd-webhook", vendor_data: "user-123", }), }); const { data } = await res.json(); // Redirect the user's browser to data.url ``` **Expected output:** HTTP `200` with `{ "success": true, "data": { "url": "https://..." } }`. Redirect the user's browser to `data.url`. The verification result arrives later via your configured webhook (step 4). ### Verification - Core APIs: the curl in step 5 returns HTTP `200` and a body where `success` is `true`. ```bash curl -s -o /dev/null -w "%{http_code}\n" -X POST https://idp.valyd.work/api/v2/age-verification \ -H "X-API-Key: $VALYD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "dob": "1995-06-01", "bands": ["is_18_plus"] }' ``` Expect `200` printed. - Hosted: the step-6 call returns a non-empty `data.url`. Confirm `echo $VALYD_WORKFLOW_ID` is non-empty before calling. ### Common errors 1. **401 / 403 Unauthorized** - **Cause:** Missing, wrong, or rotated `X-API-Key`, or the key was sent client-side. - **Fix:** Re-copy the API key from the Console (or rotate it), set `VALYD_API_KEY` server-side, and send it in the `X-API-Key` header. Never expose it in browser code. 2. **400 Bad Request on /api/v2/session** - **Cause:** Missing or invalid `workflow_id` (Hosted requires a real Workflow id), or malformed JSON body. - **Fix:** Create a Workflow in the Console, set `VALYD_WORKFLOW_ID`, and verify `echo $VALYD_WORKFLOW_ID` is non-empty. Ensure `Content-Type: application/json` and valid JSON. 3. **No webhook received after a Hosted session** - **Cause:** Webhook URL/signing secret not configured, or your endpoint is not publicly reachable. - **Fix:** Set the webhook URL and signing secret in Console → Webhooks (step 4), ensure the URL is publicly reachable, and verify the signature using the signing secret before trusting the event. ================================================================================ === FILE: https://docs.valyd.work/verify/console.md === ================================================================================ > Source: https://docs.valyd.work/verify#console > Part of: Valyd Verify API documentation — static copy generated for AI agents > Generated from repo component: ConsoleSection.tsx # The Developer Portal ## Agent Quick-Start - Source URL: https://docs.valyd.work/verify#console - Credentials / env vars needed: none to read this page; using the Console produces the App ID, App API key, `workflow_id`, and webhook signing secret your integration will need - Files an integrator edits: none — reference only (the Console is a web UI) - Estimated steps: 0 (reference; the actions described are human-only UI steps) - Can complete without human input: NO — every action here is performed by a human in the web UI at https://dev.valyd.work (Valyd SSO sign-in, creating Apps/Workflows, configuring webhooks, copying secrets) - Prerequisites: - A Valyd SSO account - A browser to reach https://dev.valyd.work The console lives at https://dev.valyd.work — **the Developer Portal is the one and only console**. There is no separate Verify dashboard: one sign-in issues **everything** your integration needs. | What you get | Where | | --- | --- | | `client_id` + `client_secret` (Login with Valyd / OAuth2) | Developer Portal → your app | | Verify **API key** (`vrf_…`, shown once) + webhook signing secret (`whsec_…`) | Developer Portal → Verify projects | | `workflow_id` — create / edit / delete workflows | Developer Portal → Verify projects → Workflows | | Balance, top-ups, session history | Developer Portal | The actions below are performed by a human in the Console web UI; they cannot be automated by an agent. The values they produce (App ID, API key, `workflow_id`, signing secret) are what your code then uses. ## Apps One app in the Developer Portal carries both identities: the OAuth `client_id` / `client_secret` for **Login 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 services (ID, liveness, face match, and the other Verify services) into a reusable Workflow. Each Workflow has a `workflow_id` used when creating Hosted sessions. - The `workflow_id` is required as the `workflow_id` field when creating a Hosted session (see https://docs.valyd.work/verify#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.work 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 Hosted mode: → Create a Workflow bundling the services you need and copy its workflow_id → Under Webhooks, set the endpoint URL and copy the signing secret IF you will use Core APIs mode only: → You only need the App API key; Workflows and Webhooks are not required ``` ================================================================================ === FILE: https://docs.valyd.work/verify/modes.md === ================================================================================ > Source: https://docs.valyd.work/verify#modes > Part of: Valyd Verify API documentation — static copy generated for AI agents > Generated from repo component: ModesSection.tsx # Hosted vs Core APIs ## Agent Quick-Start - Source URL: https://docs.valyd.work/verify#modes - Credentials / env vars needed: none to choose a mode (each mode later needs an App API key from https://dev.valyd.work; Hosted also needs a `workflow_id`) - Files an integrator edits: none — reference only (decision aid for picking a mode) - Estimated steps: 0 (decision aid) - Can complete without human input: YES — choosing a mode is a decision based on your requirements; no actions required - Prerequisites: none Valyd Verify offers two integration modes. Use the decision tree to pick one, then follow the matching path. ## Decision tree ```text IF you need an end-user KYC flow with live camera/ID + selfie capture, and you do NOT want to build the capture UI: → Use Hosted. Redirect the user to a Valyd-hosted session URL; receive the result via webhook + decision API. → Next: create a Workflow (get workflow_id) and configure a webhook in the Console (https://docs.valyd.work/verify#console), then create a session (https://docs.valyd.work/verify#quickstart). IF you want to call individual capabilities server-to-server from your backend with your own UI and data, and want a synchronous result: → Use Core APIs. Call the per-endpoint REST API (e.g. POST /api/v2/age-verification) with your App API key. → Next: get an App API key from the Console (https://docs.valyd.work/verify#console), then call the endpoint (https://docs.valyd.work/verify#quickstart). IF your use case is backoffice, batch processing, or a fully custom UX (no camera redirect): → Use Core APIs. IF unsure: → If a human end-user must take a live selfie/photo of their ID in a browser, choose Hosted. → Otherwise choose Core APIs. ``` ## Comparison | Aspect | Hosted | Core APIs | | --- | --- | --- | | UI | Valyd-hosted capture page | You build it | | Trigger | Redirect user to session URL | Server-to-server REST call | | Result delivery | Webhook + decision API | Synchronous response | | Best for | End-user KYC flows with camera | Backoffice, batch, custom UX | | Identifier | `workflow_id` (bundle of services) | Per-endpoint call | ================================================================================ === FILE: https://docs.valyd.work/verify/hosted.md === ================================================================================ > Source: https://docs.valyd.work/verify#hosted > Part of: Valyd Verify API documentation — static copy generated for AI agents > Generated from repo component: HostedSection.tsx # Hosted Verification ## Agent Quick-Start - Source URL: https://docs.valyd.work/verify#hosted - Credentials / env vars needed: VALYD_API_KEY, VALYD_WEBHOOK_SECRET, VALYD_WORKFLOW_ID (workflow created in the Developer Portal: https://dev.valyd.work), APP_URL - Files an integrator edits: .env (credentials), server route handlers (session create, redirect callback, webhook receiver) - Estimated steps: 4 (create session → redirect → handle redirect-back → read authoritative result) - Can complete without human input: NO — a human must create the App API key, the webhook signing secret, and a workflow in the Developer Portal to obtain a `workflow_id`. - Prerequisites: - An App API key (`X-API-Key`) — server-side only, never exposed to the browser. Get this from the Developer Portal: https://dev.valyd.work - A webhook signing secret (`VALYD_WEBHOOK_SECRET`) — get this from the Developer Portal: https://dev.valyd.work - A `workflow_id` — create the workflow in the Developer Portal (Workflows → "License Verification" or "KYC + License" preset) or via the API, then copy its `workflow_id`. - Node.js project with `valyd-verify-sdk` installed (for the SDK examples). > ALL server-to-server calls use the header `X-API-Key: `. Keep this key SERVER-SIDE ONLY — never expose it to the browser. Every response uses the envelope `{ success, data, error: { code, message } }`. > Base URL note: the component resolves the API base URL from `VERIFY_CONFIG.API_BASE_URL` = `https://idp.valyd.work`. The hosted session `url` returned by the API is also under `https://idp.valyd.work/s/…`. This page's canonical doc URL is on a different host (`https://docs.valyd.work`), which is expected for docs vs. API. --- ## Overview Hosted Verification lets you create a session, redirect the user to a Valyd-hosted page that captures everything, and receive the result via a signed webhook plus the decision API. No UI to build, no PII handled by you. The official Node SDK is `valyd-verify-sdk` (https://www.npmjs.com/package/valyd-verify-sdk). It wraps sessions, workflows, webhooks, and the decision API. Install it: ```bash npm i valyd-verify-sdk ``` Initialize the client: ```javascript import { VerifyClient } from "valyd-verify-sdk"; const verify = new VerifyClient({ apiKey: process.env.VALYD_API_KEY!, webhookSecret: process.env.VALYD_WEBHOOK_SECRET!, // used by verify.webhooks.constructEvent }); ``` Hosted flow at a glance: 1. Create a session on your server with a `workflow_id`. 2. Redirect the user's browser to the returned `url`. 3. Valyd captures everything and redirects back to your `redirect_url`. 4. Receive a signed webhook, then fetch the authoritative result from the decision API. --- ## The two hosted products Both products use the SAME integration code — only the `workflow_id` differs. Create the workflow in the Developer Portal (https://dev.valyd.work) under Workflows → "License Verification" or "KYC + License" preset, or via the API, then copy its `workflow_id`. ```text IF you only need to verify a professional license (no ID scan): → use the "License Verification" workflow_id IF you need identity + credential (ID scan + selfie + license): → use the "KYC + License" workflow_id IF unsure which workflow_id to use: → open the Developer Portal (https://dev.valyd.work) → Workflows, and copy the workflow_id of the preset you created ``` ### License Verification (badge: Credential only) - Services: `[credential]` - Hosted flow: State → license type → name + license number → verify. - Fastest path to verify a professional license. No ID scan required. ### KYC + License (badge: Identity + Credential) - Services: `[id_verification, liveness, face_match, credential]` - Hosted 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. --- ## Recipe: Integration steps The steps are identical for both products. Swap the `workflow_id` to switch between License Verification and KYC + License. ### Prerequisites - `VALYD_API_KEY` set server-side (App API key — get from the Developer Portal: https://dev.valyd.work). - `VALYD_WEBHOOK_SECRET` set server-side (webhook signing secret — get from the Developer Portal: https://dev.valyd.work). - `VALYD_WORKFLOW_ID` — the `workflow_id` of the workflow you want to run (get from the Developer Portal: https://dev.valyd.work, or create via the Workflows API below). - `APP_URL` — your application's public base URL (used to build `redirect_url` and `callback`). ### Steps #### Step 1 — Create a session (server-side) POST `/api/v2/session` from your backend. The response includes the hosted `url` you'll send the user to. cURL: ```bash curl -X POST https://idp.valyd.work/api/v2/session \ -H "X-API-Key: $VALYD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "workflow_id": "wf_…", "redirect_url": "https://app.example.com/verify/callback", "callback": "https://api.example.com/webhooks/valyd", "vendor_data": "user_123", "ttl_seconds": 900, "metadata": { "plan": "pro" } }' ``` SDK (Node): ```javascript const session = await verify.sessions.create({ workflowId: process.env.VALYD_WORKFLOW_ID!, // license-only OR kyc+license workflow 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, metadata: { plan: "pro" }, }); // session.url → redirect the user here // session.session_id, session.session_token, session.expires_at ``` Expected output: HTTP 200 with the envelope `{ success: true, data: { … } }`. The `data` object: ```json { "session_id": "ses_…", "status": "NOT_STARTED", "url": "https://idp.valyd.work/s/…", "session_token": "stk_…", "features": ["id_verification","liveness","face_match","credential"], "redirect_url": "https://app.example.com/verify/callback", "expires_at": "2026-06-11T12:00:00Z" } ``` > Placeholder source: `wf_…` / `VALYD_WORKFLOW_ID` is the `workflow_id` from the Developer Portal (https://dev.valyd.work → Workflows). `$VALYD_API_KEY` is your App API key from the same console. #### Step 2 — Redirect the user to the hosted page Send the user's browser to `data.url`. Valyd handles the entire capture and verification UI; the steps auto-adapt to the workflow's services. ```javascript // Express app.post("/start-verification", async (req, res) => { const session = await verify.sessions.create({ workflowId: process.env.VALYD_WORKFLOW_ID!, redirectUrl: `${process.env.APP_URL}/verify/callback`, callback: `${process.env.APP_URL}/webhooks/valyd`, vendorData: req.user.id, }); res.redirect(session.url); }); ``` Expected output: an HTTP 302 redirect of the user's browser to the Valyd-hosted `session.url` (under `https://idp.valyd.work/s/…`). #### Step 3 — Handle the redirect back When the user finishes (or abandons), Valyd redirects to `redirect_url?session_id=…&status=…`. Treat `status` as a HINT ONLY — always fetch the authoritative decision in step 4. ```javascript app.get("/verify/callback", async (req, res) => { const { session_id } = req.query; // Don't trust ?status= — confirm via the decision API or webhook. res.redirect(`/verify/pending?s=${session_id}`); }); ``` Expected output: your app receives a GET on `redirect_url` with query params `session_id` and `status`; you redirect the user to a pending/result page and do NOT trust the `status` query param as final. ```text IF you trust the ?status= query param as final: → DON'T. It is a hint only. IF you need the final outcome: → fetch it from the decision API (Step 4) or from the signed webhook (below). ``` #### Step 4 — Get the authoritative result Use the signed webhook (push) and/or call the decision endpoint (pull). See "Webhooks" and "Reading the decision" below. ### Verification To confirm the integration end-to-end, after a session reaches a terminal status: ```bash curl https://idp.valyd.work/api/v2/session/ses_…/decision \ -H "X-API-Key: $VALYD_API_KEY" ``` Expected output: HTTP 200 with `data.status` one of `"APPROVED" | "DECLINED" | "IN_REVIEW"` and a `data.checks` array, confirming the session was processed. ### Common errors 1. Invalid webhook signature - Cause: verifying the signature against a re-serialized JSON body, or using the wrong signing secret. - Fix: verify against the RAW request body (no JSON re-serialisation) using `express.raw({ type: "application/json" })`, and use the correct `VALYD_WEBHOOK_SECRET`. The SDK throws `ValydVerifyError` with `code === "invalid_signature"`; return HTTP 400. 2. Trusting the redirect `?status=` as the final decision - Cause: reading the `status` query param on `redirect_url` and treating it as authoritative. - Fix: treat it as a hint only; confirm via the webhook or the decision endpoint. 3. Exposing the API key to the browser - Cause: calling `/api/v2/session` (or any server-to-server endpoint) from frontend code. - Fix: keep `X-API-Key` server-side only. Only the hosted `url` and the `session_token` are sent to the browser. --- ## Webhooks On a terminal session, Valyd POSTs to your `callback` URL with these headers: - `X-Valyd-Timestamp` — unix seconds the request was signed. - `X-Valyd-Event-Id` — unique event id (use to dedupe). - `X-Valyd-Signature` — `hmac_sha256(`${timestamp}.${rawBody}`, webhook_signing_secret)`, lowercase hex. Verify against the RAW request body (no JSON re-serialisation), use a constant-time compare, and reject stale timestamps (e.g. > 5 minutes old). Webhook body: ```json { "event_id": "evt_…", "type": "verification.approved", // verification.declined | .in_review | .abandoned | .expired "session_id": "ses_…", "status": "APPROVED", "vendor_data": "user_123", "decision": { /* same shape as the decision API */ }, "occurred_at": "2026-06-11T12:05:00Z" } ``` Event `type` values: `verification.approved`, `verification.declined`, `verification.in_review`, `verification.abandoned`, `verification.expired`. SDK (Node) — verify and handle the webhook: ```javascript import express from "express"; import { VerifyClient, ValydVerifyError } from "valyd-verify-sdk"; const verify = new VerifyClient({ apiKey: process.env.VALYD_API_KEY!, webhookSecret: process.env.VALYD_WEBHOOK_SECRET!, }); // IMPORTANT: use raw body for signature verification app.post( "/webhooks/valyd", express.raw({ type: "application/json" }), async (req, res) => { try { const event = verify.webhooks.constructEvent(req.body, req.headers); // event.session_id, event.type, event.status, event.decision, event.vendor_data 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; } } ); ``` Manual verify (Node) — verify the signature without the SDK: ```javascript import crypto from "node:crypto"; function verifyValydSignature(rawBody, headers, secret) { const ts = headers["x-valyd-timestamp"]; const sig = headers["x-valyd-signature"]; if (!ts || !sig) throw new Error("missing signature headers"); // Reject stale (> 5 min) if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) { throw new Error("stale timestamp"); } const expected = crypto .createHmac("sha256", secret) .update(`${ts}.${rawBody}`) .digest("hex"); const a = Buffer.from(expected, "hex"); const b = Buffer.from(sig, "hex"); if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) { throw new Error("invalid signature"); } } ``` The webhook is a NOTIFICATION. For the full check breakdown, call the decision endpoint below. --- ## Reading the decision Decision tree for reading the verification decision: ```text IF you received a webhook: → it carries event.status and event.decision; still call the decision API for the full check breakdown. IF you want to pull the result yourself: → GET /api/v2/session//decision with X-API-Key. 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. ``` cURL: ```bash curl https://idp.valyd.work/api/v2/session/ses_…/decision \ -H "X-API-Key: $VALYD_API_KEY" ``` SDK (Node): ```javascript const d = await verify.sessions.decision(sessionId); // d.status → "APPROVED" | "DECLINED" | "IN_REVIEW" // d.decision → "APPROVED" | "DECLINED" (final business outcome) // 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" } ``` Expected output — HTTP 200, `data`: ```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. --- ## Other session & workflow helpers ```javascript // Sessions 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 override // Workflows (↔ /api/v2/workflows) const wf = await verify.workflows.create({ name: "KYC + License", services: ["id_verification", "liveness", "face_match", "credential"], }); await verify.workflows.list(); await verify.workflows.retrieve(wf.id); await verify.workflows.update(wf.id, { name: "KYC + License (v2)" }); await verify.workflows.remove(wf.id); ``` --- ## Statuses ### Session status ```text NOT_STARTED → IN_PROGRESS → (IN_REVIEW) → APPROVED | DECLINED ``` Plus terminal: `ABANDONED`, `EXPIRED`. Full set of session status values: `NOT_STARTED`, `IN_PROGRESS`, `IN_REVIEW`, `APPROVED`, `DECLINED`, `ABANDONED`, `EXPIRED`. ### Check status ```text pending → running → passed | failed | review ``` Full set of check status values: `pending`, `running`, `passed`, `failed`, `review`. --- ## API reference (endpoints used in this flow) | Method | Path | Auth | Purpose | | --- | --- | --- | --- | | POST | `https://idp.valyd.work/api/v2/session` | `X-API-Key` | Create a hosted verification session. | | GET | `https://idp.valyd.work/api/v2/session//decision` | `X-API-Key` | Read the authoritative decision and per-check breakdown. | | GET (SDK: `sessions.retrieve`) | `https://idp.valyd.work/api/v2/session/` | `X-API-Key` | Retrieve a session. | | GET (SDK: `sessions.list`) | `https://idp.valyd.work/api/v2/session` | `X-API-Key` | List sessions (filter by `status`, `vendorData`, `limit`). | | POST/PATCH (SDK: `sessions.updateStatus`) | `https://idp.valyd.work/api/v2/session/` | `X-API-Key` | Manually override session status to `APPROVED` or `DECLINED`. | | POST (SDK: `workflows.create`) | `https://idp.valyd.work/api/v2/workflows` | `X-API-Key` | Create a workflow. | | GET (SDK: `workflows.list`) | `https://idp.valyd.work/api/v2/workflows` | `X-API-Key` | List workflows. | | GET (SDK: `workflows.retrieve`) | `https://idp.valyd.work/api/v2/workflows/` | `X-API-Key` | Retrieve a workflow. | | PATCH (SDK: `workflows.update`) | `https://idp.valyd.work/api/v2/workflows/` | `X-API-Key` | Update a workflow. | | DELETE (SDK: `workflows.remove`) | `https://idp.valyd.work/api/v2/workflows/` | `X-API-Key` | Delete a workflow. | | POST (your endpoint) | `` | Signed via `X-Valyd-Signature` | Webhook Valyd POSTs to on a terminal session. | > The exact HTTP methods/paths for the SDK helpers `sessions.retrieve`, `sessions.list`, `sessions.updateStatus`, and the `workflows.*` helpers are inferred from the SDK method names and the noted `↔ /api/v2/workflows` mapping in the source; the component only spells out the literal paths `POST /api/v2/session` and `GET /api/v2/session//decision`. --- ## End-to-end Express example ```javascript import express from "express"; import { VerifyClient, ValydVerifyError } from "valyd-verify-sdk"; const app = express(); const verify = new VerifyClient({ apiKey: process.env.VALYD_API_KEY!, webhookSecret: process.env.VALYD_WEBHOOK_SECRET!, }); // 1) Start hosted 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.session_id }); }); // 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.session_id); await persist(event.vendor_data, 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); ``` ================================================================================ === FILE: https://docs.valyd.work/verify/standalone.md === ================================================================================ > Source: https://docs.valyd.work/verify#standalone > Part of: Valyd Verify API documentation — static copy generated for AI agents > Generated from repo component: StandaloneSection.tsx # Core APIs (server-to-server verification) ## Agent Quick-Start - Source URL: https://docs.valyd.work/verify#standalone - Credentials / env vars needed: VALYD_API_KEY (App API key — keep server-side, never ship to the browser) - Files an integrator edits: server route handler / backend service, .env (for VALYD_API_KEY) - Estimated steps: 3 (install SDK or use cURL, set VALYD_API_KEY, call the endpoint) - Can complete without human input: NO — you must obtain an App API key from the Valyd Developer Portal (https://dev.valyd.work) before any call will authenticate. - Prerequisites: - A Valyd App API key. Pass it as the HTTP header `X-API-Key: ` on every request. Get this from the Developer Portal → your project → Credentials: https://dev.valyd.work - A server/backend to make the call from (these are server-to-server APIs; never call them from the browser, because the API key would be exposed). - (SDK path only) Node.js with the `valyd-verify-sdk` npm package installed. ## Overview Direct, synchronous, server-to-server checks. You build your own UI and call these endpoints from your backend. Every request uses the header `X-API-Key: ` — keep this server-side, never ship it to the browser. Base URL for every endpoint below: `https://idp.valyd.work` Every response uses the standard envelope and includes a `check` object: ```json { "success": true, "data": { "session_id": "ses_…", "status": "passed", // passed | failed | review "check": { "type": "id_verification" | "liveness" | "face_match" | "age" | "credential", "status": "passed" | "failed" | "review", "score": 0.97, "data": { /* per-check details */ }, "error": null } }, "error": null } ``` The JSON blocks shown under each endpoint below are the contents of `check.data` (the per-check details), unless the block is labeled otherwise. ## SDK quick start The official Node SDK is published on npm as `valyd-verify-sdk` (https://www.npmjs.com/package/valyd-verify-sdk). Image fields accept a file path via `readImage("./x.jpg")`, a `Buffer`, or a base64 / data-URL string. Over plain HTTP, send images as a base64 string in the JSON field (or as a multipart file under the same field name). Install: ```bash npm i valyd-verify-sdk ``` Create a client (do this once and reuse it): ```javascript import { VerifyClient, readImage } from "valyd-verify-sdk"; const verify = new VerifyClient({ apiKey: process.env.VALYD_API_KEY! }); // keep VALYD_API_KEY on the server — never in browser code // get the API key from the Developer Portal → your project → Credentials: https://dev.valyd.work ``` Set the API key in your environment before running: ```bash export VALYD_API_KEY="" ``` --- ## POST /api/v2/id-verification — ID Verification OCR + authenticity from a government ID. **Method:** POST **Full URL:** `https://idp.valyd.work/api/v2/id-verification` **Auth header:** `X-API-Key: ` **Fields:** - `front_image` (image) **required** — Front of the ID. File, Buffer, or base64/data-URL. - `back_image` (image) — Back of the ID (when applicable). **Request (cURL):** ```bash curl -X POST https://idp.valyd.work/api/v2/id-verification \ -H "X-API-Key: $VALYD_API_KEY" \ -F "front_image=@./id_front.jpg" \ -F "back_image=@./id_back.jpg" ``` **Request (SDK, Node):** ```javascript import { VerifyClient, readImage } from "valyd-verify-sdk"; const verify = new VerifyClient({ apiKey: process.env.VALYD_API_KEY! }); const { check } = await verify.standalone.idVerification({ frontImage: readImage("./id_front.jpg"), backImage: readImage("./id_back.jpg"), // optional }); console.log(check.data.fields.full_name, check.data.fields.document_number); ``` **Expected output:** HTTP 200 with the standard envelope (`success: true`); `check.type` is `"id_verification"` and `check.data` is: ```json { "fields": { "full_name": "Jane Doe", "fathers_name": "John Doe", "document_number": "X1234567", "date_of_birth": "1990-01-15", "date_of_issue": "2020-03-10", "date_of_expiry": "2030-03-10", "sex": "F", "issuing_state": "CA", "country": "US", "document_type": "driver_license" }, "portrait": "", "dob": "1990-01-15", "authenticity": { "score": 0.96 } } ``` --- ## POST /api/v2/liveness — Liveness Passive liveness check. Passes when `live_score === 1`. **Method:** POST **Full URL:** `https://idp.valyd.work/api/v2/liveness` **Auth header:** `X-API-Key: ` **Fields:** - `image` (image) **required** — A selfie. File, Buffer, or base64/data-URL. **Request (cURL):** ```bash curl -X POST https://idp.valyd.work/api/v2/liveness \ -H "X-API-Key: $VALYD_API_KEY" \ -F "image=@./selfie.jpg" ``` **Request (SDK, Node):** ```javascript const { check } = await verify.standalone.liveness({ image: readImage("./selfie.jpg"), }); // check.status === "passed" when check.data.live_score === 1 ``` **Expected output:** HTTP 200 with the standard envelope (`success: true`); `check.type` is `"liveness"`. `check.status === "passed"` when `live_score === 1`. `check.data` is: ```json { "live_score": 1, "result": "live" } ``` `live_score` values: `1` = live, `0` = spoof, `< 0` = no face detected. --- ## POST /api/v2/face-match — Face Match Compare two images. Passes when similarity ≥ threshold (default ~0.95). **Method:** POST **Full URL:** `https://idp.valyd.work/api/v2/face-match` **Auth header:** `X-API-Key: ` **Fields:** - `image1` (image) **required** — Reference image (typically the ID portrait). - `image2` (image) **required** — Selfie to compare against the reference. **Request (cURL):** ```bash curl -X POST https://idp.valyd.work/api/v2/face-match \ -H "X-API-Key: $VALYD_API_KEY" \ -F "image1=@./id_portrait.jpg" \ -F "image2=@./selfie.jpg" ``` **Request (SDK, Node):** ```javascript const { check } = await verify.standalone.faceMatch({ idImage: readImage("./id_portrait.jpg"), selfie: readImage("./selfie.jpg"), }); // check.data.similarity, check.data.threshold ``` **Expected output:** HTTP 200 with the standard envelope (`success: true`); `check.type` is `"face_match"`. `check.data` is: ```json { "similarity": 0.973, "threshold": 0.95 } ``` --- ## POST /api/v2/age-verification — Age Verification JSON body. Computes age from DOB and verifies the requested age bands (no ZKP). **Method:** POST **Full URL:** `https://idp.valyd.work/api/v2/age-verification` **Auth header:** `X-API-Key: ` **Content-Type:** `application/json` **Fields:** - `dob` (string, `YYYY-MM-DD`) **required** — Date of birth. - `bands` (string[]) **required** — e.g. `["is_18_plus","is_21_plus"]`. **Request (cURL):** ```bash curl -X POST https://idp.valyd.work/api/v2/age-verification \ -H "X-API-Key: $VALYD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "dob": "1995-06-01", "bands": ["is_18_plus","is_21_plus"] }' ``` **Request (SDK, Node):** ```javascript const { check } = await verify.standalone.ageVerification({ dob: "1995-06-01", bands: ["is_18_plus", "is_21_plus"], }); ``` **Expected output:** HTTP 200 with the standard envelope (`success: true`); `check.type` is `"age"`. `check.data` is: ```json { "age": 30, "dob": "1995-06-01", "bands": { "is_18_plus": { "verified": true, "min_age": 18 }, "is_21_plus": { "verified": true, "min_age": 21 } } } ``` --- ## POST /api/v2/credential-verification — Credential Verification Look up a professional license in the provider registry. Registry lookups can take 10–60s — use a generous timeout. **Method:** POST **Full URL:** `https://idp.valyd.work/api/v2/credential-verification` **Auth header:** `X-API-Key: ` **Content-Type:** `application/json` **Fields:** - `first_name` (string) **required** — Required even when `required_fields` omits it — the registry always needs a name. - `last_name` (string) **required** — Or supply `full_name` instead of first/last. - `license_type` (string) **required** — Provider code, e.g. `'MD'`. Alias: `provider_code`. - `license_state` (string) **required** — 2-letter state code. Alias: `state`. - `license_number` (string) **required** — Alias: `license_no`. - `npi` (string) — Optional NPI when applicable. **Request (cURL):** ```bash curl -X POST https://idp.valyd.work/api/v2/credential-verification \ -H "X-API-Key: $VALYD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "first_name": "Jane", "last_name": "Doe", "license_type": "MD", "license_state": "CA", "license_number": "A12345", "npi": "1234567890" }' ``` **Request (SDK, Node):** ```javascript const { check } = await verify.standalone.credentialVerification({ firstName: "Jane", lastName: "Doe", providerCode: "MD", licenseState: "CA", licenseNumber: "A12345", npi: "1234567890", // optional }); // check.data.match, check.data.license ``` **Expected output:** HTTP 200 with the standard envelope (`success: true`); `check.type` is `"credential"`. `check.data` is: ```json { "match": true, "license": { "license_number": "A12345", "status": "active", "issued_at": "2015-01-01", "expires_at": "2027-01-01", "specialty": "Internal Medicine" } } ``` --- ## POST /api/v2/kyc-credential — KYC + Credential Combined ID verification + liveness + face match + license lookup, in one call. The license is matched against the name OCR'd from the ID — never a client-supplied name — so the holder cannot impersonate someone else's license. **Method:** POST **Full URL:** `https://idp.valyd.work/api/v2/kyc-credential` **Auth header:** `X-API-Key: ` **Fields:** - `front_image` (image) **required** — Front of the government ID. - `selfie` (image) **required** — Live selfie for liveness + face match. - `back_image` (image) — Back of the ID (when applicable). - `license_type` (string) **required** — Provider code. Alias: `provider_code`. - `license_state` (string) **required** — State code. Alias: `state`. - `license_number` (string) **required** — Alias: `license_no`. - `npi` (string) — Optional NPI. **Request (cURL):** ```bash curl -X POST https://idp.valyd.work/api/v2/kyc-credential \ -H "X-API-Key: $VALYD_API_KEY" \ -F "front_image=@./id_front.jpg" \ -F "selfie=@./selfie.jpg" \ -F "license_type=MD" \ -F "license_state=CA" \ -F "license_number=A12345" ``` **Request (SDK, Node):** ```javascript const result = await verify.standalone.kycCredential({ frontImage: readImage("./id_front.jpg"), selfie: readImage("./selfie.jpg"), providerCode: "MD", licenseState: "CA", licenseNumber: "A12345", }); // result.status === "passed" only when every check passes // result.checks: [id_verification, liveness, face_match, credential] // result.identity: { name, dob } ← name used for the license match ``` **Expected output:** HTTP 200 with the standard envelope (`success: true`). The `data` object is: ```json { "session_id": "ses_…", "status": "passed", "identity": { "name": "Jane Doe", "dob": "1990-01-15" }, "checks": [ { "type": "id_verification", "status": "passed", "data": { /* … */ } }, { "type": "liveness", "status": "passed", "data": { "live_score": 1 } }, { "type": "face_match", "status": "passed", "data": { "similarity": 0.97 } }, { "type": "credential", "status": "passed", "data": { "match": true, "license": { /* … */ } } } ] } ``` `status` is `"passed"` only when every check passes. --- ## Credential discovery Use these endpoints to build state and license-type pickers in your UI before calling `credential-verification` or `kyc-credential`. A provider's `required_fields` tells you which license inputs to collect — but always collect first / last name even when it isn't listed, because the registry lookup needs it. ### GET /api/v2/credential/states — list states **Method:** GET **Full URL:** `https://idp.valyd.work/api/v2/credential/states` **Auth header:** `X-API-Key: ` **Request (cURL):** ```bash curl https://idp.valyd.work/api/v2/credential/states \ -H "X-API-Key: $VALYD_API_KEY" ``` **Request (SDK, Node):** ```javascript const { states } = await verify.credentials.states(); // states: [{ state_name: "California", state_code: "CA" }, …] ``` **Expected output:** HTTP 200 with the standard envelope (`success: true`); `data` is: ```json { "states": [ { "state_name": "California", "state_code": "CA" } ] } ``` ### GET /api/v2/credential/states/{state}/providers — list providers for a state `{state}` is a 2-letter state code (e.g. `CA`). **Method:** GET **Full URL:** `https://idp.valyd.work/api/v2/credential/states/{state}/providers` **Auth header:** `X-API-Key: ` **Request (cURL):** ```bash curl https://idp.valyd.work/api/v2/credential/states/CA/providers \ -H "X-API-Key: $VALYD_API_KEY" ``` **Request (SDK, Node):** ```javascript const { providers } = await verify.credentials.providers("CA"); // providers: [{ provider_code, provider_display_name, credential_name, required_fields, … }] ``` **Expected output:** HTTP 200 with the standard envelope (`success: true`); `data` is: ```json { "providers": [ { "provider_code": "MD", "provider_display_name": "Medical Board of California", "credential_name": "Physician & Surgeon", "required_fields": ["license_number"] } ] } ``` --- ## Errors Over HTTP, failures return the envelope `{ success: false, error: { code, message } }` with the matching HTTP status: - `401` — invalid or missing API key. - `400` — validation error (missing field, bad image, unknown provider). - `404` — unknown state or provider. - `429` — rate limited. - `5xx` — upstream registry or internal error. In the SDK, the same failures throw `ValydVerifyError` with `{ code, status, message }`. Credential registry lookups can take **10–60 seconds** — configure a generous client timeout. ```javascript import { VerifyClient, ValydVerifyError, readImage } from "valyd-verify-sdk"; const verify = new VerifyClient({ apiKey: process.env.VALYD_API_KEY!, timeoutMs: 90_000, // registry lookups can be slow }); try { const { check } = await verify.standalone.credentialVerification({ firstName: "Jane", lastName: "Doe", providerCode: "MD", licenseState: "CA", licenseNumber: "A12345", }); } catch (err) { if (err instanceof ValydVerifyError) { console.error(err.status, err.code, err.message); } else { throw err; } } ``` ### Common errors 1. **HTTP 401 — invalid or missing API key.** - Cause: The `X-API-Key` header is absent or holds a wrong/revoked key. - Fix: Set `X-API-Key: ` on the request (or `apiKey` in the SDK client). Obtain a valid key from the Developer Portal → your project → Credentials: https://dev.valyd.work 2. **Client timeout on credential / kyc-credential calls.** - Cause: Registry lookups can take 10–60 seconds; the default client timeout aborts first. - Fix: Configure a generous timeout (e.g. `timeoutMs: 90_000` in the SDK client, or `--max-time 90` for cURL). 3. **HTTP 400 / 404 — validation error or unknown state/provider.** - Cause: A required field is missing, an image is unreadable, or the supplied `license_state` / `license_type` (`provider_code`) is not in the registry. - Fix: Call `GET /api/v2/credential/states` and `GET /api/v2/credential/states/{state}/providers` first to get valid codes and each provider's `required_fields`; always include first/last name even if `required_fields` omits it. ================================================================================ === FILE: https://docs.valyd.work/verify/sdk.md === ================================================================================ > Source: https://docs.valyd.work/verify#sdk > Part of: Valyd Verify API documentation — static copy generated for AI agents > Generated from repo component: SdkSection.tsx # Node SDK ## Agent Quick-Start - Source URL: https://docs.valyd.work/verify#sdk - Credentials / env vars needed: VALYD_API_KEY, VALYD_WEBHOOK_SECRET (for hosted/webhook flows), VALYD_WORKFLOW_ID (for hosted sessions) - Files an integrator edits: .env, your server bootstrap (VerifyClient init), and a webhook route handler (e.g. Express) - Estimated steps: 3 (install, initialise, call resources) - Can complete without human input: NO — you must first obtain an API key, a webhook secret, and a workflow ID from the Valyd Developer Portal (https://dev.valyd.work); these cannot be generated programmatically here. - Prerequisites: - Node 18+ (the SDK relies on the built-in `fetch` and `crypto`) - A Valyd Verify API key (X-API-Key) — get it from the dashboard: https://dev.valyd.work - For hosted/webhook flows: a webhook secret and a workflow ID from the dashboard - Server-side runtime only — the API key must never reach the browser The official Node SDK for Valyd Verify: `valyd-verify-sdk` on npm (https://www.npmjs.com/package/valyd-verify-sdk). Zero-dependency, dual ESM + CJS, fully typed TypeScript. Requires Node 18+ (built-in `fetch` / `crypto`). > Server-side only. Your API key must never reach the browser. The hosted flow is just a redirect to `session.url` — there is no browser 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.work): - `VALYD_API_KEY` — sent as the `X-API-Key` header on every request. - `VALYD_WEBHOOK_SECRET` — needed to verify webhook signatures (hosted flow). - `VALYD_WORKFLOW_ID` — needed when creating hosted sessions. ```text IF you are building a hosted flow (redirect the user to a Valyd-hosted page): → you need VALYD_API_KEY, VALYD_WEBHOOK_SECRET, and VALYD_WORKFLOW_ID IF you are building a Core APIs flow (call individual checks server-side): → you only need VALYD_API_KEY IF unsure which credentials you have: → log in to https://dev.valyd.work and check your project's API keys / webhooks / workflows ``` ### Steps 1. **Install the SDK.** ```bash npm i valyd-verify-sdk ``` **Expected output:** npm adds `valyd-verify-sdk` to `dependencies` in `package.json` and reports `added 1 package`. Versions follow semver and are pinned per release — lock to `^x.y.z` for backwards-compatible upgrades. 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.work. ```bash VALYD_API_KEY=your_api_key_here # X-API-Key for every request VALYD_WEBHOOK_SECRET=your_webhook_secret # required for hosted/webhook flows VALYD_WORKFLOW_ID=your_workflow_id # required to create hosted 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-verify-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. Sent as the `X-API-Key` header on every request. | | `baseUrl` | string | `https://idp.valyd.work` | 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. Increase for credential lookups (10–60s). | | `fetch` | typeof fetch | — | Custom fetch implementation (proxies, instrumentation, tests). | ### Resources After initialising `verify`, use these resource namespaces. #### `verify.sessions` - `create(params): Promise` — Create a hosted session. Returns `.url` and `.sessionId` — see the Hosted Verification guide. - `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). #### `verify.workflows` - `create({ name, features, settings? }): Promise` — e.g. `features: ["id_verification","liveness","face_match","credential"]`. - `list(): Promise` — List all workflows in the app. - `retrieve(id): Promise` — Fetch a workflow. - `update(id, patch): Promise` — Partial update. - `remove(id): Promise` — Delete a workflow. #### `verify.standalone` - `idVerification({ frontImage, backImage? }): Promise` — OCR + authenticity from a government ID. - `liveness({ image }): Promise` — Passive liveness on a selfie. - `faceMatch({ idImage, selfie }): Promise` — 1:1 face match. - `ageVerification({ dob, bands? }): Promise` — Age + bands (e.g. `["is_18_plus"]`). - `credentialVerification({ firstName, lastName, providerCode, licenseState, licenseNumber, npi? }): Promise` — Professional license lookup. - `kycCredential({ frontImage, selfie, backImage?, providerCode, licenseState, licenseNumber, npi? }): Promise` — ID + liveness + face match + license, matched against the OCR'd name. See the Core APIs guide for full field details. #### `verify.credentials` - `states(): Promise<{ states: CredentialState[] }>` — List supported states. - `providers(state): Promise<{ providers: CredentialProvider[] }>` — 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-verify-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-verify-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-verify-sdk"; const verify = new VerifyClient({ apiKey: process.env.VALYD_API_KEY!, timeoutMs: 90_000 }); try { const { check } = await verify.standalone.credentialVerification({ firstName: "Jane", lastName: "Doe", providerCode: "MD", licenseState: "CA", licenseNumber: "A12345", }); } 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 #### Hosted quickstart ```javascript import { VerifyClient } from "valyd-verify-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.session_id); // 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[]`. #### Core APIs quickstart ```javascript import { VerifyClient, readImage } from "valyd-verify-sdk"; const verify = new VerifyClient({ apiKey: process.env.VALYD_API_KEY! }); // 1) Build a state/license picker const { states } = await verify.credentials.states(); const { providers } = await verify.credentials.providers("CA"); // 2) Run KYC + License in one call const result = await verify.standalone.kycCredential({ frontImage: readImage("./id_front.jpg"), selfie: readImage("./selfie.jpg"), providerCode: "MD", licenseState: "CA", licenseNumber: "A12345", }); // result.status === "passed" only when ALL checks pass ``` **Expected output:** `verify.credentials.states()` resolves to `{ states }`, `verify.credentials.providers("CA")` resolves to `{ providers }`, and `verify.standalone.kycCredential(...)` resolves to a `KycCredentialResult` whose `.status` is `"passed"` only when ALL checks pass. ### Express webhook Use `express.raw()` so the body bytes match what Valyd signed. ```javascript import express from "express"; import { VerifyClient, ValydVerifyError } from "valyd-verify-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.session_id); await persist(event.vendor_data, 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-verify-sdk ``` **Expected output:** a line like `valyd-verify-sdk@x.y.z`. - Confirm credentials are wired (Core APIs path, only needs `VALYD_API_KEY`): ```javascript import { VerifyClient } from "valyd-verify-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.work/verify/webhooks.md === ================================================================================ > Source: https://docs.valyd.work/verify#webhooks > Part of: Valyd Verify API documentation — static copy generated for AI agents > Generated from repo component: WebhooksSection.tsx # Webhooks ## Agent Quick-Start - Source URL: https://docs.valyd.work/verify#webhooks - Credentials / env vars needed: VALYD_WEBHOOK_SECRET (the webhook signing secret), VALYD_API_KEY (App API key, to fetch the full decision) - Files an integrator edits: server route handler (e.g. an Express handler), .env (to store VALYD_WEBHOOK_SECRET and VALYD_API_KEY) - Estimated steps: 4 - Can complete without human input: NO — you must obtain the webhook signing secret and register a callback URL in the Valyd Developer Portal (https://dev.valyd.work), which is a human/portal action. - Prerequisites: - A publicly reachable HTTPS endpoint to receive POST callbacks - A webhook signing secret configured on your App or session (from the Developer Portal: https://dev.valyd.work) - An App API key to call the decision endpoint - Ability to read the raw (unparsed) request body in your web framework Valyd POSTs to your app or session callback URL when a verification session reaches a terminal state. The webhook is a **notification only** — always call `GET /api/v2/session/{id}/decision` for the full extracted data. > IMPORTANT: Read the **raw** request body before parsing it as JSON. You need the exact bytes that were sent in order to verify the HMAC signature. If your framework auto-parses JSON and re-serializes it, the bytes will differ and verification will fail. ## 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.work). 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.work) 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.work) VALYD_API_KEY=... # your App API key from the Developer Portal (https://dev.valyd.work) ``` **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"); 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`. 4. **Fetch the full decision after acknowledging the webhook.** The webhook body is a notification; retrieve the complete extracted data with the decision endpoint using the session id from the event. ```bash curl https://idp.valyd.work/api/v2/session/SES_ID/decision \ -H "X-API-Key: $VALYD_API_KEY" ``` **Expected output:** HTTP 200 with the full decision and per-check data for that session. (See api-reference.md → Decision for the endpoint details.) ### 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. - `type` — the event type, e.g. `verification.approved`. - `session_id` — the session that triggered the event; pass it to `GET /api/v2/session/{id}/decision`. - `status` — the terminal session status (e.g. `APPROVED`). See statuses.md for every possible value. - `decision` — short decision string (e.g. `approved`). ### Delivery and retries - Your endpoint must return a 2xx status and respond fast — defer heavy work to a background queue. - Non-2xx responses are retried with exponential backoff. - Use `X-Valyd-Event-Id` for idempotency, since the same event may be delivered more than once. ### 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.work/verify/statuses.md === ================================================================================ > Source: https://docs.valyd.work/verify#statuses > Part of: Valyd Verify API documentation — static copy generated for AI agents > Generated from repo component: StatusesSection.tsx # Statuses & decisioning ## Agent Quick-Start - Source URL: https://docs.valyd.work/verify#statuses - Credentials / env vars needed: none (reference only) - Files an integrator edits: none — reference only - Estimated steps: 0 - Can complete without human input: YES — this is a reference page; no actions required. - Prerequisites: none This page lists every session status and every per-check status, what each means, and a decision tree for how to act on each. There are two independent status dimensions: - **Session status** — the lifecycle state of an entire verification session. - **Check status** — the result of an individual check within a session. ## Session status Lifecycle transitions: ```text NOT_STARTED ──► IN_PROGRESS ──► (IN_REVIEW) ──► APPROVED | DECLINED └─► ABANDONED | EXPIRED ``` `IN_REVIEW` is optional (parenthesized): 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 hosted 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`. ### Decision tree — how to act on a session status ```text IF status == NOT_STARTED: → do nothing yet; wait for the user to open the hosted 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: → fetch GET /api/v2/session/{id}/decision for the full extracted data, then grant access / complete onboarding. IF status == DECLINED: → fetch GET /api/v2/session/{id}/decision 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: → run `curl https://idp.valyd.work/api/v2/session/{id} -H "X-API-Key: $VALYD_API_KEY"` 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: → run `curl https://idp.valyd.work/api/v2/session/{id}/decision -H "X-API-Key: $VALYD_API_KEY"` 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. ================================================================================ === FILE: https://docs.valyd.work/verify/api-reference.md === ================================================================================ > Source: https://docs.valyd.work/verify#api-sessions > Part of: Valyd Verify API documentation — static copy generated for AI agents > Generated from repo component: ApiReferenceSection.tsx # API Reference ## Agent Quick-Start - Source URL: https://docs.valyd.work/verify#api-sessions - Credentials / env vars needed: VALYD_API_KEY (your App API key, sent as the `X-API-Key` header) - Files an integrator edits: none — reference only (server code uses these endpoints) - Estimated steps: 0 - Can complete without human input: YES — this is a reference page; no actions required. - Prerequisites: - An App API key from the Valyd Developer Portal (https://dev.valyd.work) - For workflow-based sessions, a `workflow_id` configured in the Developer Portal Base URL for all endpoints: `https://idp.valyd.work` ### Authentication (applies to every call) Every call uses the header: ```http X-API-Key: ``` A Bearer token is also accepted: ```http Authorization: Bearer ``` Get your App API key from the Valyd Developer Portal (https://dev.valyd.work). Authenticated endpoints are billed per call against your App. ## Sessions | Method | Path | Description | |--------|-----------------------------------|------------------------------------------------------| | POST | `/api/v2/session` | Create a hosted verification session | | GET | `/api/v2/session` | List sessions | | GET | `/api/v2/session/{id}` | Retrieve a session | | PATCH | `/api/v2/session/{id}/status` | Manual override: `{ status: APPROVED | DECLINED }` | ### POST /api/v2/session — create a hosted verification session Full URL: `https://idp.valyd.work/api/v2/session` ```bash curl -X POST https://idp.valyd.work/api/v2/session \ -H "X-API-Key: $VALYD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "workflow_id": "WORKFLOW_ID", "vendor_data": "user-123" }' ``` `workflow_id` (get it from the Developer Portal: https://dev.valyd.work) selects the bundle of services to run. `vendor_data` is your own correlation string, echoed back on webhooks and the decision. **Expected output:** HTTP 200/201 with a created session object including its session id and a hosted verification URL to send the user to. ### GET /api/v2/session — list sessions Full URL: `https://idp.valyd.work/api/v2/session` ```bash curl https://idp.valyd.work/api/v2/session \ -H "X-API-Key: $VALYD_API_KEY" ``` **Expected output:** HTTP 200 with a list of session objects. ### GET /api/v2/session/{id} — retrieve a session Full URL: `https://idp.valyd.work/api/v2/session/{id}` ```bash curl https://idp.valyd.work/api/v2/session/SES_ID \ -H "X-API-Key: $VALYD_API_KEY" ``` **Expected output:** HTTP 200 with the session object, including its current `status` (see statuses.md for all values). ### PATCH /api/v2/session/{id}/status — manual override Full URL: `https://idp.valyd.work/api/v2/session/{id}/status` Manually force a terminal decision. Body must set `status` to `APPROVED` or `DECLINED`. ```bash curl -X PATCH https://idp.valyd.work/api/v2/session/SES_ID/status \ -H "X-API-Key: $VALYD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "status": "APPROVED" }' ``` **Expected output:** HTTP 200 with the updated session reflecting the overridden status. ## Workflows Workflows are configured in the Developer Portal (https://dev.valyd.work). A workflow bundles services and exposes a stable `workflow_id` that you pass when creating a session. Available services that a workflow can bundle: - `id_verification` - `liveness` - `face_match` - `age` - `credential` There is no REST endpoint to create a workflow from this reference — workflows are defined in the Developer Portal, and you reference the resulting `workflow_id` in `POST /api/v2/session`. ## Core checks Run a single check directly without a hosted session. | Method | Path | Description | |--------|-------------------------------------------|------------------------------------------| | POST | `/api/v2/id-verification` | OCR + authenticity from a government ID | | POST | `/api/v2/liveness` | Passive liveness from a selfie | | POST | `/api/v2/face-match` | Selfie vs reference portrait | | POST | `/api/v2/age-verification` | Age bands from a DOB | | POST | `/api/v2/credential-verification` | Professional license lookup | Each is called with the `X-API-Key` header. Example: ```bash curl -X POST https://idp.valyd.work/api/v2/id-verification \ -H "X-API-Key: $VALYD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "...": "service-specific input" }' ``` **Expected output:** HTTP 200 with the check result, including a per-check status of `passed`, `failed`, or `review` (see statuses.md). Full URLs: - `https://idp.valyd.work/api/v2/id-verification` - `https://idp.valyd.work/api/v2/liveness` - `https://idp.valyd.work/api/v2/face-match` - `https://idp.valyd.work/api/v2/age-verification` - `https://idp.valyd.work/api/v2/credential-verification` ## Decision | Method | Path | Description | |--------|-------------------------------------|-----------------------------------| | GET | `/api/v2/session/{id}/decision` | Full decision and per-check data | Full URL: `https://idp.valyd.work/api/v2/session/{id}/decision` ```bash curl https://idp.valyd.work/api/v2/session/SES_ID/decision -H "X-API-Key: $VALYD_API_KEY" ``` **Expected output:** HTTP 200 with the full decision and per-check data for the session. Call this after a terminal webhook to retrieve all extracted data (the webhook is only a notification). ## POST /api/v2/location — the location check A **real GPS fix is always mandatory**. This step can never be skipped: a blocked permission or missing coordinates is a hard `failed` (never a pass, never a review). What the `status` means depends on what you asked for: | You send | `status` | `data.match` | Notes | | --- | --- | --- | --- | | `expected_latitude` + `expected_longitude` + `radius_m` | **`passed` inside the radius, `failed` outside it** | `true` / `false` | The status **is** the verdict. `error` reads e.g. `"You are 119.3 km from the required location (must be within 200 m)."` | | `expected_latitude` + `expected_longitude`, **no** `radius_m` | `passed` | `null` | We cannot judge without a threshold — we just report `data.distance_m`; you decide. | | No expected point | `passed` | absent | Capture-only: returns the captured coordinates + accuracy. | Request: ```bash curl -X POST https://idp.valyd.work/api/v2/location \ -H "X-API-Key: $VALYD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "latitude": 37.3382, "longitude": -121.8863, "accuracy": 12, "expected_latitude": 37.3390, "expected_longitude": -121.8850, "radius_m": 200 }' ``` Response (`data.check`): ```json { "type": "location", "status": "passed", "score": 137.4, "data": { "latitude": 37.3382, "longitude": -121.8863, "accuracy": 12, "source": "gps", "captured_at": "2026-07-13T18:04:10+00:00", "expected_latitude": 37.339, "expected_longitude": -121.885, "distance_m": 137.4, "radius_m": 200, "match": true } } ``` Outside the radius the same call returns `"status": "failed"`, `"match": false` and the human-readable `error`. There is no separate `location-match` endpoint and no `evv_presence` bundle — `location` is the single location check (feature key `location`), and it does the geofence verdict itself. ```text IF you need a yes/no "is the user at the place?": → send expected_latitude + expected_longitude + radius_m and read `status` IF you only want the distance: → send the expected point WITHOUT radius_m and read `data.distance_m` IF you only want the coordinates: → send just latitude/longitude/accuracy IF status is failed with reason permission_denied: → the user blocked location; ask them to allow it and retry (it cannot be skipped) ``` ## Errors & rate limits Errors use the same response envelope with `success: false` and an HTTP status that reflects the error class. ```json { "success": false, "error": { "code": "invalid_api_key", "message": "API key is missing or invalid" } } ``` | HTTP | Code | When | |------|--------------------|------------------------------------------------------| | 400 | `validation_error` | Malformed body or missing required field | | 401 | `invalid_api_key` | Missing/invalid `X-API-Key` header | | 404 | `not_found` | Session or resource does not exist | | 422 | `unprocessable` | Could not process the input (e.g. unreadable image) | | 429 | `rate_limited` | Public/demo endpoints rate limit exceeded | | 500 | `internal_error` | Unexpected server error — safe to retry | Rate limits and billing: - Authenticated endpoints are billed per call against your App. - Public/demo endpoints return HTTP 429 with `code: "rate_limited"` when limits are exceeded. ### Decision tree — handling an error response ```text IF HTTP 400 (validation_error): → fix the request body / add the missing required field, then retry. IF HTTP 401 (invalid_api_key): → set the X-API-Key header to a valid App API key (from https://dev.valyd.work); do not retry until fixed. IF HTTP 404 (not_found): → verify the session/resource id; do not retry the same id. IF HTTP 422 (unprocessable): → the input could not be processed (e.g. unreadable image); collect new/clearer input and retry. IF HTTP 429 (rate_limited): → back off and retry later; you have exceeded public/demo limits. IF HTTP 500 (internal_error): → safe to retry (with backoff). ```