Developer documentation

lemma.id proof of humanity

Integrate verified-human credentials with site-private PPIDs, browser-local verification, and server-side abuse controls.

/

5-minute quickstart

  1. Set siteId to your site's canonical hostname (for example app.example.com). It must match the hostname users see in the browser on your domain.
  2. Add the verifier SDK to your page (see SDK integration below).
  3. Gate a sensitive action with await verifier.verify({ autoProvision: true }). The first visit may open a Lemma-hosted popup (wallet unlock + live IDV). Fail closed when human is false.
  4. Store ppid on your user or account record. It is an opaque, site-private identifier — not a government ID or email.
  5. Optional — API key: create one in the API key manager when you need server-side blocks or revocation requests. An API key is not required for the basic human check.
  6. On confirmed abuse: call POST /api/ishuman/site-block with your API key (see Revocation). Do not rely on the abuser's browser to enforce the ban.
No webhooks to configure

Relying sites do not register webhook URLs with lemma.id. Verification completes in the browser (SDK + Lemma-hosted popup). Your backend learns the outcome when the client sends you the ppid or a stamped audit event — not via a lemma.id callback.

When to register your site

Most integrations need only the browser SDK. Dashboard registration is required only when you call server-side abuse APIs.

GoalRegistration required?What to do
Gate actions with verify()NoSet siteId to the hostname users see in the browser (for example app.example.com).
Block or revoke a PPID server-sideYesRegister your site domain in the API key manager and use the returned X-API-Key.

When you register, site_domain must match your SDK siteId after normalization: lowercase, no scheme or path, no port. Strip www. if that is how users reach your app. Internal site_... identifiers are for API keys and database ownership — they are not your SDK siteId.

End-to-end recipe: gate signup

A minimal signup flow with no webhook handler on your backend:

HTML + JavaScript
<script src="https://lemma.id/sdk/ishuman-verifier.js"></script> <script> const verifier = new IsHumanVerifier({ siteId: 'app.example.com' }); document.getElementById('signup-form').addEventListener('submit', async (e) => { e.preventDefault(); const result = await verifier.verify({ autoProvision: true }); if (!result.human) { alert('Verification required: ' + (result.reason || 'not verified')); return; } const email = document.getElementById('email').value; await fetch('/api/signup', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, ppid: result.ppid }), }); }); </script>

On your server, store ppid on the new account record. Optionally stamp the first login for audit:

JavaScript (after signup succeeds)
const event = await verifier.stamp( { action: 'signup_complete', email }, { includeCredential: true } ); await fetch('/api/audit-log', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(event), });

lemma.id never calls your servers when IDV completes. The client sends ppid (or a stamped event) to your API after verify() succeeds.

Troubleshooting

  • siteId hostname mismatch — staging and production subdomains derive different PPIDs. Use the exact hostname users see; do not mix www. and bare domain unless that is intentional.
  • Persistent no_credential — pass { autoProvision: true } on entry-point calls (signup, post, checkout). Without it, the SDK will not open the Lemma popup for first-time users.
  • Blocks not applying — the API key's registered site_domain must match SDK siteId. A block issued for example.com does not affect PPIDs derived for app.example.com.
  • idv_cancelled — the user closed the popup or the browser blocked it. Prompt them to allow popups and try again.
  • revocation_data_untrusted — local revocation snapshot could not be validated (clock skew, stale cache). Retry after a moment; check system clock if it persists.

What you configure vs what lemma.id runs

You (relying site)lemma.id (platform)
Embed ishuman-verifier.js and call verify()Hosts the wallet + IDV popup and issues signed credentials
Set siteId to your hostnameDerives a per-site PPID bound to that hostname
Store ppid on your user recordDoes not receive your app's business data or audit logs
API key — only for site-block, network-revoke, etc.Operates upstream IDV (Didit by default) and platform webhooks internally
No webhook URL, Didit account, or upstream IDV setupIDV issuer webhooks terminate at lemma.id, not at your servers

What is lemma.id proof of humanity?

lemma.id proof of humanity is an identity-rooted enforcement layer for websites and applications. A user verifies once through a trusted IDV issuer (Didit by default), then receives an Ed25519-signed credential that can be reused on your site while preserving privacy via a site-private PPID.

Assurance

Credentials are issued only after a live identity check — liveness detection plus document-to-selfie matching. A bot, AI agent, or stolen ID image cannot obtain a credential, because a live human had to match a genuine government document at issuance.

Core model

The credential is rooted in a verified-person root derived from the IDV result. A master credential is issued for lemma.id; on your site, a Lemma-hosted popup derives a site-specific credential (a per-site PPID) on first request and caches it for later checks. Because the binding is to the person root — not to a wallet, email, or device — resetting any of those does not mint a fresh, clean identity.

What your servers store: nothing sensitive

