Humad is a World ID–powered identity layer for any web app. Two products live here:Login with Humad (OIDC) for full sign-in flows, and a Verify APIfor one-shot human-verification checks. Pick the section that matches what you're building.
Adds a "Sign in" button to your app. Users authenticate with World ID, you get a stable sub, handle, and verified-human status. Standards-compliant OAuth 2.1 / OIDC.
client_id and client_secret.npm install humad-sdksignIn() and handleCallback() (see below).Live demo: humad-demo.vercel.app
Replace CAPTCHA. No session, no users — just "is this a human?" with a managed nullifier dedup store.
npm install humad-sdk @worldcoin/idkitimport { HumanAuth } from "humad-sdk/react";
function VerifyButton() {
return (
<HumanAuth
appId="app_your_world_id"
apiKey="ha_your_key"
action="login"
onVerified={(r) => console.log(r.nullifier_hash, r.is_new_user)}
onError={(e) => console.error(e)}
/>
);
}The React component is still exported as HumanAuth for v0.2.x compatibility; it will be renamed to Humad in v1.0.
Standards-compliant OAuth 2.1 + OpenID Connect. PKCE-required for public clients, client_secret_post / client_secret_basic for confidential clients, RS256-signed id_tokens, refresh token rotation, RP-Initiated Logout.
Go to /dashboard/oauthand create a client. You'll need at minimum a name and one redirect_uri. For SPAs, also set post_logout_redirect_uris.
client_secret; PKCE is enforced.client_secret in your backend; PKCE still recommended.openid (required), profile, verified_human, email, offline_access (for refresh tokens).You can later edita client's name / homepage / redirect_uris / scopes from the dashboard (the client_id and client_type stay frozen). Confidential clients also expose a Rotate secret button — handy when a secret leaks; the old secret stops working immediately, so coordinate with your RP deploy.
import { signIn, handleCallback, getUser } from "humad-sdk";
// On your "Login with Humad" button:
await signIn({
clientId: "ha_oauth_xxxxxxxx",
redirectUri: "https://yourapp.com/oauth/callback",
scopes: ["openid", "profile", "verified_human"],
});
// On /oauth/callback:
const { tokens } = await handleCallback({ clientId: "ha_oauth_xxxxxxxx" });
const user = await getUser(tokens);
// user.sub — stable, pairwise user id (string)
// user.handle — Humad handle (e.g. "alice")
// user.verified_human — boolean
// user.verification_level — "orb" | "device" | "phone"import { exchangeCodeForTokens, getUserInfo } from "humad-sdk";
// In your /oauth/callback route handler:
const tokens = await exchangeCodeForTokens({
clientId: process.env.HUMAD_CLIENT_ID!,
clientSecret: process.env.HUMAD_CLIENT_SECRET!,
code,
codeVerifier,
redirectUri,
});
const user = await getUserInfo({ accessToken: tokens.accessToken });Access tokens expire in 15 minutes. Pick the helper that matches your security posture.
import { startAutoRefresh } from "humad-sdk";
const controller = startAutoRefresh({
clientId: "ha_oauth_xxxxxxxx",
initialTokens: tokens,
onUpdate: (next) => setTokens(next), // persist the rotated tokens
onError: (err) => console.warn(err), // refresh failed — fall back to signIn()
refreshLeewaySec: 60, // renew 60s before exp
});
// later (sign out / unmount):
controller.stop();import { silentRenew, handleSilentCallback } from "humad-sdk";
try {
const { tokens: next } = await silentRenew({
clientId: "ha_oauth_xxxxxxxx",
redirectUri: "https://yourapp.com/oauth/callback",
});
setTokens(next);
} catch (err) {
// SSO expired or interaction required — fall back to signIn()
}
// On /oauth/callback, handle BOTH normal and silent iframe results:
const wasSilent = await handleSilentCallback({ clientId });
if (!wasSilent) {
const { tokens } = await handleCallback({ clientId });
// ... normal post-login flow
}Silent renew relies on the Humad SSO cookie. If the user has been inactive long enough for the SSO session to expire, the iframe will time out and you must initiate a regular signIn().
Two flavors, depending on whether you want to keep the Humad SSO session alive across other RPs.
Invalidates the user's tokens for your app. The Humad SSO session continues, so the user stays signed in to other RPs.
import { signOut } from "humad-sdk";
await signOut({
token: tokens.refreshToken!,
tokenTypeHint: "refresh_token",
clientId: "ha_oauth_xxxxxxxx",
});Ends the user's Humad SSO session and redirects back to your post-logout page. Use this for a real "sign out everywhere" UX. Requires post_logout_redirect_uri to be pre-registered on the client.
await signOut({
mode: "navigate",
clientId: "ha_oauth_xxxxxxxx",
idTokenHint: tokens.idToken,
postLogoutRedirectUri: "https://yourapp.com/goodbye",
state: "csrf-token-or-return-marker",
});Need a URL only (e.g. for an <a href>)? Use buildEndSessionUrl(...). The endpoint follows OIDC RP-Initiated Logout 1.0.
Direct REST access if you don't want to use the SDK. Use this when you only need one-shot human verification (CAPTCHA-replacement) and don't need user sessions.
/api/verifyVerify a World ID proof. Handles Cloud API verification, nullifier dedup, and MAU tracking.
x-humanauth-key: ha_your_api_key{
"proof": "0x...",
"merkle_root": "0x...",
"nullifier_hash": "0x...",
"action": "login",
"verification_level": "orb" // optional
}{
"success": true,
"nullifier_hash": "0x...",
"is_new_user": true,
"action": "login"
}/api/rp-contextGenerate a signed RP context for World ID 4.0 verification. Required for IDKit v4.
{ "action": "login" } // optional, defaults to "humanauth-verify"{
"rp_context": {
"rp_id": "app_xxx",
"nonce": "...",
"created_at": 1234567890,
"expires_at": 1234567890,
"signature": "0x..."
}
}/api/appsApp management endpoints. Requires dashboard JWT authentication.
GET /api/apps — list your appsPOST /api/apps — register a new appGET /api/apps/:id — app details + recent logsDELETE /api/apps/:id — delete (cascades keys, logs, nullifiers)GET /api/apps/:id/keys — list API keysPOST /api/apps/:id/keys — create new API keyDELETE /api/apps/:id/keys — revoke a key (body: {"key_id": "..."})Most OIDC client libraries can configure themselves from the discovery document.
https://humanauth.vercel.app/.well-known/openid-configurationNote on domains: the IdP issuer currently lives at humanauth.vercel.app as a placeholder. A humad.*custom domain will be assigned later. The SDK reads the issuer URL from discovery, so SDK consumers won't see a breaking change. If you hardcode issuer in your config, plan a one-line update.
A unique, anonymous identifier per (user, action). The same person verifying "login" and "vote" produces different nullifiers — privacy by design. Humad stores and deduplicates these automatically. For OIDC, the sub claim is a pairwise identifier derived from the user's World ID nullifier and the RP's client_id.
World ID 4.0 requires all verification requests to be signed by the Relying Party. Humad manages your signing keys (encrypted at rest with AES-256-GCM) and generates signed RP contexts on demand.
Monthly Active Users are counted by unique nullifier per calendar month. Your dashboard shows real-time MAU counts and historical trends.
Humad maintains a session cookie on the IdP domain so a user signing in to one RP can sign in to other RPs without re-prompting World ID. Silent refresh and prompt=none rely on this. RP-Initiated Logout ends the SSO session for all RPs at once.
Humad signs id_tokens with RS256 keys exposed at /.well-known/jwks.json. We support overlap-style rotation: the new key publishes alongside the previous key for a grace period so already-issued id_tokens remain verifiable. RPs that fetch JWKS dynamically (the SDK does this) need no action.
Run tsx scripts/rotate-oidc-keys.ts to generate a fresh keypair; promote it via the OIDC_* / OIDC_PREV_* environment variables. Only the Humad operator can do this.
access_token: 15 minutes (opaque, server-introspected)id_token: 15 minutes (RS256 JWT)refresh_token: 30 days, rotated on every useGET /api/health returns 200 with a JSON body when the IdP is healthy (DB reachable, OIDC signing key loaded), 503 otherwise. Suitable for UptimeRobot / Vercel Cron / your own monitor.
{
"ok": true,
"service": "humanauth",
"env": "production",
"sha": "ab12cdef",
"durationMs": 42,
"checks": {
"db": { "ok": true, "durationMs": 38 },
"jwks": { "ok": true, "durationMs": 4 }
}
}Beta. Issuer is humanauth.vercel.app until the humad.* domain is assigned. Production RPs are welcome — give us a heads up before launch so we can keep your client_id pinned through any migrations.