Build with Veri.
Veri is built for developers who want to add high-assurance identity verification without asking users to hand over their data.
Integration Guide
Contents
1. What is Veri
Your users have to prove who they are, and today that usually means handing you their email address — which you then have to store, protect, and justify holding. Veri removes that trade. A user proves they control a verified email address at a real domain, and you find out which domain without ever learning the address. Fewer personal details on your side, less to leak, and the same confidence that a real person with a real, verified mailbox is signing in.
After a successful sign-in your app receives a small set of claims. The important ones are sub, a stable identifier for that person on your site only; email_domain, the real verified domain such as gmail.com, which is what you show the user; and veri_credential_type, telling you how strong the proof was. You never receive, and cannot request, the user's actual email address — or their wallet address.
"Sign in with Veri" is a standard OpenID Connect provider (authorization code flow, RS256-signed ID tokens, pairwise subjects). Use whatever OIDC library your stack already has, or plug Veri into an off-the-shelf app that supports a generic OIDC provider.
2. How it works
- Your app redirects the user to Veri's authorization endpoint.
- Veri opens the user's vault, which names your app and shows exactly what you will learn. If the user holds more than one credential, they choose which one to share.
- On approval, Veri redirects back to your registered redirect URI with a one-time authorization code and your
state. - Your server exchanges the code for tokens, authenticating with your client secret.
- Your app verifies the ID token and reads the credential. Veri mints it from the user's credential as recorded on-chain at that moment.
3. Before you start
- A
client_idandclient_secret, issued instantly — no waiting and no emailing us:- Click Get client credentials on getveri.co/developers.
- Enter your email, your app's name and its redirect URIs.
- Type in the 6-digit code we email you.
- Copy your client ID and secret straight into your
.envfile. The secret is shown once.
- Your redirect URI(s), registered with Veri. Matching is exact string equality — there are no wildcards and no localhost exception, so register every port you develop on —
http://localhost:3000/callbackmust be registered as its own entry, andhttps://example.com/callbackandhttps://example.com/callback/are different URIs. Use HTTPS in production; the authorization code travels on this URL. - PKCE with
S256. Clients registered today are required to send acode_challenge; a request without one is refused. - An OIDC library for your stack — see Section 4.
4. Quick start
All four use the same environment variables. SESSION_SECRET is any long random string; the Express and FastAPI examples sign their session cookie with it.
VERI_ISSUER=https://id.veribeta.io
VERI_CLIENT_ID=your_client_id
VERI_CLIENT_SECRET=your_client_secret
VERI_REDIRECT_URI=http://localhost:3000/callback
SESSION_SECRET=a_long_random_string5. Endpoints & tokens
Everything below is published in the discovery document, which every OIDC library reads for you. Point your library at the issuer and let it discover the rest.
https://id.veribeta.io/.well-known/openid-configuration| Endpoint | URL |
|---|---|
| Authorization | https://id.veribeta.io/api/oidc/authorize |
| Token | https://id.veribeta.io/api/oidc/token |
| UserInfo | https://id.veribeta.io/api/oidc/userinfo |
| JWKS | https://id.veribeta.io/.well-known/jwks.json |
| Setting | Value |
|---|---|
| Flow | Authorization code only — response_type=code, grant_type=authorization_code. No implicit or hybrid flow, and no refresh tokens. |
| Scopes | openid is required. Also supported: email, email_domain, profile. Request openid email email_domain. |
| PKCE | S256 only. plain is rejected. |
| Client auth | At the token endpoint: client_secret_basic (HTTP Basic, the OIDC default) or client_secret_post (in the body). Both work. |
| ID token | JWT signed with RS256; verify it against the JWKS. Valid for 1 hour (exp = iat + 3600). |
| Access token | Opaque, valid for 1 hour, usable only at the UserInfo endpoint. Returned with token_type: Bearer and expires_in: 3600. |
| Authorization code | Single use, expires after 10 minutes, and bound to the exact redirect_uri and PKCE verifier of its request. |
| Subject type | pairwise — see the warning in Section 6. |
The UserInfo endpoint returns the same claims as the ID token (except the token-only iss, aud, iat, exp and nonce) for clients that read profile data there instead. It accepts the access token as Authorization: Bearer on GET or POST.
6. Reading the credential
Once your library has verified the token you will see the standard OIDC claims — iss, aud, sub, iat, exp, nonce — plus a few that are specific to Veri and that no OIDC documentation covers. Here is what each one means and what to do with it.
| Claim | What it is | Example | What to do with it |
|---|---|---|---|
| sub | A pairwise identifier, unique to this user on your site | 9f2c81a4e0… (64 hex characters) | Use as your user ID. Store the issuer alongside it. A sub without its issuer cannot be matched if Veri ever migrates issuers. |
| A pseudonymous address at Veri's domain | 4f3a9c2e1b7d5a80@ id.veribeta.io | Never display it, never email it. It is stable per user per site but is not a real inbox. It exists for platforms that refuse to create an account without an email claim. | |
| email_ | Always true | true | Veri does not mint a token for a user without a verified credential, so you can trust it. |
| email_ | The real verified domain | gmail.com | Show this to the user. "Verified gmail.com address" is the right display string. |
| name, preferred_ | A pseudonymous handle derived from the email claim | veri-4f3a9c2e | For apps that require a display name or username to create a profile. It reveals nothing about the user. |
| veri_ | How strong the proof was | email_address_v2 | email_address_v2 proves control of a specific mailbox; email_domain_v1 proves only that they hold an address at the domain. Most apps just check email_verified. |
| veri_ | The credentials disclosed in this sign-in, as objects | [{"type": "email_address_v2", "domain": "gmail.com"}] | Sorted strongest first. If the user chose a credential to share, this holds only that one, and email_domain and veri_credential_type describe it too. |
WarningThe sub is different on every site
The same Veri user gets a different sub on your site than on any other. That is by design — it means nobody can correlate your users against another service. Store the issuer alongside the sub and use the pair as your account key. A future issuer migration changes every sub, and if you stored the sub alone you will silently stop recognising returning users.
const userKey = { issuer: "https://id.veribeta.io", sub: claims.sub }; // right
const userKey = claims.sub; // wrongTwo things worth knowing. veri_credentials holds objects, not strings — each has a type and a domain. And a user's sub and pseudonymous email are derived from their vault, not from the credential, so whichever credential they choose to share, they land in the same account on your site.
Device possession & token holdings
On the staging issuer (https://id-staging.veribeta.io), a sign-in can also carry two claims that production does not issue yet. Both are absent on an ordinary sign-in, so treat them as optional.
veri_mesh— what was proven during this sign-in:credentials(for exampledevice_possession_v1, ortoken_holder_v1with anasset_idandthreshold), per-checkoutcomes(asserted,not_held,rejected,unavailable), pluscomplete,degradedandcompleted_at. Ifdegradedis true, a check could not run — never read that as "the user lacks it".amr—["swk"](RFC 8176) only when the user proved possession of their vault key from their device. Gate on this if you require the stronger proof.
"veri_mesh": {
"credentials": [{ "type": "device_possession_v1" }],
"outcomes": [{ "verifier": "device_possession", "status": "asserted" }],
"complete": true,
"degraded": false,
"completed_at": 1791000000
},
"amr": ["swk"]Verified Token Holder credentials reach a relying party only through veri_mesh; they never appear in veri_credentials.
7. Security checklist
- PKCE — always use
S256. Veri rejectsplain, and new clients must send a challenge. - Nonce — generate a random nonce, store it in the session, verify the token's nonce matches.
- State — generate a random state, store it in the session, verify it in the callback to prevent CSRF.
- Signature — verify the ID token's RS256 signature against the JWKS. Some libraries skip this by default when the token arrives over TLS; turn it on.
- Issuer — pin
isstohttps://id.veribeta.ioand reject anything else. - Audience — pin
audto yourclient_idand reject tokens meant for others. - Expiry — check
exp. Most libraries do this for you; confirm yours does. - Secret — keep the
client_secretand the token exchange on your server, never in a browser or app bundle. - Sub storage — store the issuer alongside the
sub. See Section 6 for why. - HTTPS — use HTTPS for your redirect URI in production, never plain HTTP.
8. Error handling
Errors reach you in three places, and only the first two come from Veri.
From the token endpoint
JSON bodies of the form { "error": "…", "error_description": "…" }.
| Error | What it means | What to do |
|---|---|---|
| invalid_ | HTTP 401. Wrong client_id or client_secret, or the client is inactive. | Check your environment variables. |
| invalid_ | The code is unknown, already used or expired (10 minutes); the redirect_uri differs from the original request by even a byte; or PKCE verification failed. | Restart the sign-in flow. If it repeats, compare the redirect URI and code verifier you send. |
| credential_ | The user holds no verified email credential. | Ask them to verify an email in their Veri vault, then sign in again. |
| invalid_ | A required parameter is missing or malformed. | Fix the request. Your library normally prevents this. |
| unsupported_ | Anything other than authorization_code. | Use the authorization code flow. |
| rate_ | HTTP 429. More than 30 token requests a minute for your client. | Back off and retry. |
On your callback URL
Veri redirects back only to a registered URI. Handle error=access_denied on your callback (it arrives with your state and an error_description): that is the standard OIDC response to a declined sign-in, and it is how Veri reports a user who declines. Until that rollout completes, a user who declines is not sent back to your app.
Shown to the user instead
If the authorization request itself is invalid — an unknown or inactive client_id, an unregistered redirect_uri, a missing PKCE challenge, or a scope without openid — Veri shows the user an error page and does not redirect to you, because the redirect URI cannot be trusted. Nothing arrives at your callback. Test your configuration end to end before you ship.
Everything else — an expired ID token, a nonce or state mismatch, a network failure reaching Veri — is raised by your OIDC library, not by Veri. Catch the library's error types rather than matching on error strings, and treat a nonce or state mismatch as a security signal, not a retry.
9. Known limitations
Browser-only apps.
Veri requires a confidential client, which means a client_secret, and a secret in a web page is a secret anyone can read and use to impersonate your app. A purely static site therefore cannot integrate directly. Add one small serverless function to hold the secret and do the token exchange — the IETF's OAuth 2.0 for Browser-Based Applications recommends this pattern over public clients in the browser anyway.
Python's standard library.
Cloudflare returns 403 to any client whose User-Agent begins with Python-urllib, on every Veri OIDC endpoint. This breaks PyJWT's PyJWKClient, which fetches through urllib, and the failure is a bare HTTP Error 403: Forbidden with nothing pointing at the cause. Authlib uses httpx and mozilla-django-oidc uses requests, so both are unaffected — use one of those rather than the stdlib HTTP client.
Sessions are yours.
There are no refresh tokens and no logout endpoint. The ID token proves the sign-in; after that, keep the user signed in with your own session, and sign them out by ending it — the Express example in Section 4 shows a three-line /logout.
Mobile apps.
React Native and other mobile frameworks need a different pattern: they cannot hold a client secret either, and they need a custom URI scheme or app link as the redirect. Contact us before you start rather than adapting the web flow.
10. Live demo
Try a live demo
See the full Sign in with Veri flow in under 30 seconds. The demo site learns only your verified email domain — never your address.
11. Getting help
- Questions or problems: email team@getveri.co — let's solve problems now, not later.
- Register a client: use Get client credentials in Section 3. To change a redirect URI or replace a lost secret, email us your
client_id. - This page is the current version of the integration guide. If you were sent a copy, check it against this page.
Ready to integrate?
Get client credentials in about a minute, or download the guide as a PDF to share with your team.