The live identity check runs at the IDV issuer. verify() returns only { human, ppid, reason, timeMs }. Your backend never receives the government ID, selfie, legal name, or date of birth — so you get an enforcement-grade human signal without running a KYC stack or holding the PII that makes identity verification a compliance and breach liability. Store the ppid on your user record; treat it as an opaque per-site identifier, not as sensitive identity data.

Integration flow (your site)

This is the path a relying-site developer implements. Platform-side IDV webhooks are handled by lemma.id and are not part of your integration.

StepWhereWhat happens
1Your pageYou call verify({ autoProvision: true }) before a protected action (signup, post, checkout, etc.).
2Lemma popupIf the user has no proof yet, a popup at /wallet/ishuman-idv unlocks the wallet (passkey) and runs live IDV at Didit.
3lemma.idAfter approved IDV, lemma.id issues a master verified-human credential and derives a site-bound credential for your siteId.
4Your pageThe SDK validates signature, expiry, and revocation locally, then returns { human: true, ppid: "did:lemma:ppid_..." }.
5Your backendAccept the ppid (or a stamped event with includeCredential: true) from your client. Optionally re-verify cryptographically on your server — no lemma.id webhook required.

SDK integration

Add the hosted verifier SDK and gate actions with verify(). Use autoProvision: true on entry points so first-time users can complete IDV in a popup.

