Organization API
Manage your workforce roster server-to-server. Add and invite members, look people up,
deactivate/reactivate, remove, re-send invites, and read your seats & billing — all under the base
path /api/sdk. Every call is made with your organization’s client_id + client_secret, so
this API is server-side only — the secret must never reach a browser.
The examples below use the @valyd/sdk client and raw HTTP against
https://dev.valyd.work/api/sdk.
import { ValydClient } from "@valyd/sdk";
// Server-side only — the client secret never touches the browser.
const client = new ValydClient({
clientId: process.env.VALYD_CLIENT_ID,
clientSecret: process.env.VALYD_CLIENT_SECRET,
});Authentication
All endpoints authenticate with your organization app’s client_id + client_secret, sent as
request headers (X-Client-Id / X-Client-Secret), scoped to the organization that owns the app.
The @valyd/sdk client sends them for you.
X-Client-Id: <your client_id>
X-Client-Secret: <your client_secret>
Content-Type: application/jsonResponse envelope
Every response is wrapped in a consistent envelope.
Success (any 2xx):
{ "success": true, "data": { } }Error:
{ "success": false, "error": { "code": "…", "message": "…" }, "data": { } }The per-endpoint examples below show what goes inside data.
The member object
Wherever a member appears in a response, it is the member object — the same shape everywhere:
{
"id": 123,
"member_id": "vmem_1a2b3c4d5e6f7a8b9c0d1e2f", // stable public id = the valyd_org_member_id login claim; prefix vmem_ + 24 hex
"first_name": "Ada",
"last_name": "Lovelace",
"email": "ada@acme.com",
"role": "member", // one of: owner | admin | developer | member
"status": "active", // one of: invited | link_sent | active | deactivated
"valyd_id": "usr_…", // null until the member connects their Valyd ID (face)
"active": true, // convenience: status === "active"
"created_at": "2026-08-21T10:00:00+00:00"
}member_id— the stable public id (vmem_+ 24 hex). It’s the value of thevalyd_org_member_idlogin claim, so it’s your join key between your roster and Valyd’s login layer. Wherever a path takes{member_id}, the member’s email orvalyd_idis also accepted.valyd_id—nulluntil the member scans their face and connects their Valyd ID.active— a convenience boolean equal tostatus === "active".
List members
Returns your workforce roster (role: member only), each with status and valyd_id, plus the
organization it belongs to.
const { members, organization } = await client.getMembers();
const activated = members.filter((m) => m.status === "active");GET /api/sdk/members HTTP/1.1
Host: dev.valyd.work
X-Client-Id: <your client_id>
X-Client-Secret: <your client_secret>{
"success": true,
"data": {
"members": [
{
"id": 123,
"member_id": "vmem_1a2b3c4d5e6f7a8b9c0d1e2f",
"first_name": "Ada",
"last_name": "Lovelace",
"email": "ada@acme.com",
"role": "member",
"status": "active",
"valyd_id": "usr_9f8e7d6c5b4a",
"active": true,
"created_at": "2026-08-21T10:00:00+00:00"
}
],
"organization": { "id": 42, "name": "Acme, Inc." }
}
}Resolve one person
Look up one person’s membership at any role — so you can tell “you’re a developer here”
from “not in this org.” Pass a valyd_id or an email (at least one).
const { found, member } = await client.resolveMember({ valydId: "usr_9f8e7d6c5b4a" });
// or: await client.resolveMember({ email: "ada@acme.com" });POST /api/sdk/members/resolve HTTP/1.1
Host: dev.valyd.work
X-Client-Id: <your client_id>
X-Client-Secret: <your client_secret>
Content-Type: application/json
{ "valyd_id": "usr_9f8e7d6c5b4a" }{
"success": true,
"data": {
"found": true,
"member": {
"id": 123,
"member_id": "vmem_1a2b3c4d5e6f7a8b9c0d1e2f",
"first_name": "Ada",
"last_name": "Lovelace",
"email": "ada@acme.com",
"role": "member",
"status": "active",
"valyd_id": "usr_9f8e7d6c5b4a",
"active": true,
"created_at": "2026-08-21T10:00:00+00:00"
}
}
}Not in the organization returns { "found": false, "member": null }.
Errors — 400 missing_identifier when neither valyd_id nor email is given.
Add member(s)
Add one or more people (1–500 per call). email is required per member; first_name, last_name,
and role (default member) are optional. Options: notify (default true) emails the invite;
invite (auto | email | face, default auto) picks the invite method — see
Members & onboarding. Returns 201.
const { created, skipped, notified } = await client.addMembers(
[
{ email: "jane@acme.com", firstName: "Jane", lastName: "Doe" },
{ email: "sam@acme.com" },
],
{ notify: true, invite: "auto" }
);POST /api/sdk/members HTTP/1.1
Host: dev.valyd.work
X-Client-Id: <your client_id>
X-Client-Secret: <your client_secret>
Content-Type: application/json
{
"members": [
{ "email": "jane@acme.com", "first_name": "Jane", "last_name": "Doe" },
{ "email": "sam@acme.com" }
],
"notify": true,
"invite": "auto"
}{
"success": true,
"data": {
"created": [
{
"id": 201,
"member_id": "vmem_aa11bb22cc33dd44ee55ff66",
"first_name": "Jane",
"last_name": "Doe",
"email": "jane@acme.com",
"role": "member",
"status": "link_sent",
"valyd_id": null,
"active": false,
"created_at": "2026-08-21T10:05:00+00:00",
"activation_link": "https://idp.valyd.work/m/activate/xxxxxxxxxxxx",
"invite_method": "face"
}
],
"skipped": [
{ "email": "sam@acme.com", "reason": "duplicate" }
],
"notified": true
}
}Each created entry is a member object plus:
activation_link— a string for face invites only;nullotherwise. Email/claim links are emailed, never returned. Passingnotify: falsehands the face link back so you can deliver it yourself.invite_method—"face","email", or"none".reactivated— present andtruewhen re-adding a previously deactivated email reactivates that person instead of skipping them.
Errors — 400 invalid_request, with a per-field validation map in data.errors.
Deactivate
Stops billing and revokes the member’s app logins, but keeps the row. The person’s Valyd identity is not deleted.
const { member } = await client.deactivateMember("vmem_1a2b3c4d5e6f7a8b9c0d1e2f");PATCH /api/sdk/members/vmem_1a2b3c4d5e6f7a8b9c0d1e2f/deactivate HTTP/1.1
Host: dev.valyd.work
X-Client-Id: <your client_id>
X-Client-Secret: <your client_secret>{
"success": true,
"data": {
"member": {
"id": 123,
"member_id": "vmem_1a2b3c4d5e6f7a8b9c0d1e2f",
"first_name": "Ada",
"last_name": "Lovelace",
"email": "ada@acme.com",
"role": "member",
"status": "deactivated",
"valyd_id": "usr_9f8e7d6c5b4a",
"active": false,
"created_at": "2026-08-21T10:00:00+00:00"
}
}
}The path takes the vmem_… member_id (the member’s email or valyd_id is also accepted).
Remove
Removes a member. Default (no permanent) is the same as deactivate. Pass
permanent=true to delete the membership row outright. The person’s Valyd account is never deleted.
await client.removeMember("vmem_1a2b3c4d5e6f7a8b9c0d1e2f", { permanent: true });DELETE /api/sdk/members/vmem_1a2b3c4d5e6f7a8b9c0d1e2f?permanent=true HTTP/1.1
Host: dev.valyd.work
X-Client-Id: <your client_id>
X-Client-Secret: <your client_secret>Permanent delete — the row is gone:
{ "success": true, "data": { "member": null, "removed": true } }Default (deactivate) — no permanent flag returns the deactivated member object:
{
"success": true,
"data": {
"member": {
"id": 123,
"member_id": "vmem_1a2b3c4d5e6f7a8b9c0d1e2f",
"first_name": "Ada",
"last_name": "Lovelace",
"email": "ada@acme.com",
"role": "member",
"status": "deactivated",
"valyd_id": "usr_9f8e7d6c5b4a",
"active": false,
"created_at": "2026-08-21T10:00:00+00:00"
}
}
}Already gone — removing a member that no longer exists is idempotent:
{ "success": true, "data": { "member": null, "already_removed": true } }The path takes the vmem_… member_id (the member’s email or valyd_id is also accepted).
Reactivate
Restores a deactivated member to active if their Valyd identity still exists, or back to
invited (they must re-activate by face) if it doesn’t.
const { member } = await client.reactivateMember("vmem_1a2b3c4d5e6f7a8b9c0d1e2f");PATCH /api/sdk/members/vmem_1a2b3c4d5e6f7a8b9c0d1e2f/reactivate HTTP/1.1
Host: dev.valyd.work
X-Client-Id: <your client_id>
X-Client-Secret: <your client_secret>{
"success": true,
"data": {
"member": {
"id": 123,
"member_id": "vmem_1a2b3c4d5e6f7a8b9c0d1e2f",
"first_name": "Ada",
"last_name": "Lovelace",
"email": "ada@acme.com",
"role": "member",
"status": "active",
"valyd_id": "usr_9f8e7d6c5b4a",
"active": true,
"created_at": "2026-08-21T10:00:00+00:00"
}
}
}The path takes the vmem_… member_id (the member’s email or valyd_id is also accepted).
Errors — 404 not_found.
Re-send invite
Re-issues the member’s face-activation link — it supersedes the old one, is emailed, and is also returned in the response. Throttled to 10 requests / minute.
const { member, activation_link } = await client.resendMemberInvite("vmem_1a2b3c4d5e6f7a8b9c0d1e2f");POST /api/sdk/members/vmem_1a2b3c4d5e6f7a8b9c0d1e2f/invite HTTP/1.1
Host: dev.valyd.work
X-Client-Id: <your client_id>
X-Client-Secret: <your client_secret>{
"success": true,
"data": {
"member": {
"id": 123,
"member_id": "vmem_1a2b3c4d5e6f7a8b9c0d1e2f",
"first_name": "Ada",
"last_name": "Lovelace",
"email": "ada@acme.com",
"role": "member",
"status": "link_sent",
"valyd_id": null,
"active": false,
"created_at": "2026-08-21T10:00:00+00:00"
},
"activation_link": "https://idp.valyd.work/m/activate/yyyyyyyyyyyy"
}
}The path takes the vmem_… member_id (the member’s email or valyd_id is also accepted).
Errors — 404 not_found; 409 already_active (the member already connected a Valyd ID);
409 deactivated (reactivate first).
Billing & seats
Read your subscription, seat count, next-charge estimate, wallet balance, and invoices. Full seat model on Pricing & billing.
const billing = await client.getBilling();GET /api/sdk/billing HTTP/1.1
Host: dev.valyd.work
X-Client-Id: <your client_id>
X-Client-Secret: <your client_secret>{
"success": true,
"data": {
"subscription": {
"plan": "workforce",
"status": "active",
"price_per_seat": 0.99,
"in_trial": false,
"trial_ends_at": "2026-08-14T00:00:00+00:00",
"current_period_end": "2026-09-01T00:00:00+00:00"
},
"seats": 12,
"estimated_next_charge": 11.88,
"currency": "USD",
"balance": 4.20,
"invoices": [
{ "period": "2026-07", "seats": 10, "amount": 9.90, "created_at": "2026-08-01T00:00:00+00:00" }
]
}
}subscription is null before a plan exists. seats is the count of active members;
estimated_next_charge is seats × price_per_seat.