> ## Documentation Index
> Fetch the complete documentation index at: https://docs.versine.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Add “Are you an agent?”

> Let people's AI agents sign in and create accounts in your product.

Versine is an OpenID Connect (OIDC) provider. Connect it to the auth you
already have, like any other sign-in option. Agents sign in through it like any
other user, and your app keeps control of accounts, sessions and permissions.

The agent's assistant vouches for who its user is.
Versine verifies the assistant and sends your auth provider a standard ID
token. See [How Versine works](https://docs.versine.com/docs/how-it-works.md).

## 1. Register the project

Sign in to Console and choose **Create project**. Have these ready:

> [tab] Auth0
>
> * **Auth0 domain:** your tenant or custom domain, such as `https://example.us.auth0.com`.
> * **Connection name:** such as `versine`. You'll use the same name in Auth0.
> * **Callback URL:** that domain followed by `/login/callback`, exactly.

> [tab] Supabase
>
> * **Supabase project URL:** the Project URL in **Project Settings → Data API**, without any path such as `/rest/v1/`: for example `https://abcdefgh.supabase.co`. If you use a custom domain (**Project Settings → Custom Domains**), use that instead.
> * **Provider identifier:** a name you choose, of lowercase letters, numbers and hyphens, such as `versine`. Supabase calls the provider `custom:versine`.
> * **Callback URL:** that URL followed by `/auth/v1/callback`, exactly. Supabase also shows it as the **Callback URL** when you add the provider under **Authentication → Sign In / Providers**.

> [tab] Other
>
> * **Auth provider:** choose **Other**.
> * **Callback URL:** the exact redirect URI that receives Versine's response. If your auth provider handles sign-in, it shows this when you add an OIDC identity provider. If your own server does, it's your callback route, such as `https://example.com/auth/versine/callback`.

Continuing past **Integration** saves a draft and creates your credentials. Drafts can't sign anyone in; choose **Finish setup** after steps 2–5.

```bash theme={null}
VERSINE_CLIENT_ID=...
VERSINE_CLIENT_SECRET=...    # your auth provider and server; never the browser
```

> [accordion] Set up with a coding agent
>
> Choose **Use my AI assistant** when creating the project. Console copies a
> prompt with the full integration guide and your project's non-secret
> settings. Paste it into your coding agent with your repository open. Enter
> secrets into your auth provider and secret manager yourself.

> [accordion] Terms of Service and Privacy Policy
>
> Under **Advanced**, optionally add your Terms of Service and Privacy Policy
> URLs. Versine gives them to assistants, which decide how to show them to
> their users. Versine doesn't record acceptance: if you need a record, ask
> for it in your product after sign-in. Leave both empty to skip this.

## 2. Connect your auth provider

> [tab] Auth0
>
> In Auth0, open **Authentication → Enterprise → OpenID Connect** and create a connection:
>
> * **Name:** the connection name from step 1.
> * **Discovery URL:** `https://auth.versine.com/.well-known/openid-configuration`.
> * **Client ID and secret:** from your Versine project, not your Auth0 application.
> * **Type:** Back Channel, with `client_secret_basic` authentication.
> * **Scopes:** `openid profile email phone`.
> * **PKCE:** S256.
>
> Confirm the callback Auth0 shows matches the one in your project. Then enable the connection for your application on the **Applications** tab.
>
> > [accordion] Security profiles
> >
> > Keep **S256 PKCE**, the default, unless Auth0 can't use it on this connection.
> > Choose **Nonce** only if Auth0 sends and validates a fresh nonce and `state`
> > to Versine instead. PKCE between your app and Auth0 does not protect the
> > separate connection from Auth0 to Versine.

> [tab] Supabase
>
> Add Versine as a [custom OIDC provider](https://supabase.com/docs/guides/auth/custom-oauth-providers). From your server, with your service-role key:
>
> ```ts theme={null}
> await supabase.auth.admin.customProviders.createProvider({
>   provider_type: "oidc",
>   identifier: "custom:versine", // your provider identifier from step 1
>   name: "Versine",
>   issuer: "https://auth.versine.com",
>   client_id: process.env.VERSINE_CLIENT_ID,
>   client_secret: process.env.VERSINE_CLIENT_SECRET,
>   scopes: ["openid", "profile", "email", "phone"],
>   pkce_enabled: true,
>   email_optional: true, // users of text-message assistants may have no email
>   custom_claims_allowlist: [
>     "https://versine.com/claims/provider",
>     "https://versine.com/claims/assistant",
>     "https://versine.com/claims/email_verification",
>     "https://versine.com/claims/phone_verification",
>     "phone_number",
>     "phone_number_verified",
>   ],
> });
> ```
>
> You can also create it in the Supabase dashboard; check that PKCE, `email_optional` and the claims allowlist match. Confirm the callback Supabase shows matches the one in your project. Supabase always uses S256 PKCE, so Versine projects for Supabase use the PKCE profile.

> [tab] Other
>
> Add Versine wherever your auth provider configures an OIDC identity provider, or to your server's OIDC client library:
>
> * **Issuer:** `https://auth.versine.com`. Discovery is at `https://auth.versine.com/.well-known/openid-configuration`.
> * **Client ID and secret:** from your Versine project, sent with `client_secret_basic`.
> * **Flow:** authorization code, with S256 PKCE and `state`.
> * **Scopes:** `openid profile email phone`.
>
> If you handle the response yourself, use a maintained OIDC library rather than parsing tokens by hand. It must check the ID token's signature against Versine's JWKS, and that `iss` is `https://auth.versine.com`, `aud` is your client ID and the token hasn't expired.
>
> > [accordion] Security profiles
> >
> > Keep **S256 PKCE**, the default. Choose **Nonce** only if your provider
> > can't send PKCE to Versine, and sends and validates a fresh nonce and
> > `state` instead.

## 3. Add the sign-in button

Add **Are you an agent?** to both your login and sign-up pages. Keep your existing sign-in options.

> [tab] Auth0
>
> Point the button at Auth0's login with your Versine connection:
>
> ```tsx theme={null}
> import { useAuth0 } from "@auth0/auth0-react";
>
> const { loginWithRedirect } = useAuth0();
>
> <button
>   onClick={() =>
>     loginWithRedirect({ authorizationParams: { connection: "versine" } })
>   }
> >
>   Are you an agent?
> </button>;
> ```
>
> If you use one of Auth0's server-side SDKs instead, such as `@auth0/nextjs-auth0`, pass `connection=versine` as an authorization parameter to its login route. Let Auth0's SDK handle `state`, nonce and PKCE.

> [tab] Supabase
>
> Start Supabase's OAuth sign-in with your Versine provider, returning to a callback route on your server:
>
> ```tsx theme={null}
> <button
>   onClick={() =>
>     supabase.auth.signInWithOAuth({
>       provider: "custom:versine",
>       options: { redirectTo: `${location.origin}/auth/callback` },
>     })
>   }
> >
>   Are you an agent?
> </button>
> ```
>
> Use Supabase's server-side (PKCE) flow so your server exchanges the code in step 4.

> [tab] Other
>
> With a hosted auth provider, start its sign-in with the Versine identity provider, as you would for any other social or enterprise connection.
>
> On your own server, start the flow and handle the callback with an OIDC library. For example, with [`openid-client`](https://github.com/panva/openid-client) for Node.js:
>
> ```ts theme={null}
> import * as oidc from "openid-client";
>
> const config = await oidc.discovery(
>   new URL("https://auth.versine.com"),
>   process.env.VERSINE_CLIENT_ID!,
>   undefined,
>   oidc.ClientSecretBasic(process.env.VERSINE_CLIENT_SECRET!),
> );
> const redirectUri = "https://example.com/auth/versine/callback";
>
> // The "Are you an agent?" button links here.
> app.get("/auth/versine", async (req, res) => {
>   const verifier = oidc.randomPKCECodeVerifier();
>   const state = oidc.randomState();
>   req.session.versine = { verifier, state };
>   const url = oidc.buildAuthorizationUrl(config, {
>     redirect_uri: redirectUri,
>     scope: "openid profile email phone",
>     code_challenge: await oidc.calculatePKCECodeChallenge(verifier),
>     code_challenge_method: "S256",
>     state,
>   });
>   res.redirect(url.href);
> });
>
> app.get("/auth/versine/callback", async (req, res) => {
>   const { verifier, state } = req.session.versine;
>   const tokens = await oidc.authorizationCodeGrant(
>     config,
>     new URL(req.originalUrl, "https://example.com"),
>     { pkceCodeVerifier: verifier, expectedState: state },
>   );
>   const claims = tokens.claims()!; // verified ID token claims
>   // Find or create the account from claims.sub, email and phone: step 4.
> });
> ```
>
> ```tsx theme={null}
> <a href="/auth/versine">Are you an agent?</a>
> ```
>
> Keep `state` and the PKCE verifier in the browser's own session. They're what stops a sign-in from completing in a different browser than the one that started it.

## 4. Connect user accounts

Versine sends:

```json theme={null}
{
  "sub": "Jx3kP0…",
  "email": "ada@example.com",
  "email_verified": true,
  "given_name": "Ada",
  "family_name": "Lovelace",
  "https://versine.com/claims/provider": "versine",
  "https://versine.com/claims/assistant": "example-assistant",
  "https://versine.com/claims/email_verification": {
    "method": "provider_sso",
    "provider": "google",
    "verified_at": 1790000000
  },
  "phone_number": "+14155550123",
  "phone_number_verified": true,
  "https://versine.com/claims/phone_verification": {
    "method": "messaging",
    "channel": "whatsapp",
    "verified_at": 1790500000
  }
}
```

* `sub` is stable for one person, through one assistant, on your platform.
  The same person signing in through a different assistant has a different
  `sub`.
* `email_verified` is `true` only if the assistant proved the person controls
  the mailbox in the last year. `email_verification` says how and when:
  `provider_sso` (signed in through Google, Apple or Microsoft) or `otp` (a
  one-time code or link).
* `phone_number_verified` is `true` only if the assistant proved the person
  has the number in the last 30 days. `phone_verification` says how and when:
  `messaging` (the person messages the assistant from that number) or `otp`.
* A sign-in can have an email, a phone number or both. People who use an
  assistant only by text message may have no email.

Match each sign-in to an account like this:

| Sign-in | What to do |
| - | - |
| A `sub` you've seen before | The same account, even if the email or number changed |
| A new email or phone number | Create an account |
| An existing account's email or number, verified | Link to that account |
| An existing account's email or number, not verified | Send a code to that email or number, and link only after the person uses it. This happens once per account |

> [!WARNING]
>
> Never link on an unverified email or number. Both can change hands: a former
> employee's work email, a domain that expired and was bought, or a phone number
> the carrier gave to someone new. Work addresses can change hands within the
> year, so you may want a stricter rule for them. Phone numbers can also be
> taken over by SIM swapping: if you already treat SMS as a weak factor, require
> a second check before a phone-only match reaches payment or admin rights.

> [tab] Auth0
>
> Auth0 creates a separate user for each connection, with an ID such as
> `oidc|versine|<sub>`. Identify users by that ID. To reach an existing
> account, link the users in a post-login Action keyed to the Versine
> connection, following the table above. For an unverified email, redirect
> to a step where the person verifies it first. See
> [Auth0 account linking](https://auth0.com/docs/manage-users/user-accounts/user-account-linking).
>
> Auth0 doesn't store the phone number by default. In the connection's
> attribute mapping, map `phone_number` and `phone_number_verified` from the
> ID token. Add them to your app's tokens in the post-login Action if your app
> needs them there.

> [tab] Supabase
>
> Identify users by their Supabase user ID. The Versine subject is the identity's `provider_id` in `auth.identities`, for provider `custom:versine`.
>
> Supabase follows the table on its own. It automatically links a sign-in to an existing user with the same verified email, and this can't be turned off. With **Confirm email** on, it refuses a sign-in with an unverified email (`provider_email_needs_verification`) and emails a confirmation link. Keep it on: that's your verification step. After the person confirms, the agent signs in again. Don't add your own email-based linking on top. See [identity linking](https://supabase.com/docs/guides/auth/auth-identity-linking).
>
> Supabase doesn't copy the phone number to the user's `phone`, and never links accounts by phone. With `phone_number` in the claims allowlist from step 2, read it on your server from the identity's `identity_data.custom_claims` in `auth.identities`, through the admin API or a custom access token hook. Don't read it from `user_metadata`, which the user can edit. Match existing accounts by phone yourself, following the table.

> [tab] Other
>
> Identify Versine users by the pair (`iss`, `sub`), never by email. Store it against your user when you first create or link the account.
>
> If your auth provider links accounts by email automatically, check that it links only when `email_verified` is `true`, and that you can ask for proof otherwise. If it links unverified emails, turn automatic linking off for Versine and follow the table yourself.
>
> Many providers store `phone_number` only once you map it, in the identity provider's attribute or claim mapping. Map `phone_number_verified` too: providers copy it but don't act on it, so check it in your own code. Some providers can't create a user without an email; on those, people who use their assistant only by text message can't sign up.

> [accordion] Identify agent sign-ins
>
> Versine adds `https://versine.com/claims/provider: "versine"` and the
> assistant's ID to its ID token and userinfo. Many auth providers,
> including Auth0 and Supabase, don't forward these claims into your app's
> tokens.
>
> > [tab] Auth0
> >
> > Set the source in a post-login Action keyed to the Versine connection:
> >
> > ```js theme={null}
> > exports.onExecutePostLogin = async (event, api) => {
> >   if (event.connection.id !== "con_...") return; // your Versine connection ID
> >   const claim = "https://versine.com/claims/provider";
> >   api.idToken.setCustomClaim(claim, "versine");
> >   api.accessToken.setCustomClaim(claim, "versine");
> > };
> > ```
> >
> > To use the assistant ID in your app, map the upstream claim through the
> > connection's attribute mapping and a reviewed Action.
>
> > [tab] Supabase
> >
> > Supabase's session doesn't record which provider signed in, and
> > `user_metadata` can be edited by the user. In your callback, exchange
> > the code, then ask Versine who signed in using the session's
> > `provider_token`:
> >
> > ```ts theme={null}
> > const { data } = await supabase.auth.exchangeCodeForSession(code);
> > const res = await fetch("https://auth.versine.com/userinfo", {
> >   headers: { Authorization: `Bearer ${data.session.provider_token}` },
> > });
> > // 200: this sign-in came through Versine. Any other provider's token fails.
> > const versine = res.ok ? await res.json() : null;
> > ```
> >
> > Save the result, including the assistant, on your server against the
> > session's `session_id` claim. Don't store the `provider_token`; it
> > expires in five minutes.
>
> > [tab] Other
> >
> > If you handle the callback yourself, the claims are in the verified ID
> > token. Save the source and assistant in your server session when it
> > starts. If your auth provider drops custom claims, map them through its
> > claim or attribute mapping, or call `https://auth.versine.com/userinfo`
> > from your server with Versine's access token.
>
> Treat this as where the sign-in came from, not as a role or permission.

> [accordion] Decide what agents can do
>
> Agree which reads, writes and resources agents may use, and which actions
> need a person, such as billing, admin and deletion. Enforce it on your
> backend:
>
> * Allow the user's own rights, limited to your agent allowlist.
> * Keep the sign-in source in your server session, not in client parameters.
> * Treat a missing source as an agent, not a person.
> * Keep restrictions through refresh, silent sign-in and account linking.

## 5. Choose assistants

In Console, open **Configuration → Assistants**. By default your project
accepts every assistant Versine has reviewed, including ones added later.
Choose **Only selected assistants** to accept a fixed list instead.

An agent from an assistant you don't accept, or one that doesn't work with
Versine, sees a page saying agent sign-in isn't available. It can't sign in
through Versine.

> [accordion] Allow code sign-in
>
> Some agents drive a browser that can't add the sign-in header, such as an
> agent controlling a person's desktop. With **Advanced → Allow code sign-in**
> on, Versine's page asks these agents for a code from their assistant, which
> they type in. Each code works once, on one sign-in page, for two minutes.
>
> It's off by default because the code passes through the agent itself. An
> agent tricked into revealing its code could let someone else sign in as its
> user. Turn it on only if you need these agents, and keep agent permissions
> tight.

## 6. Test sign-in

In Console, open **Test sign-in** and enter your app's login page. Versine
opens it in a browser that acts as a test assistant, clicks **Are you an
agent?** and signs in as the test person you choose:

* **New person:** confirm an account is created. Test again and confirm the same account is used.
* **Existing email, verified:** confirm the sign-in reaches that account.
* **Existing email, not verified:** confirm your app asks for proof before linking, and creates no duplicate.
* **Phone number only:** confirm your app creates or finds an account with no email.

Then:

* Ask an agent from a supported assistant to sign in, and confirm it reaches a protected page.
* Sign out and confirm access is denied.

> [accordion] Troubleshoot a failed sign-in
>
> Check these first:
>
> * `invalid_redirect_uri`: match the project's callback to your provider's exactly, with no query.
> * `invalid_client`: use your Versine project's client ID and secret in your provider, with `client_secret_basic`.
> * `invalid_request`: check S256 PKCE, or `state` and nonce for the nonce profile.
> * The agent sees **Agent sign-in isn't available**: its assistant isn't in your accepted list or doesn't work with Versine.
> * The agent is asked for a sign-in code you didn't expect: its browser didn't send a sign-in token and code sign-in is on. Turn it off if you don't need it.
> * Supabase returns `provider_email_needs_verification`: the email isn't verified. The person confirms it from Supabase's email, then the agent signs in again.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.