Add AgentID to Better Auth
Put a Continue with AgentMail button in your app, so AI agents can sign in to it. Assumes a running Better Auth app; every step is copy-paste.
On this page
01Register a client in the console
Open the AgentID console and register a client with your app name and redirect URI. AgentID generates the client id and uses client_secret_basic automatically. Scope selection happens in the Better Auth configuration in step 3.
The redirect URI is an address on your own app — your origin, plus Better Auth’s fixed callback path:
https://yourapp.com/api/auth/oauth2/callback/agentidThe trailing agentid must match the providerId in step 3.
02Copy the secret while it is on screen
Copy the client_id and client_secret before closing the dialog. The secret is shown only once.

Watch out. You now have a client id and a client secret — your app’s username and password. Step 3 needs both.
03Point it at AgentID
This is the whole integration. Add the plugin to the auth config you already have, and keep the comments — each marked line costs you a working sign-in if you drop it. No new tables: the plugin uses the ones your app already migrated.
AGENTID_CLIENT_ID=<from step 2>
AGENTID_CLIENT_SECRET=<from step 2>import { betterAuth } from 'better-auth'
import { genericOAuth } from 'better-auth/plugins'
export const auth = betterAuth({
// ...your existing options (database, plugins, …)
onAPIError: { errorURL: '/sign-in-failed' }, // REQUIRED to see AgentID's errors
plugins: [
genericOAuth({
config: [
{
providerId: 'agentid', // same name as the redirect URI's last part
discoveryUrl: 'https://auth.agentid.com/.well-known/openid-configuration',
clientId: process.env.AGENTID_CLIENT_ID!,
clientSecret: process.env.AGENTID_CLIENT_SECRET!,
scopes: ['openid', 'email', 'profile'],
pkce: true, // REQUIRED: this connector does not bind the browser session with an OIDC nonce
authentication: 'basic', // REQUIRED; matches what you registered
// REQUIRED; agents with no display name cannot sign in without it
mapProfileToUser: (p) => ({ name: p.name ?? p.email.split('@')[0] }),
},
],
}),
],
})This base configuration identifies the agent without requesting information about its human owner.
New to Better Auth? Its installation guide covers the app, the database and the migration; come back here once it runs.
04Add the client plugin and the button
Your existing /api/auth handler already serves the callback path from step 1 — there is no route to write. Add the client half of the plugin, and a button:
import { createAuthClient } from 'better-auth/react'
import { genericOAuthClient } from 'better-auth/client/plugins'
export const authClient = createAuthClient({
plugins: [genericOAuthClient()], // alongside any plugins you already have
})await authClient.signIn.oauth2({ providerId: 'agentid', callbackURL: '/' })05Sign in, and let the agent approve
Open your app and click the button. You land on a page showing a request id, and it waits.

The waiting is the point. Give that id to your agent. It signs the id and sends it back, and the browser returns to your app signed in, with session.user.email set to the agent’s inbox address. What the agent does is at https://auth.agentid.com/docs/approve, linked from the waiting page too.
Watch out. If it fails, look in two places. AgentID’s own errors land on the page you set as onAPIError.errorURL, as error and error_description. Later failures show only oauth_code_verification_failed in the browser, and the real message is in the terminal running npm run dev.
06Optional: ask who owns the agent
If your app needs the human behind the agent, add owner_profile and owner_email to the scopes array above. The agent’s signing credential must permit them. The owner claims do not arrive in the id_token, so session.user will not have them; read them separately from /userinfo:
import { headers } from 'next/headers'
import { auth } from '@/lib/auth'
export async function GET() {
const { accessToken } = await auth.api.getAccessToken({
body: { providerId: 'agentid' },
headers: await headers(), // needed; it finds the token through the session
})
const res = await fetch('https://auth.agentid.com/v0/userinfo', {
headers: { Authorization: 'Bearer ' + accessToken },
})
return Response.json(await res.json())
}{
"sub": "…", "org": "…",
"email": "agent-inbox@yourdomain", "email_verified": true,
"name": "…",
"owner_name": "…", "owner_email": "…"
}Endpoints, scopes and token claims are on the integration reference.