Skip to Content
Valyd IDAPI reference

API Reference

Agent Quick-Start

  • Source URL: https://docs.valyd.id/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.id )
  • Prerequisites:
    • A registered application with a Client ID and Client Secret (get these from the Developer Portal: https://dev.valyd.id )
    • 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.id/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.id/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.id/api/auth/tpsso/token
  • Base URL: https://idp.valyd.id/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.

Authorization codes are **bound to the client they were issued to**, single-use, and expire **2 minutes** after they are issued. Exchange the code as soon as your callback receives it — a code issued for one application can never be redeemed by another.

Parameters

NameTypeRequiredDescription
grant_typestringYesMust be “authorization_code”
client_idstringYesYour assigned Client ID (get this from the Developer Portal → your project → Credentials: https://dev.valyd.id )
client_secretstringYesYour Client Secret (server-side only!) (get this from the Developer Portal → your project → Credentials: https://dev.valyd.id )
codestringYesThe authorization code from callback
redirect_uristringRecommendedThe exact redirect_uri you used at /authorize. Validated when supplied; will become required in a future release

Request body (application/json)

{ "grant_type": "authorization_code", "client_id": "YOUR_CLIENT_ID", "client_secret": "YOUR_CLIENT_SECRET", "code": "AUTH_CODE_FROM_CALLBACK", "redirect_uri": "https://yourapp.com/auth/valyd/callback" }

YOUR_CLIENT_ID / YOUR_CLIENT_SECRET: get these from the Developer Portal → your project → Credentials: https://dev.valyd.id  AUTH_CODE_FROM_CALLBACK: the one-time authorization code delivered to your registered callback URL after the user authenticates.

Code examples

curl -X POST "https://idp.valyd.id/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" }'
const response = await fetch("https://idp.valyd.id/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;
import requests response = requests.post( "https://idp.valyd.id/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 $ch = curl_init(); curl_setopt_array($ch, [ CURLOPT_URL => "https://idp.valyd.id/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"]; ?>
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.id/api/auth/tpsso/token")) .header("Content-Type", "application/json") .header("Accept", "application/json") .POST(HttpRequest.BodyPublishers.ofString(body)) .build(); HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); // Parse response with your preferred JSON library

Expected output

Response — 200 OK (Success Response):

{ "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):

{ "success": false, "error": { "code": "invalid_client", "message": "client_id/client_secret invalid" } }

GET /userinfo — Get User Profile

  • Method: GET
  • Full URL: https://idp.valyd.id/api/auth/tpsso/userinfo
  • Base URL: https://idp.valyd.id/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

curl -X GET "https://idp.valyd.id/api/auth/tpsso/userinfo" \ -H "Accept: application/json" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
const response = await fetch("https://idp.valyd.id/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);
import requests response = requests.get( "https://idp.valyd.id/api/auth/tpsso/userinfo", headers={ "Accept": "application/json", "Authorization": f"Bearer {access_token}" } ) user = response.json()["data"] print(user["full_name"], user["email"])
<?php $ch = curl_init(); curl_setopt_array($ch, [ CURLOPT_URL => "https://idp.valyd.id/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"]; ?>
HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://idp.valyd.id/api/auth/tpsso/userinfo")) .header("Accept", "application/json") .header("Authorization", "Bearer " + accessToken) .GET() .build(); HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());

Expected output

Response — 200 OK (Success Response):

{ "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):

{ "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.id/api/auth/tpsso/licenses
  • Base URL: https://idp.valyd.id/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

curl -X GET "https://idp.valyd.id/api/auth/tpsso/licenses" \ -H "Accept: application/json" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
const response = await fetch("https://idp.valyd.id/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}`); });
import requests response = requests.get( "https://idp.valyd.id/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 $ch = curl_init(); curl_setopt_array($ch, [ CURLOPT_URL => "https://idp.valyd.id/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"; } ?>
HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://idp.valyd.id/api/auth/tpsso/licenses")) .header("Accept", "application/json") .header("Authorization", "Bearer " + accessToken) .GET() .build(); HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); // Parse licenses from response

Expected output

Response — 200 OK (Success Response):

{ "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):

{ "success": false, "error": { "code": "invalid_token", "message": "token invalid/expired" } }

GET /verifications — Get Identity Verifications

  • Method: GET
  • Full URL: https://idp.valyd.id/api/auth/tpsso/verifications
  • Base URL: https://idp.valyd.id/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 the user’s verification status: whether they passed a human (liveness) check, whether they completed identity (KYC) verification, and any professional licenses linked to their Valyd identity. Use alongside /userinfo for a complete user picture.

YOUR_ACCESS_TOKEN: the access_token returned by POST /token (or POST /refresh).

Response fields (data.verifications):

FieldTypeDescription
human_verifiedbooleanThe user passed a liveness / anti-spoof human check. Falls back to id_verified when no explicit human check is on file.
id_verifiedbooleanThe user completed identity (KYC) document verification.
licensesarrayProfessional / credential licenses linked to the user. Empty array if none.
licenses[].license_typestringThe kind of license (e.g. drivers_license, medical).
licenses[].verifiedbooleanWhether that license is currently verified.
licenses[].verified_fromstring | nullSource the license was verified against.
licenses[].expire_atstring | nullISO-8601 expiry timestamp, or null if it does not expire.

Code examples

curl -X GET "https://idp.valyd.id/api/auth/tpsso/verifications" \ -H "Accept: application/json" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
const response = await fetch("https://idp.valyd.id/api/auth/tpsso/verifications", { method: "GET", headers: { "Accept": "application/json", "Authorization": `Bearer ${accessToken}` } }); const data = await response.json(); const { human_verified, id_verified, licenses } = data.data.verifications; if (human_verified && id_verified) { console.log("User is a verified human with completed KYC!"); }
import requests response = requests.get( "https://idp.valyd.id/api/auth/tpsso/verifications", headers={ "Accept": "application/json", "Authorization": f"Bearer {access_token}" } ) verifications = response.json()["data"]["verifications"] if verifications["human_verified"] and verifications["id_verified"]: print("User is a verified human with completed KYC!")
<?php $ch = curl_init(); curl_setopt_array($ch, [ CURLOPT_URL => "https://idp.valyd.id/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["human_verified"] && $verifications["id_verified"]) { echo "User is a verified human with completed KYC!"; } ?>
HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://idp.valyd.id/api/auth/tpsso/verifications")) .header("Accept", "application/json") .header("Authorization", "Bearer " + accessToken) .GET() .build(); HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); // Parse verifications from response

Expected output

Response — 200 OK (Success Response):

{ "success": true, "data": { "verifications": { "human_verified": true, "id_verified": true, "licenses": [ { "license_type": "drivers_license", "verified": true, "verified_from": "kyc", "expire_at": "2027-03-01T00:00:00+00:00" } ] } } }

Response — Error (Error Response):

{ "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.id/api/auth/tpsso/refresh
  • Base URL: https://idp.valyd.id/api/auth/tpsso
  • Path: /refresh
  • Auth / required scope: Client authentication required — client_id + client_secret (request body, or HTTP Basic)
  • Required headers:
    • Content-Type: application/json
    • Accept: application/json

Use your refresh_token to obtain a new access_token when it expires.

**Breaking change.** This endpoint now requires your `client_id` and `client_secret`, and the refresh token is validated against the client it was issued to. Calls that send only a `refresh_token` are rejected with `401 invalid_client`. Refresh tokens are **server-side credentials** — perform this exchange from your backend, never from a browser or mobile app.

Parameters

NameTypeRequiredDescription
refresh_tokenstringYesYour current refresh token
client_idstringYesYour application’s client ID
client_secretstringYesYour application’s client secret (server-side only)
rotate_refreshbooleanNoDefaults to true. Set to false only if you cannot store the replacement token

Request body (application/json)

{ "refresh_token": "rfrsh_abc123...", "client_id": "your_client_id", "client_secret": "your_client_secret" }

refresh_token: the refresh_token previously returned by POST /token or by a prior POST /refresh.

Rotation and reuse detection

Rotation is on by default. Each successful refresh returns a new refresh_token and immediately revokes the one you presented — always persist the new token and discard the old.

If a token that has already been rotated away is presented again, Valyd treats it as a stolen credential and revokes every refresh token for that user and client. Both the attacker’s copy and the legitimate session stop working, and the user simply signs in again. In practice this only fires if you keep using a stale token, so store the replacement atomically.

Code examples

curl -X POST "https://idp.valyd.id/api/auth/tpsso/refresh" \ -H "Content-Type: application/json" \ -H "Accept: application/json" \ -d '{ "refresh_token": "rfrsh_abc123...", "client_id": "your_client_id", "client_secret": "your_client_secret" }'
const response = await fetch("https://idp.valyd.id/api/auth/tpsso/refresh", { method: "POST", headers: { "Content-Type": "application/json", "Accept": "application/json" }, body: JSON.stringify({ refresh_token: refreshToken, client_id: process.env.VALYD_CLIENT_ID, client_secret: process.env.VALYD_CLIENT_SECRET // server-side only }) }); const data = await response.json(); const { access_token, refresh_token } = data.data; // Rotation is on by default: the token you just sent is now revoked. // Persist the replacement, or the next refresh will trip reuse detection. await saveTokens({ access_token, refresh_token });
import requests response = requests.post( "https://idp.valyd.id/api/auth/tpsso/refresh", json={ "refresh_token": refresh_token, "client_id": os.environ["VALYD_CLIENT_ID"], "client_secret": os.environ["VALYD_CLIENT_SECRET"], # server-side only }, headers={ "Content-Type": "application/json", "Accept": "application/json" } ) tokens = response.json()["data"] access_token = tokens["access_token"] # Rotation is on by default — the token you just sent is revoked. Store this one. refresh_token = tokens["refresh_token"]
<?php $ch = curl_init(); curl_setopt_array($ch, [ CURLOPT_URL => "https://idp.valyd.id/api/auth/tpsso/refresh", CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode([ "refresh_token" => $refreshToken, "client_id" => getenv("VALYD_CLIENT_ID"), "client_secret" => getenv("VALYD_CLIENT_SECRET"), // server-side only ]), CURLOPT_HTTPHEADER => [ "Content-Type: application/json", "Accept: application/json" ], CURLOPT_RETURNTRANSFER => true ]); $response = curl_exec($ch); $tokens = json_decode($response, true)["data"]; $accessToken = $tokens["access_token"]; // Rotation is on by default — the token you just sent is revoked. Store this one. $refreshToken = $tokens["refresh_token"]; ?>
HttpClient client = HttpClient.newHttpClient(); String body = """ { "refresh_token": "%s", "client_id": "%s", "client_secret": "%s" } """.formatted(refreshToken, clientId, clientSecret); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://idp.valyd.id/api/auth/tpsso/refresh")) .header("Content-Type", "application/json") .header("Accept", "application/json") .POST(HttpRequest.BodyPublishers.ofString(body)) .build(); HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());

Expected output

Response — 200 OK (Success Response):

{ "success": true, "data": { "tokens": { "access_token": "eyJhbGciOi...", "token_type": "Bearer", "expires_in": 900, "refresh_token": "rfrsh_new456..." } } }

Response — Error (Error Response):

{ "success": false, "error": { "code": "invalid_grant", "message": "refresh_token is invalid or expired" } }
Last updated on