AgentOnboard Docs
Partner GuideExample app

Example app: REST API integration

How the notes app authenticates agents — the x-session-token header, verifySessionToken, the TokenVerifyResult union, and the authFailureResponse HTTP mapping

Example app: REST API integration

Every route in the notes REST API authenticates the same way: it reads the agent's session token from the x-session-token header, verifies it with AgentOnboard, and maps any failure to an HTTP response through one shared function. This page documents that machinery against the real code in apps/web/src/lib/auth/.

The x-session-token header

Agents pass the session token in a request header:

GET /api/notes HTTP/1.1
Host: notes.aawej.in
x-session-token: <session-token>

The header name is exactly x-session-token (lowercase by convention). The token itself comes from aon token get and is valid for 5 minutes, so agents mint a fresh one for each interaction.

verifySessionToken(request)

The single entry point is verifySessionToken(), which every route handler calls before touching data. It does four things:

Read the header

If there is no x-session-token header, it fails immediately with MISSING_TOKEN — no network call is made.

Verify with AgentOnboard

It calls verify() from @agentonboard/sdk with the app's partner key, read from the AGENTONBOARD_PARTNER_KEY environment variable. If that variable is not set, it fails with SERVER_CONFIG_ERROR instead of making a doomed request.

Check the result

verify() returning { ok: false } (invalid/expired token) or failing to include an email both become INVALID_TOKEN failures. A thrown error — the API being unreachable, for example — is also surfaced as INVALID_TOKEN with a detail message.

Resolve the user

The verified email is looked up in the notes database via services.getUserByEmail(). If no local user has that email, the result is USER_NOT_FOUND. Otherwise the handler gets { userId, email } — everything it needs to scope queries to that person's data.

TokenVerifyResult

The function returns a discriminated union, so every failure is a typed code rather than a string:

Prop

Type

In TypeScript terms:

type TokenVerifyError =
  | { code: "MISSING_TOKEN" }
  | { code: "INVALID_TOKEN"; detail?: string }
  | { code: "USER_NOT_FOUND"; email: string }
  | { code: "SERVER_CONFIG_ERROR" };

type TokenVerifyResult =
  | { ok: true; auth: { userId: string; email: string } }
  | { ok: false; error: TokenVerifyError };

authFailureResponse(error) — error codes to HTTP

One function turns every TokenVerifyError into an HTTP response, so routes never duplicate the mapping:

Error codeHTTP statusResponse body
MISSING_TOKEN401{ "error": "Missing X-Session-Token header" }
INVALID_TOKEN401{ "error": "Invalid session token: <detail>" }
USER_NOT_FOUND401{ "error": "User not found — sign up at https://notes.aawej.in first", "email": "<email>" }
SERVER_CONFIG_ERROR500{ "error": "Server configuration error — contact support" }

Why USER_NOT_FOUND is 401, not 404

Returning 404 for an unknown email would let anyone probe which emails have accounts in the notes database. Treating it as an authentication failure keeps the endpoint uniform: no valid token, no access, same status.

SERVER_CONFIG_ERROR is a 500, not a 401

MISSING_TOKEN and INVALID_TOKEN mean the agent got it wrong. SERVER_CONFIG_ERROR means the notes app is misconfigured — the partner key is missing from the environment. That is a server fault, so it maps to 500. If you see it in the wild, set AGENTONBOARD_PARTNER_KEY and redeploy.

How routes use it

Every route follows the same shape — verify first, then act:

export async function GET(request: NextRequest) {
  const verified = await verifySessionToken(request);
  if (!verified.ok) {
    return authFailureResponse(verified.error);
  }

  // verified.auth.userId scopes every query to this user.
  const notes = await listNotesForUser(verified.auth.userId);
  return NextResponse.json({ notes });
}

The routes also set export const dynamic = "force-dynamic" so they always run on the server and never get statically cached with a stale token check.

Environment

The only AgentOnboard-related configuration is the partner key:

AGENTONBOARD_PARTNER_KEY=aon_partner_…  # your partner key, server-side only

It lives in the notes app's server environment and is never exposed to the client.

Next steps

On this page