Badges
A badge is an org-scoped label you define and attach to a person’s Valyd identity — “Background Check”, “Licensed Contractor”, “KYC Tier 2”. You use badges two ways:
- Mark your workforce — assign badges to your own members in the Developer Portal (Organization → Members).
- Verification reuse — after someone passes a check on your site (e.g. a background check), you grant them a badge on their Valyd identity. Other apps can then trust that proof instead of re-running the check. This is the “Connect with Valyd → claim your badge” flow.
Every badge belongs to one organization and is managed server-to-server with your
client_id + client_secret, under the base path /api/sdk — the same credentials and envelope
as the Organization API.
Private vs public
Each badge has a visibility that controls who can see and trust it:
visibility | Who can see it | Where it appears |
|---|---|---|
private (default) | Only your org. | Your own apps and API calls. Not shared with anyone else. |
public | Every relying app. | Carried in the portable OIDC badges claim, so any app a user signs into can trust it (cross-org). |
New badges are private by default — a badge only becomes shareable across organizations when you
explicitly make it public. An expired badge is never deleted: it stays, marked expired: true,
and is excluded from cross-org sharing.
The badge object
Wherever a badge appears in a response it has the same shape:
{
"id": 12, // numeric — the badge_id you pass to grant/revoke
"name": "Background Check",
"visibility": "private", // "private" | "public"
"expiry_date": "2027-01-01", // date, or null = never expires
"expired": false, // true once expiry_date has passed
"created_at": "2026-09-30T10:00:00+00:00"
}id— the numeric badge id. This is thebadge_idyou pass to grant, revoke, update and delete. Get it from List badges (or the portal).visibility—privateorpublic(see above).expiry_date— an ISO date ornull. Past-dated badges come back withexpired: true.created_at— when the badge was created.
List a user’s badges adds one more field, organization (the issuing
org), so a cross-org public badge names its trust anchor.
Authentication
Every badge endpoint uses your organization app’s client_id + client_secret, sent as request
headers and scoped to the org that owns the app. Server-side only — the secret must never reach a
browser. The @valyd/sdk client sends them for you.
X-Client-Id: <your client_id>
X-Client-Secret: <your client_secret>
Content-Type: application/jsonResponses use the standard envelope — { "success": true, "data": { … } } on success,
{ "success": false, "error": { "code", "message" } } on error.
import { ValydClient } from "@valyd/sdk";
// Server-side only — the client secret never touches the browser.
const client = new ValydClient({
clientId: process.env.VALYD_CLIENT_ID,
clientSecret: process.env.VALYD_CLIENT_SECRET,
});List badges
Returns your organization’s badge catalog — every badge you’ve created, with its id, visibility,
expiry and creation time. This is where you find the id to grant.
const { badges, organization } = await client.listBadges();
const backgroundCheck = badges.find((b) => b.name === "Background Check");
// backgroundCheck.id → pass as badge_id to grantBadge()GET /api/sdk/badges HTTP/1.1
Host: dev.valyd.id
X-Client-Id: <your client_id>
X-Client-Secret: <your client_secret>{
"success": true,
"data": {
"badges": [
{
"id": 12,
"name": "Background Check",
"visibility": "private",
"expiry_date": "2027-01-01",
"expired": false,
"created_at": "2026-09-30T10:00:00+00:00"
},
{
"id": 13,
"name": "Licensed Contractor",
"visibility": "public",
"expiry_date": null,
"expired": false,
"created_at": "2026-09-30T10:05:00+00:00"
}
],
"organization": { "id": 42, "name": "Acme, Inc." }
}
}Create a badge
Create a badge in your org’s catalog. name is required; expiry_date and visibility are
optional (visibility defaults to private).
const { badge } = await client.createBadge({
name: "Background Check",
expiryDate: "2027-01-01", // optional; omit for no expiry
visibility: "private", // optional; "private" (default) | "public"
});POST /api/sdk/badges HTTP/1.1
Host: dev.valyd.id
X-Client-Id: <your client_id>
X-Client-Secret: <your client_secret>
Content-Type: application/json
{ "name": "Background Check", "expiry_date": "2027-01-01", "visibility": "private" }{
"success": true,
"data": {
"badge": {
"id": 12,
"name": "Background Check",
"visibility": "private",
"expiry_date": "2027-01-01",
"expired": false,
"created_at": "2026-09-30T10:00:00+00:00"
}
}
}Errors — 409 duplicate_badge if a badge with that name (case-insensitive) already exists in
your org; 400 invalid_request on a bad field.
Update a badge
Change a badge’s name, expiry_date and/or visibility. Send only the fields you want to change.
const { badge } = await client.updateBadge(12, { visibility: "public" });PATCH /api/sdk/badges/12 HTTP/1.1
Host: dev.valyd.id
X-Client-Id: <your client_id>
X-Client-Secret: <your client_secret>
Content-Type: application/json
{ "visibility": "public" }Returns the updated badge object. Errors — 404 not_found,
409 duplicate_badge, 400 invalid_request.
Delete a badge
Delete a badge from your catalog. This cascades: the badge is removed from every member and every user who held it.
await client.deleteBadge(12);DELETE /api/sdk/badges/12 HTTP/1.1
Host: dev.valyd.id
X-Client-Id: <your client_id>
X-Client-Secret: <your client_secret>{ "success": true, "data": { "deleted": true } }Errors — 404 not_found.
Grant a badge
Offer one of your org’s badges to a Valyd user by their valyd_id. This is the verification-reuse
step: a user passed a check on your site and connected their Valyd ID.
A grant is an offer, not an immediate assignment. The badge is not added right away. The user gets a notification in their Valyd app and must approve it — and if their identity isn’t KYC-verified yet, they complete a quick ID check first. Only then does the badge land on their account. So this call returns
status: "pending"; the outcome happens on the user’s device and there is no callback to your server.
first_name / last_name are optional and display-only — shown in the user’s approval
notification.
const res = await client.grantBadge({
valydId: "valyd_9f8e7d6c5b4a3210fedcba98",
badgeId: 12,
firstName: "Ada", // optional, display-only
lastName: "Lovelace", // optional, display-only
});
// res.status === "pending" → offered, awaiting the user's approval
// res.status === "granted" → the user already had it (nothing to do)POST /api/sdk/badges/grant HTTP/1.1
Host: dev.valyd.id
X-Client-Id: <your client_id>
X-Client-Secret: <your client_secret>
Content-Type: application/json
{ "valyd_id": "valyd_9f8e7d6c5b4a3210fedcba98", "badge_id": 12, "first_name": "Ada", "last_name": "Lovelace" }A fresh offer — the user must approve it:
{
"success": true,
"data": {
"status": "pending",
"granted": false,
"request_id": "7b3f…",
"valyd_id": "valyd_9f8e7d6c5b4a3210fedcba98",
"badge": { "id": 12, "name": "Background Check", "visibility": "private", "expiry_date": "2027-01-01", "expired": false, "created_at": "2026-09-30T10:00:00+00:00" },
"display": "Acme, Inc. Background Check"
}
}Already held — a no-op:
{ "success": true, "data": { "status": "granted", "granted": true, "already_held": true, "valyd_id": "valyd_9f8e7d6c5b4a3210fedcba98", "badge": { "id": 12, "name": "Background Check", "visibility": "private", "expiry_date": "2027-01-01", "expired": false, "created_at": "2026-09-30T10:00:00+00:00" } } }- Identify the user with
valyd_id(their Valyd identity id — the OIDCsub). Email is not accepted here. - Identify the badge with its numeric
badge_idfrom List badges. - Idempotent — a user holds a badge at most once, and a repeat call while an offer is still pending reuses the open request (no duplicate notification).
The user must have connected to your app first. A grant only succeeds for a user who has signed in to your application through Valyd (OAuth/OIDC) at least once. This stops a client from granting badges to arbitrary identities. If they haven’t connected, the call returns
403 user_not_connected— send them through your “Connect with Valyd” flow, then grant.
Errors — 404 not_found (badge isn’t in your org), 403 user_not_connected,
400 invalid_request (missing valyd_id/badge_id).
Revoke a badge
Remove a granted badge from a user. Only removes the grant (the user_badges row) — staff badges
assigned through the member lifecycle are managed on the member, not here. Idempotent.
const { revoked } = await client.revokeBadge({ valydId: "valyd_9f8e7d6c5b4a3210fedcba98", badgeId: 12 });POST /api/sdk/badges/revoke HTTP/1.1
Host: dev.valyd.id
X-Client-Id: <your client_id>
X-Client-Secret: <your client_secret>
Content-Type: application/json
{ "valyd_id": "valyd_9f8e7d6c5b4a3210fedcba98", "badge_id": 12 }{ "success": true, "data": { "revoked": true } }revoked is false if the user didn’t hold that badge. Errors — 404 not_found,
400 invalid_request.
List a user’s badges
List the badges a user holds that you are allowed to see: all of your own org’s badges
(private + public) plus any other org’s public badges — the same rule as the portable OIDC
badges claim. Each badge carries its issuing organization so a cross-org public badge names
its trust anchor. Sources both directly-granted badges and active staff badges.
const { badges } = await client.getUserBadges("valyd_9f8e7d6c5b4a3210fedcba98");GET /api/sdk/users/valyd_9f8e7d6c5b4a3210fedcba98/badges HTTP/1.1
Host: dev.valyd.id
X-Client-Id: <your client_id>
X-Client-Secret: <your client_secret>{
"success": true,
"data": {
"valyd_id": "valyd_9f8e7d6c5b4a3210fedcba98",
"badges": [
{
"id": 12,
"name": "Background Check",
"visibility": "private",
"expiry_date": "2027-01-01",
"expired": false,
"created_at": "2026-09-30T10:00:00+00:00",
"organization": { "id": 42, "name": "Acme, Inc." }
}
]
}
}Require a badge for login
Badges can gate sign-in to your private apps. In the Developer Portal, open your project, make it private, and select one or more required badges — only users who hold every selected badge can sign in through Valyd (staff membership plus granted badges both count). This is configured in the portal, not this API; the requirement is enforced by Valyd at login.
Typical flow: verification reuse
- Create the badge once — in the portal or with
createBadge(); note itsid(or read it later withlistBadges()). - User connects — the person passes your check, then signs in to your app with Valyd (OAuth), so they’re connected to your client.
- Offer — call
grantBadge({ valydId, badgeId, firstName, lastName }). This returnsstatus: "pending"and notifies the user. - User approves — on their device they approve the offer (and complete KYC if their identity isn’t verified yet). Only then is the badge added to their Valyd identity. You get no callback.
- Reuse — once granted, if the badge is
public, other apps see it in the user’sbadgesclaim; if you gate a private app on it, the badge lets the user straight in.
See the Checkr example app (checkr/server) for an end-to-end verification-reuse integration.