HTML + JavaScript
<script src="https://lemma.id/sdk/ishuman-verifier.js"></script> <script> const verifier = new IsHumanVerifier({ siteId: 'app.example.com' }); async function requireHuman() { const result = await verifier.verify({ autoProvision: true }); if (!result.human) { throw new Error(result.reason || 'not_verified'); } return result.ppid; // store on your user record } </script>

Constructor options

OptionTypeDefaultNotes
siteIdstringwindow.location.hostnameCanonical hostname for your site. The derived PPID is bound to this value.
lemmaOriginstringhttps://lemma.idOverride only for non-production testing.
autoProvisionbooleanfalseWhen true on the constructor, every verify() may open the popup if no proof exists. Prefer passing { autoProvision: true } on entry-point calls only.
debugbooleanfalseEnables SDK console logging.
isBlockedLocallyfunctionnullOptional callback (ppid) => boolean for site-level blocks without a network round-trip.

verify() result

verify() returns { human: boolean, ppid: string | null, reason: string, timeMs: number, error: string | null }.

Common reason values (grouped):

  • Success: valid, vc_valid, session_valid
  • Needs popup / first visit: no_credential, site_proof_required, wallet_locked, no_ishuman_credential
  • Failure: expired, revoked, invalid_signature, site_blocked, idv_cancelled, not_ishuman, revocation_data_untrusted

After the first successful verification, repeat calls on the same tab typically validate from cache with no network call (local Ed25519 + revocation bloom).

Attach verification to your own logs

Once a user is verified, associate their site-scoped PPID with actions you record — a comment, checkout, login, moderation decision. The SDK provides helpers to stamp events. This data lives entirely in your systems; lemma.id stores none of it.

The pattern

Call verify({ autoProvision: true }) once at an entry point. After that, getPPID(), getVerification(), and stamp() read the cached session with no popup — safe to use inline anywhere in your app.

Helper methods

MethodReturnsNotes
getPPID(opts?)Promise<string | null>Verified PPID, or null. Does not open a popup unless autoProvision: true.
getVerification(opts?)Promise<Stamp>Compact verification stamp for logging.
stamp(payload, opts?)Promise<Object>Copies payload and merges a lemma field. Your object is not mutated.

Options: { key?: string (default 'lemma'), includeCredential?: boolean, includeProof?: boolean, autoProvision?: boolean }.

Which evidence should I store?

For audit logs, use { includeCredential: true } — it stores the bare verifiable credential. It is compact, offline-verifiable, and durable until expiry. Use { includeProof: true } only when you also need the signed session assertion (replay resistance); it ages out and is less suitable for long-lived logs. Both re-verify on your backend without calling lemma.id per check.

Stamp shape

FieldTypeMeaning
verifiedbooleanWas a valid human proof present?
ppidstring | nullSite-scoped pseudonymous identifier.
reasonstringThe verify() reason code.
siteIdstringYour siteId hostname binding.
verifiedAtnumberUnix ms when the stamp was produced.
expiresAtnumber | nullCredential expiry (unix seconds).
credentialIdstring | nullUnderlying credential id.
credentialobject | nullPresent with { includeCredential: true }. Recommended for durable audit evidence.
proofobject | nullPresent with { includeProof: true }. Adds session assertion; not ideal for long-term logs.

Copy-paste

JavaScript
const verifier = new IsHumanVerifier({ siteId: 'app.example.com' }); // 1) Verify once at an entry point (may open a popup on first visit). await verifier.verify({ autoProvision: true }); // 2) Stamp actions and POST to YOUR backend. async function logAction(action, extra = {}) { const event = await verifier.stamp({ action, ...extra, at: Date.now() }, { includeCredential: true }); await fetch('/my/api/audit-log', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(event), }); } await logAction('post_comment', { commentId: 123 }); const ppid = await verifier.getPPID();

Re-verify a stored stamp on your backend

If you captured a stamp with { includeCredential: true }, confirm on your own infrastructure that a logged action came from a verified human. verifyStamp() checks the credential, revocation status, and that the stamp's ppid matches. lemma.id is contacted only to refresh the signed revocation snapshot (cached ~15 min), not on every audit row.

For old log rows, pass { durable: true } so an aged session assertion is treated as informational while the credential check still applies.

Node.js / Deno / Workers
import { createVerifier } from "https://lemma.id/sdk/lemma-ishuman-verify.mjs"; const verifier = createVerifier({ siteId: "app.example.com" }); const check = await verifier.verifyStamp(row.lemma); if (!check.ok) { flagSuspiciousLogRow(row, check.reason); }
Python
# curl -O https://lemma.id/sdk/lemma_ishuman_verify.py from lemma_ishuman_verify import VerificationContext ctx = VerificationContext(site_id="app.example.com") check = ctx.verify_stamp(row["lemma"]) if not check.ok: flag_suspicious_log_row(row, check.reason)

API reference

Most integrations use only the browser SDK. These HTTP endpoints support moderation, optional server checks, and the Lemma-hosted wallet popup (which calls some of them on your users' behalf).

MethodPathPurposeAuth
POST/api/ishuman/start-verificationCreate a Didit IDV session.Wallet assertion
GET/api/ishuman/verification-status/<session_id>Poll status after IDV (used by the Lemma popup; not required if you use the SDK only).None (unguessable session id)
POST/api/ishuman/derive-site-proofDerive site-bound credential from master proof.Wallet assertion + wallet_secret (popup path)
POST/api/ishuman/verify-presentationOptional server-side re-verify (prefer local verifyStamp()).None
POST/api/ishuman/site-blockImmediate site-level PPID block.X-API-Key
POST/api/ishuman/site-unblockRemove site-scoped block.X-API-Key
POST/api/ishuman/network-revokeSubmit network-wide revocation request (reviewed).X-API-Key
GET/api/ishuman/checkCheck block / revocation status for a PPID.None
GET/api/ishuman/site-blocksList active blocks for your site.X-API-Key
GET/api/ishuman/statsPublic network statistics.None

Wallet assertion proves control of the user's wallet signing key (Ed25519 challenge/response). It is used by the Lemma wallet popup — not by typical relying-site server code. X-API-Key is your site API key from the API key manager.

Revocation and abuse controls

Revocation is tiered. Do not rely on the abuser's browser or wallet to enforce a ban.

Tier 0: Immediate site deny (your app)

Deny the current session or action (403, sign-out, etc.) while lemma.id propagates canonical revocation.

Tier 1: Site-bound PPID revocation (immediate, canonical)

Call POST /api/ishuman/site-block with your site API key. lemma.id writes a SiteBlock row and a canonical RevocationList entry, then publishes a Bloom snapshot so verifiers reject that PPID on your site across devices.

You can also revoke IAM users via POST /api/developer/sites/<site_id>/users/<ppid>/revoke, which triggers the same canonical PPID path.

With the verifier SDK, use isBlockedLocally(ppid) or call GET /api/ishuman/check server-side.

curl — site-block
curl -X POST https://lemma.id/api/ishuman/site-block \ -H "Content-Type: application/json" \ -H "X-API-Key: YOUR_SITE_API_KEY" \ -d '{"ppid": "did:lemma:ppid_...", "reason": "Terms violation — automated activity"}'

Tier 2: Network revocation (reviewed)

Request network action via POST /api/ishuman/network-revoke with your site API key. A tier 1 site block applies immediately while the request is reviewed. lemma.id operators approve network-wide wallet revocation internally — POST /api/ishuman/approve-revocation is not a customer-facing endpoint.

For a side-by-side comparison of what your site, lemma.id, Auth0, and direct KYC each store, see the Trust & Personal Data Minimization page.

Privacy model (PPID by site)

  • Each site's PPID is derived from the verified-person root and the normalized site hostname. Binding to the person root — not a wallet secret, email, or device — is what makes a ban survive credential rotation on your site.
  • Derived credentials are site-specific and pairwise-unlinkable; one site's PPID cannot be correlated with another site's PPID by the relying sites.
  • Relying sites receive only a human verdict and their own site-private PPID — never the user's name, ID document, date of birth, or other IDV PII.
  • Internal site identifiers (site_..., used for API keys and database ownership) are separate from the hostname used as siteId in the SDK.
What lemma.id can and can't see

lemma.id holds the verified-person root that links credentials for re-verification and network revocation. After approved issuance (and on declined, expired, or abandoned sessions), lemma.id requests upstream deletion of raw IDV session data at Didit — document images, liveness captures, and selfies. Your site never receives that material. Routine verification on your site runs locally in the browser; lemma.id does not observe which pages users visit or when they pass verify() on your domain.