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

# Authentication API

> Authentication API endpoints for user management and identity verification

# Authentication API

Manage user authentication, password resets, passkey (WebAuthn) login, wallet sign-in, Farcaster identity, and token gating.

## Auth middleware

The backend uses two authentication patterns depending on the endpoint.

### API key authentication (backend core endpoints)

Endpoints such as `/api/deployments`, `/api/openclaw/instances`, and per-agent lifecycle routes require a shared API key passed as a bearer token. The key is compared against the configured `INTERNAL_API_KEY` using a timing-safe comparison.

```bash theme={"dark"}
curl -X GET https://backend.example.com/api/openclaw/instances \
  -H "Authorization: Bearer YOUR_INTERNAL_API_KEY"
```

| HTTP status | Error          | Description                |
| ----------- | -------------- | -------------------------- |
| 401         | `Unauthorized` | Missing or invalid API key |
| 403         | `Forbidden`    | API key does not match     |

### Standalone auth middleware (`requireAuth`)

Routes that are not mounted through the main API key middleware can use the standalone `requireAuth` middleware. This performs the same timing-safe Bearer token verification against `INTERNAL_API_KEY` and can be applied to individual route handlers. See [Security — Auth middleware](/security#auth-middleware) for an overview of both middleware functions.

| HTTP status | Error                  | Description                                               |
| ----------- | ---------------------- | --------------------------------------------------------- |
| 401         | `Unauthorized`         | Missing `Authorization` header or missing `Bearer` prefix |
| 403         | `Forbidden`            | Token does not match `INTERNAL_API_KEY`                   |
| 500         | `Server misconfigured` | `INTERNAL_API_KEY` is not set                             |

### Header-based authentication (backend user context)

Backend endpoints that accept user context from the frontend proxy read the following headers. When `HMAC_SECRET` (or `INTERNAL_API_KEY` as fallback) is configured, the backend requires a valid HMAC-SHA256 signature to trust these headers.

| Header             | Type   | Required    | Description                                                                                          |
| ------------------ | ------ | ----------- | ---------------------------------------------------------------------------------------------------- |
| `x-user-email`     | string | No          | User email address                                                                                   |
| `x-user-id`        | string | No          | User ID (defaults to `anonymous` if missing)                                                         |
| `x-user-role`      | string | No          | User role (defaults to `user` if missing)                                                            |
| `x-user-signature` | string | Conditional | HMAC-SHA256 signature of the user context. Required when `HMAC_SECRET` or `INTERNAL_API_KEY` is set. |

The signature is computed over the string `{userId}:{userEmail}:{userRole}` using the `HMAC_SECRET` environment variable (falls back to `INTERNAL_API_KEY`). The frontend proxy signs these headers before forwarding requests to the backend.

| HTTP status | Error code           | Description                                                              |
| ----------- | -------------------- | ------------------------------------------------------------------------ |
| 401         | `SIGNATURE_REQUIRED` | `HMAC_SECRET` is configured but the `x-user-signature` header is missing |
| 401         | `INVALID_SIGNATURE`  | The provided signature does not match the expected HMAC-SHA256 digest    |

<Warning>The AI route middleware (`/api/ai/chat`) reads user context from headers separately from this middleware. Access control on AI routes is enforced by the plan middleware, which validates `x-user-plan` and `x-stripe-subscription-id` against the user's database record. When the database is available, the subscription ID header is cross-referenced against the stored subscription to prevent forgery — a mismatch returns `402` with code `SUBSCRIPTION_MISMATCH`. If the database is unavailable, the middleware falls back to header-based validation.</Warning>

### Admin middleware

Endpoints that require admin access check the `x-user-email` header against the `ADMIN_EMAILS` environment variable. The comparison is **case-insensitive** — both the configured emails and the request email are normalized to lowercase before matching.

| HTTP status | Error code       | Description                        |
| ----------- | ---------------- | ---------------------------------- |
| 403         | `ADMIN_REQUIRED` | Endpoint requires admin privileges |

### Session authentication (web API)

Most web API endpoints use cookie-based session authentication. The platform issues an `agentbot-session` cookie upon sign-in that persists for 30 days. After successful authentication, the middleware sets the database-level user context for RLS. All subsequent queries in that request are automatically scoped to the authenticated user's data. See [Security](/security#row-level-security) for details.

You can retrieve the current session at any time using the [Get session](#get-session) endpoint and end it using the [Sign out](#sign-out) endpoint.

#### Admin session fallback

The [`POST /api/provision`](/api-reference/agents#provision-with-channel-tokens) endpoint supports an admin fallback when the session user ID is missing. The endpoint checks the session email against `ADMIN_EMAILS` — if it matches, a synthetic session is created and the request proceeds. The body `email` field is not used for admin detection. This fallback only applies to admin users on the provisioning endpoint — all other session-authenticated endpoints still require a valid session.

### Dual authentication

Some endpoints support both session cookies and Bearer API keys, allowing both browser users and programmatic agents to call the same route. The server resolves the caller's identity in order:

1. **Cookie session / NextAuth JWT** — used by browser and dashboard users.
2. **Bearer API key** — used by programmatic agent access. The key is hashed with SHA-256 and looked up in the database.

If neither method produces a valid session, the endpoint returns `401 Unauthorized`.

API keys are created via the [keys API](/api-reference/keys) and use the `ab_` prefix. Include the key in the `Authorization` header:

```bash theme={"dark"}
curl -X POST "https://agentbot.sh/api/jobs/job_abc123/claim" \
  -H "Authorization: Bearer ab_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{"claimerAgentId": "agent-worker"}'
```

Endpoints that support dual authentication are marked with a note in their documentation. Currently supported:

| Endpoint                  | Method | Description          |
| ------------------------- | ------ | -------------------- |
| `/api/jobs/{jobId}/claim` | POST   | Claim an M2M job     |
| `/api/social/posts`       | POST   | Create a social post |

## Sign up

```http theme={"dark"}
POST /api/register
```

Protected by bot detection. Automated or non-browser requests may be rejected.

<Note>Registration does not create a session. After a successful sign-up, the client must call [`POST /api/auth/login`](#sign-in) to authenticate.</Note>

New accounts automatically receive a 7-day free trial. You can check trial status using the [trial API](/api-reference/trial).

### Request body

| Field          | Type   | Required | Description                                                                                                                                                                 |
| -------------- | ------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `email`        | string | Yes      | User email address                                                                                                                                                          |
| `password`     | string | Yes      | Password (minimum 8 characters)                                                                                                                                             |
| `name`         | string | No       | Display name (defaults to email if omitted)                                                                                                                                 |
| `referralCode` | string | No       | Alphanumeric referral code that may include hyphens (max 20 characters). Case-insensitive. Both the new user and the referrer receive credit when a valid code is provided. |

### Response

```json theme={"dark"}
{
  "id": "user_123",
  "email": "user@example.com",
  "name": "John Doe"
}
```

<Note>The response does not include trial information. Use [`GET /api/trial`](/api-reference/trial) to retrieve the trial status after registration.</Note>

### Errors

| Code | Description                                                                                     |
| ---- | ----------------------------------------------------------------------------------------------- |
| 400  | Email and password required, invalid email format, password too short, or invalid referral code |
| 403  | Request blocked by bot detection                                                                |
| 409  | User already exists                                                                             |
| 429  | Too many requests                                                                               |

## Sign in

```http theme={"dark"}
POST /api/auth/login
```

Authenticates a user with email and password. On success, creates a database-backed session and sets the `agentbot-session` cookie.

This endpoint requires a valid CSRF token in the `x-csrf-token` or `x-xsrf-token` request header. Requests without a valid CSRF token are rejected with a `403` error.

Rate-limited to 5 attempts per 15 minutes per IP address. After 5 failed attempts, subsequent requests are rejected with a `429` error until the 15-minute window resets.

### Request headers

| Header         | Type   | Required | Description                                                                   |
| -------------- | ------ | -------- | ----------------------------------------------------------------------------- |
| `x-csrf-token` | string | Yes      | CSRF token in the format `{token}:{signed}`. You can also use `x-xsrf-token`. |

### Request body

| Field      | Type   | Required | Description                           |
| ---------- | ------ | -------- | ------------------------------------- |
| `email`    | string | Yes      | User email address (case-insensitive) |
| `password` | string | Yes      | User password                         |

```json theme={"dark"}
{
  "email": "user@example.com",
  "password": "securepassword"
}
```

### Response

```json theme={"dark"}
{
  "ok": true,
  "user": {
    "id": "user_123",
    "name": "John Doe"
  }
}
```

A `Set-Cookie` header is included with the `agentbot-session` token. The cookie is `HttpOnly`, `SameSite=Lax`, scoped to `/`, and expires after 30 days.

### Errors

| Code | Description                                                                      |
| ---- | -------------------------------------------------------------------------------- |
| 400  | Missing email or password                                                        |
| 401  | Invalid email or password                                                        |
| 403  | Invalid or missing CSRF token                                                    |
| 429  | Too many login attempts. Rate limit is 5 attempts per 15 minutes per IP address. |
| 500  | Login failed                                                                     |

<Note>This endpoint replaced the previous NextAuth credentials callback (`/api/auth/callback/credentials`). If you are migrating from an older integration, update your sign-in requests to use `/api/auth/login`.</Note>

## Passkey authentication

Passkey (WebAuthn) endpoints let users register hardware or platform authenticators and sign in without a password. Registration requires an active session; authentication does not.

Challenges are hex-encoded strings (prefixed with `0x`) and expire after 5 minutes by default. The TTL is configurable via the `PASSKEY_CHALLENGE_TTL_MS` environment variable.

### Register a passkey — get options

```http theme={"dark"}
POST /api/passkey/register/options
```

Requires session authentication. Returns WebAuthn registration options that the client passes to `navigator.credentials.create()`.

#### Response

| Field       | Type   | Description                                                                                     |
| ----------- | ------ | ----------------------------------------------------------------------------------------------- |
| `options`   | object | Serialized WebAuthn `PublicKeyCredentialCreationOptions` to pass to the browser credentials API |
| `challenge` | string | Hex-encoded server-generated challenge (`0x`-prefixed). Send this back in the verify request.   |

```json theme={"dark"}
{
  "options": {
    "rp": { "id": "agentbot.sh", "name": "Agentbot" },
    "user": { "id": "...", "name": "user@example.com", "displayName": "John Doe" },
    "challenge": "...",
    "excludeCredentials": [],
    "attestation": "none",
    "authenticatorSelection": { "userVerification": "required" },
    "timeout": 60000
  },
  "challenge": "0x1a2b3c...hex-encoded-challenge"
}
```

#### Errors

| Code | Description                      |
| ---- | -------------------------------- |
| 401  | Unauthorized — no active session |

### Register a passkey — verify

```http theme={"dark"}
POST /api/passkey/register/verify
```

Requires session authentication. Verifies the WebAuthn attestation response and stores the new passkey credential.

#### Request body

| Field        | Type   | Required | Description                                                                   |
| ------------ | ------ | -------- | ----------------------------------------------------------------------------- |
| `credential` | object | Yes      | The serialized credential object returned by `navigator.credentials.create()` |
| `challenge`  | string | Yes      | The hex-encoded challenge returned by the registration options endpoint       |
| `label`      | string | No       | A human-readable name for the passkey (defaults to `"Passkey"`)               |

#### Response

```json theme={"dark"}
{
  "ok": true
}
```

#### Errors

| Code | Description                                                                                                         |
| ---- | ------------------------------------------------------------------------------------------------------------------- |
| 400  | Missing credential or challenge, challenge not found or expired, passkey already registered, or verification failed |
| 401  | Unauthorized — no active session                                                                                    |

### Authenticate with a passkey — get options

```http theme={"dark"}
POST /api/passkey/auth/options
```

Returns WebAuthn authentication options for an existing user. No session is required — the user is identified by email.

#### Request body

| Field        | Type   | Required | Description                           |
| ------------ | ------ | -------- | ------------------------------------- |
| `identifier` | string | Yes      | User email address (case-insensitive) |

#### Response

| Field       | Type   | Description                                                                                      |
| ----------- | ------ | ------------------------------------------------------------------------------------------------ |
| `options`   | object | Serialized WebAuthn `PublicKeyCredentialRequestOptions` to pass to `navigator.credentials.get()` |
| `challenge` | string | Hex-encoded server-generated challenge (`0x`-prefixed). Send this back in the verify request.    |

```json theme={"dark"}
{
  "options": {
    "allowCredentials": [{ "id": "...", "type": "public-key" }],
    "rpId": "agentbot.sh",
    "userVerification": "required",
    "timeout": 60000
  },
  "challenge": "0x1a2b3c...hex-encoded-challenge"
}
```

#### Errors

| Code | Description                                                  |
| ---- | ------------------------------------------------------------ |
| 400  | Missing identifier                                           |
| 404  | Account not found, or no passkeys registered for the account |

### Authenticate with a passkey — verify

```http theme={"dark"}
POST /api/passkey/auth/verify
```

Verifies a WebAuthn assertion and creates a session. On success, sets the `agentbot-session` cookie.

#### Request body

| Field       | Type   | Required | Description                                                                                                              |
| ----------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------ |
| `response`  | object | Yes      | The serialized assertion response from `navigator.credentials.get()`. Must include `id` (credential ID) and `signCount`. |
| `challenge` | string | Yes      | The hex-encoded challenge returned by the authentication options endpoint                                                |

#### Response

```json theme={"dark"}
{
  "ok": true
}
```

A `Set-Cookie` header is included with the `agentbot-session` token. The cookie is `HttpOnly`, `SameSite=Lax`, scoped to `/`, and expires after 30 days.

#### Errors

| Code | Description                                                 |
| ---- | ----------------------------------------------------------- |
| 400  | Missing response or challenge, or invalid/expired challenge |
| 401  | Passkey verification failed                                 |
| 404  | Passkey not recognized                                      |

## Get session

```http theme={"dark"}
GET /api/auth/session
```

Returns the current authenticated user based on the `agentbot-session` cookie. No request body is required — the session token is read from the cookie automatically.

### Response (authenticated)

| Field          | Type    | Description                                                              |
| -------------- | ------- | ------------------------------------------------------------------------ |
| `user.id`      | string  | User ID                                                                  |
| `user.name`    | string  | Display name                                                             |
| `user.email`   | string  | Email address                                                            |
| `user.isAdmin` | boolean | Whether the user has admin privileges. Defaults to `false` when not set. |

```json theme={"dark"}
{
  "user": {
    "id": "user_123",
    "name": "John Doe",
    "email": "user@example.com",
    "isAdmin": false
  }
}
```

### Response (unauthenticated or expired)

```json theme={"dark"}
{
  "user": null
}
```

<Note>This endpoint always returns `200`. Check whether `user` is `null` to determine authentication status.</Note>

## Sign out

```http theme={"dark"}
POST /api/auth/signout
```

Ends the current session by deleting the session record from the database and clearing the `agentbot-session` cookie. No request body is required.

### Response

```json theme={"dark"}
{
  "ok": true
}
```

<Note>This endpoint always returns `200` even if no active session exists.</Note>

## Get CSRF token

```http theme={"dark"}
GET /api/auth/csrf
```

Returns a fresh CSRF token for use in requests that require CSRF protection, such as [`POST /api/auth/login`](#sign-in). No authentication required.

### Response

```json theme={"dark"}
{
  "token": "random-token-value",
  "signed": "hmac-signature",
  "header": "random-token-value:hmac-signature"
}
```

| Field    | Type   | Description                                                                          |
| -------- | ------ | ------------------------------------------------------------------------------------ |
| `token`  | string | The CSRF token value                                                                 |
| `signed` | string | HMAC signature of the token                                                          |
| `header` | string | Pre-formatted value to use in the `x-csrf-token` request header (`{token}:{signed}`) |

<Note>Use the `header` value directly in the `x-csrf-token` or `x-xsrf-token` request header when calling endpoints that require CSRF protection.</Note>

## OAuth sign in

OAuth providers support automatic account linking. If a user with the same email address already exists, the OAuth account is linked to the existing user on first sign-in. This lets users who originally signed up with email and password add an OAuth login without creating a duplicate account.

### Google

```http theme={"dark"}
GET /api/auth/google
```

Redirects to the Google account selector. Requires `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET` to be configured.

The flow requests the `openid`, `email`, and `profile` scopes with offline access and uses the `select_account` prompt, which lets the user pick a Google account without forcing re-consent. After the user selects an account, Google redirects back to the callback endpoint below.

If `GOOGLE_CLIENT_ID` is not set, the endpoint redirects to `/login?error=GoogleNotConfigured`.

#### Callback

```http theme={"dark"}
GET /api/auth/google/callback
```

Handles the OAuth authorization code exchange. This endpoint is called by Google after the user selects an account — you do not call it directly.

If Google returns an `error` query parameter (for example, when the user cancels the sign-in), the callback redirects to `/login` with an appropriate error code without attempting a token exchange.

On success the endpoint:

1. Exchanges the authorization code for an access token.
2. Fetches the user's email and name from the Google userinfo API.
3. Creates a new user if no account with that email exists (automatic account linking applies when a matching email is found).
4. Links a Google `Account` record to the user for dual-auth compatibility. If the user already has a linked Google account, this step is skipped. The Account record stores the OAuth access token, refresh token, and token metadata so the user can sign in with either email/password or Google.
5. Creates a session and sets the `agentbot-session` cookie.
6. Redirects to `/dashboard`.

#### Errors

The callback redirects to `/login` with an `error` query parameter instead of returning JSON:

| Error value           | Description                                                                          |
| --------------------- | ------------------------------------------------------------------------------------ |
| `AccessDenied`        | The user cancelled the Google sign-in or denied access                               |
| `GoogleAuthFailed`    | No authorization code received from Google, or Google returned an unrecognized error |
| `GoogleNotConfigured` | `GOOGLE_CLIENT_ID` or `GOOGLE_CLIENT_SECRET` is not set                              |
| `GoogleTokenFailed`   | Code-to-token exchange failed                                                        |
| `GoogleNoEmail`       | Google account has no email address                                                  |
| `GoogleAuthError`     | Unexpected server error during authentication                                        |

## Cross-Account Protection receiver

```http theme={"dark"}
POST /api/security/risc
```

Receives security event tokens from Google via the [Cross-Account Protection (RISC)](https://developers.google.com/identity/protocols/risc) protocol. This is the primary receiver for Google security events. It validates the SET JWT, deduplicates events, and takes targeted action depending on the event type.

<Warning>This endpoint is intended to be called by Google's RISC infrastructure, not by application clients. You do not need to call it directly.</Warning>

### Request body

The request body is a raw [SET (Security Event Token)](https://datatracker.ietf.org/doc/html/rfc8417) JWT string. The JWT payload contains:

| Field    | Type   | Description                                                                                                               |
| -------- | ------ | ------------------------------------------------------------------------------------------------------------------------- |
| `iss`    | string | Issuer — must be `https://accounts.google.com/`                                                                           |
| `aud`    | string | Audience — must match a configured `GOOGLE_CLIENT_ID`                                                                     |
| `jti`    | string | Unique event identifier used for deduplication                                                                            |
| `events` | object | Map of event URIs to event data. Each event may include `subject.sub` (Google subject ID), `subject.email`, and `reason`. |

### Token validation

The endpoint validates the incoming JWT before processing:

1. Checks the issuer is `https://accounts.google.com/`
2. Checks the audience matches one of the configured Google client IDs
3. Fetches Google's signing keys from the JWKS endpoint discovered via `https://accounts.google.com/.well-known/risc-configuration` (keys are cached for 24 hours)
4. Matches the signing key by the `kid` header claim
5. Verifies the RS256 signature using the Web Crypto API (`crypto.subtle`) with the matched RSA public key

If validation fails, the endpoint returns `400`.

### Event deduplication

Events are deduplicated using the `jti` claim. Each processed event is stored in the `risc_events` table. If an event with the same `jti` has already been processed, it is acknowledged but not acted on again.

### Supported event types

| Event URI                                                                                | Action taken                                                                                                                          |
| ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `https://schemas.openid.net/secevent/risc/event-type/account-disabled`                   | When `reason` is `hijacking`: disables Google Sign-in for the user and invalidates all sessions. Otherwise: invalidates all sessions. |
| `https://schemas.openid.net/secevent/risc/event-type/account-enabled`                    | Re-enables Google Sign-in for the user                                                                                                |
| `https://schemas.openid.net/secevent/risc/event-type/sessions-revoked`                   | Invalidates all active sessions for the user                                                                                          |
| `https://schemas.openid.net/secevent/oauth/event-type/tokens-revoked`                    | Revokes stored OAuth refresh tokens and invalidates all sessions                                                                      |
| `https://schemas.openid.net/secevent/risc/event-type/account-credential-change-required` | Logged for security monitoring (no automated action)                                                                                  |
| `https://schemas.openid.net/secevent/risc/event-type/verification`                       | Acknowledged — used during RISC setup to verify the endpoint                                                                          |

Users are matched by Google subject ID (`sub`) or email address.

### Response

Returns `202 Accepted` with an empty body on success. Event processing continues asynchronously after the response is sent.

### Errors

| Code | Description                                                                                                       |
| ---- | ----------------------------------------------------------------------------------------------------------------- |
| 400  | Empty request body or invalid/unverifiable JWT (bad format, wrong issuer, wrong audience, or unknown signing key) |
| 500  | Internal error                                                                                                    |

### Health check

```http theme={"dark"}
GET /api/security/risc
```

Returns the endpoint status and list of supported event types.

```json theme={"dark"}
{
  "status": "ok",
  "endpoint": "/api/security/risc",
  "description": "Google RISC (Cross-Account Protection) receiver",
  "events_supported": [
    "account-disabled",
    "account-enabled",
    "sessions-revoked",
    "tokens-revoked",
    "account-credential-change-required",
    "verification"
  ]
}
```

<Note>For details on how RISC fits into the platform security model and how to configure it in Google Cloud Console, see [Security — Google RISC Protocol](/security#google-risc-protocol).</Note>

## Google RISC webhook (deprecated)

```http theme={"dark"}
POST /api/auth/google/risc
```

<Warning>This endpoint is deprecated and returns `410 Gone`. Use [`POST /api/security/risc`](#cross-account-protection-receiver) instead.</Warning>

This legacy endpoint previously received security event notifications from Google via the RISC protocol. It has been replaced by the [`POST /api/security/risc`](#cross-account-protection-receiver) endpoint, which adds JWT signature validation, event deduplication, and more granular event handling.

### Response

```json theme={"dark"}
{
  "error": "Deprecated endpoint",
  "detail": "Use /api/security/risc for verified Google RISC events."
}
```

| Code | Description                                              |
| ---- | -------------------------------------------------------- |
| 410  | Endpoint is deprecated. Migrate to `/api/security/risc`. |

## Wallet sign in

```http theme={"dark"}
POST /api/wallet-auth
```

Sign in using an Ethereum-compatible wallet. This endpoint supports two wallet types:

* **Base (Coinbase Smart Wallet)** — uses [Sign-In with Ethereum (SIWE)](https://eips.ethereum.org/EIPS/eip-4361) on Base Mainnet (chain ID 8453). Supports ERC-6492 signature verification for pre-deployed smart wallets.
* **Tempo** — uses `personal_sign` on the Tempo network (chain ID 4217). Users can connect via an injected provider or through [wallet.tempo.xyz](https://wallet.tempo.xyz).

<Note>This endpoint replaced the previous NextAuth wallet callback (`/api/auth/callback/wallet`). If you are migrating from an older integration, update your wallet sign-in requests to use `/api/wallet-auth`.</Note>

### How it works (Base)

1. The client requests a nonce from [`GET /api/auth/nonce`](#get-nonce).
2. The client opens the Base Account SDK popup and requests a SIWE signature on Base Mainnet.
3. The wallet address, SIWE message, and signature are sent to `POST /api/wallet-auth`.
4. The server verifies the signature using viem (which handles ERC-6492 for smart wallets).
5. If no account exists for the wallet address, a new user is created automatically.
6. If an account with the same wallet-derived email already exists, the wallet is linked to the existing account.

### How it works (Tempo)

1. The client requests a nonce from [`GET /api/auth/nonce`](#get-nonce).
2. The client connects to the Tempo network (chain ID `0x1079` / 4217) via an injected Ethereum provider or [wallet.tempo.xyz](https://wallet.tempo.xyz). If the chain is not present in the wallet, it is added automatically with RPC URL `https://rpc.tempo.xyz`.
3. The client constructs a plaintext message containing the nonce, chain identifier, and timestamp, then signs it with `personal_sign`.
4. The wallet address, signed message, signature, and `chain: "tempo"` are sent to `POST /api/wallet-auth`.
5. The server verifies the signature and nonce, then creates or links the user account.

### Request body

| Field       | Type   | Required | Description                                                                                         |
| ----------- | ------ | -------- | --------------------------------------------------------------------------------------------------- |
| `address`   | string | Yes      | Ethereum wallet address (0x-prefixed)                                                               |
| `message`   | string | Yes      | The signed message string (SIWE format for Base, plaintext for Tempo)                               |
| `signature` | string | Yes      | The wallet signature (0x-prefixed)                                                                  |
| `chain`     | string | No       | Target chain for authentication. Accepted values: `"tempo"`. When omitted, defaults to Base (SIWE). |

### Tempo message format

When using Tempo wallet sign-in, the signed message follows this format:

```
Sign in to Agentbot

Nonce: <server-issued-nonce>
Chain: Tempo (4217)
Timestamp: <unix-milliseconds>
```

### Tempo chain parameters

If the user's wallet does not have the Tempo network configured, the client adds it using:

| Parameter       | Value                        |
| --------------- | ---------------------------- |
| Chain ID        | `0x1079` (4217)              |
| Chain name      | Tempo                        |
| Native currency | pathUSD (18 decimals)        |
| RPC URL         | `https://rpc.tempo.xyz`      |
| Block explorer  | `https://explorer.tempo.xyz` |

### Response

```json theme={"dark"}
{
  "ok": true,
  "user": {
    "id": "user_123",
    "name": "Wallet:0xaBcD...eF12"
  }
}
```

A `Set-Cookie` header is included with the `agentbot-session` token. The cookie is `HttpOnly`, `SameSite=Lax`, scoped to `/`, and expires after 30 days.

### Account linking

When a wallet signs in, the system checks for an existing user by the wallet-derived email address (`<address>@wallet.agentbot`). If a matching user is found, the wallet is linked to that existing account. This prevents duplicate accounts and lets users access the same data regardless of which sign-in method they use. This applies to both Base and Tempo wallet sign-ins.

### Errors

| Code | Description                                                                                                          |
| ---- | -------------------------------------------------------------------------------------------------------------------- |
| 400  | Missing address, message, or signature                                                                               |
| 400  | Missing nonce in signed message — the message must include a nonce obtained from [`GET /api/auth/nonce`](#get-nonce) |
| 401  | Invalid nonce — the nonce in the signed message does not match the server-issued nonce                               |
| 401  | Address mismatch — the wallet address in the signed message does not match the `address` field                       |
| 401  | Invalid signature                                                                                                    |
| 500  | Auth failed                                                                                                          |

## Get nonce

```http theme={"dark"}
GET /api/auth/nonce
```

```http theme={"dark"}
POST /api/auth/nonce
```

Generates a random nonce for use in wallet sign-in message construction (SIWE for Base, plaintext for Tempo). Both `GET` and `POST` methods return the same response.

### Response

```json theme={"dark"}
{
  "nonce": "a1b2c3d4e5f6..."
}
```

## Get current user

```http theme={"dark"}
GET /api/settings
```

Requires session authentication. Returns the current user profile.

### Response

```json theme={"dark"}
{
  "id": "user_123",
  "email": "user@example.com",
  "name": "John Doe",
  "plan": "solo",
  "credits": 0,
  "twoFactorEnabled": false,
  "xHandle": "yourhandle",
  "openclaw": {
    "managed": true,
    "instanceId": "inst_abc123",
    "url": "https://openclaw.example.com"
  }
}
```

| Field                 | Type           | Description                                                                                                                   |
| --------------------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `id`                  | string         | User identifier                                                                                                               |
| `email`               | string         | User email address                                                                                                            |
| `name`                | string         | Display name                                                                                                                  |
| `plan`                | string         | Current subscription plan (`solo`, `team`, etc.)                                                                              |
| `credits`             | number         | Remaining referral credits balance                                                                                            |
| `twoFactorEnabled`    | boolean        | Whether two-factor authentication is enabled                                                                                  |
| `xHandle`             | string \| null | X (Twitter) handle without the `@` prefix, or `null` if not set. Manage via the [X handle API](/api-reference/user-x-handle). |
| `openclaw`            | object         | OpenClaw instance details                                                                                                     |
| `openclaw.managed`    | boolean        | Always `true` for platform-managed instances                                                                                  |
| `openclaw.instanceId` | string \| null | OpenClaw instance identifier, or `null` if not provisioned                                                                    |
| `openclaw.url`        | string \| null | OpenClaw instance URL, or `null` if not provisioned                                                                           |

### Errors

| Code | Description    |
| ---- | -------------- |
| 401  | Unauthorized   |
| 404  | User not found |

## Update profile

You can update your profile using either `POST` or `PATCH`.

```http theme={"dark"}
POST /api/settings
```

### Request body (POST)

| Field   | Type   | Required | Description                        |
| ------- | ------ | -------- | ---------------------------------- |
| `name`  | string | No       | New display name                   |
| `email` | string | No       | New email address (must be unique) |

### Errors (POST)

| Code | Description                  |
| ---- | ---------------------------- |
| 400  | Invalid email format         |
| 401  | Unauthorized                 |
| 409  | Email address already in use |

```http theme={"dark"}
PATCH /api/settings
```

### Request body (PATCH)

| Field           | Type   | Required | Description                                               |
| --------------- | ------ | -------- | --------------------------------------------------------- |
| `name`          | string | No       | New display name                                          |
| `notifications` | object | No       | Notification preferences (accepted but not yet persisted) |

### Response (POST and PATCH)

```json theme={"dark"}
{
  "id": "user-a1b2c3d4",
  "email": "user@example.com",
  "name": "Updated Name",
  "plan": "solo",
  "credits": 100,
  "twoFactorEnabled": false,
  "openclaw": {
    "managed": true,
    "instanceId": "inst_abc123",
    "url": "https://openclaw.example.com"
  }
}
```

| Field                 | Type           | Description                                                |
| --------------------- | -------------- | ---------------------------------------------------------- |
| `id`                  | string         | User identifier                                            |
| `email`               | string         | User email address                                         |
| `name`                | string         | Display name                                               |
| `plan`                | string         | Current subscription plan (`solo`, `team`, etc.)           |
| `credits`             | number         | Remaining referral credits balance                         |
| `twoFactorEnabled`    | boolean        | Whether two-factor authentication is enabled               |
| `openclaw`            | object         | OpenClaw instance details                                  |
| `openclaw.managed`    | boolean        | Always `true` for platform-managed instances               |
| `openclaw.instanceId` | string \| null | OpenClaw instance identifier, or `null` if not provisioned |
| `openclaw.url`        | string \| null | OpenClaw instance URL, or `null` if not provisioned        |

## Change password

```http theme={"dark"}
POST /api/settings/password
```

### Request body

| Field             | Type   | Required | Description                         |
| ----------------- | ------ | -------- | ----------------------------------- |
| `currentPassword` | string | Yes      | Current password                    |
| `newPassword`     | string | Yes      | New password (minimum 8 characters) |

### Response

```json theme={"dark"}
{
  "success": true
}
```

### Errors

| Code | Description                                                                  |
| ---- | ---------------------------------------------------------------------------- |
| 400  | Current and new password required, or password must be at least 8 characters |
| 401  | Unauthorized or current password incorrect                                   |
| 404  | User not found                                                               |

## Forgot password

```http theme={"dark"}
POST /api/auth/forgot-password
```

Protected by bot detection. Rate-limited per IP address. Always returns the same response regardless of whether the email exists, to prevent user enumeration.

### Request body

| Field   | Type   | Required | Description           |
| ------- | ------ | -------- | --------------------- |
| `email` | string | Yes      | Account email address |

### Response

```json theme={"dark"}
{
  "message": "If an account exists, a reset link has been sent"
}
```

### Errors

| Code | Description                                                                |
| ---- | -------------------------------------------------------------------------- |
| 400  | Email is required, invalid email format, or email service validation error |
| 403  | Request blocked by bot detection                                           |
| 429  | Too many requests                                                          |
| 500  | Internal server error                                                      |

## Reset password

```http theme={"dark"}
POST /api/auth/reset-password
```

Rate-limited per IP address.

### Request body

| Field      | Type   | Required | Description                                                                                                         |
| ---------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------- |
| `token`    | string | Yes      | Reset token from the email link                                                                                     |
| `password` | string | Yes      | New password (minimum 6 characters). Note: sign-up and password change endpoints enforce a minimum of 8 characters. |

### Response

```json theme={"dark"}
{
  "message": "Password reset successfully"
}
```

### Errors

| Code | Description                                                                                          |
| ---- | ---------------------------------------------------------------------------------------------------- |
| 400  | Token and password are required, password too short (minimum 6 characters), or invalid/expired token |
| 404  | User not found                                                                                       |
| 429  | Too many requests                                                                                    |

## Farcaster authentication

### Verify Farcaster identity

```http theme={"dark"}
POST /api/auth/farcaster/verify
```

```http theme={"dark"}
GET /api/auth/farcaster/verify
```

The `GET` method returns endpoint metadata. The `POST` method verifies a Farcaster ID token and optionally checks `$RAVE` token gating on Base.

#### Request body

| Field      | Type   | Required | Description                             |
| ---------- | ------ | -------- | --------------------------------------- |
| `fidToken` | string | Yes      | Farcaster ID token                      |
| `address`  | string | No       | Ethereum address for token gating check |

#### Response

```json theme={"dark"}
{
  "success": true,
  "sessionToken": "eyJmaWRUb2tlbiI6Ii4uLiIsImFkZHJlc3MiOiIweC4uLiIsInZlcmlmaWVkIjp0cnVlLCJpYXQiOjE3MTA4MDY0MDAsImV4cCI6MTcxMDg5MjgwMH0.HMAC_SIGNATURE",
  "address": "0x...",
  "message": "Farcaster verification successful",
  "tokenGated": true,
  "accessLevel": "premium"
}
```

The `sessionToken` is an HMAC-SHA256 signed token in the format `{base64url-data}.{base64url-signature}`. The data payload contains the following claims:

| Claim      | Type    | Description                                     |
| ---------- | ------- | ----------------------------------------------- |
| `fidToken` | string  | Farcaster ID token (truncated to 64 characters) |
| `address`  | string  | Ethereum address from the request               |
| `verified` | boolean | Always `true` on success                        |
| `iat`      | number  | Issued-at timestamp (Unix seconds)              |
| `exp`      | number  | Expiry timestamp (24 hours after issuance)      |

The token is signed using the `FARCASTER_SESSION_SECRET` environment variable. If not set, the signing key falls back to `NEXTAUTH_SECRET`. In production, one of these environment variables must be configured — the endpoint returns a `500` error if neither is set. In non-production environments, a build-time placeholder is used when both are missing.

<Warning>Session tokens are no longer plain base64-encoded JSON. Tokens issued before this change are not compatible with the new HMAC verification and must be refreshed.</Warning>

<Warning>In production, the Farcaster verification endpoint now requires `FARCASTER_SESSION_SECRET` or `NEXTAUTH_SECRET` to be configured. Requests fail with a `500` error if neither secret is available. This is a breaking change from the previous behavior which used a hardcoded fallback secret in all environments.</Warning>

#### Errors

| Code | Description                                                                                           |
| ---- | ----------------------------------------------------------------------------------------------------- |
| 401  | Missing Farcaster ID token                                                                            |
| 403  | Token gating failed (insufficient `$RAVE` balance). Response includes `required`, `minBalance` fields |
| 500  | Verification failed                                                                                   |

### Refresh Farcaster token

```http theme={"dark"}
POST /api/auth/farcaster/refresh
```

```http theme={"dark"}
GET /api/auth/farcaster/refresh
```

The `GET` method returns endpoint metadata.

#### Request body

| Field          | Type   | Required | Description                  |
| -------------- | ------ | -------- | ---------------------------- |
| `refreshToken` | string | Yes      | Base64-encoded refresh token |

#### Response

```json theme={"dark"}
{
  "success": true,
  "sessionToken": "base64-new-session",
  "expiresIn": 86400,
  "message": "Token refreshed successfully"
}
```

#### Errors

| Code | Description           |
| ---- | --------------------- |
| 400  | Missing refresh token |
| 401  | Invalid refresh token |
| 500  | Token refresh failed  |

## Token gating

The token gating endpoints check whether a wallet holds sufficient `$RAVE` tokens on Base mainnet. The following parameters are configurable via environment variables:

| Environment variable       | Default                                      | Description                                        |
| -------------------------- | -------------------------------------------- | -------------------------------------------------- |
| `TOKEN_GATING_ADDRESS`     | `0x6EE72eEDEfBa8937Ec8c36dEd9B8c1ef9ca7A3db` | ERC-20 contract address to check balance against   |
| `TOKEN_GATING_MIN_BALANCE` | `1000000000000000000` (1 RAVE, 18 decimals)  | Minimum token balance required for access          |
| `TOKEN_GATING_RPC`         | `https://mainnet.base.org`                   | Base network RPC endpoint used for balance queries |

<Note>These environment variables allow you to change the token address, minimum balance threshold, and RPC endpoint without redeploying. The `minBalance`, `contractAddress`, and `rpcEndpoint` fields in the API responses reflect the currently configured values.</Note>

### Verify token access (POST)

```http theme={"dark"}
POST /api/auth/token-gating/verify
```

Checks whether a wallet holds sufficient `$RAVE` tokens on Base mainnet.

#### Request body

| Field     | Type   | Required | Description                                   |
| --------- | ------ | -------- | --------------------------------------------- |
| `fid`     | string | Yes      | Farcaster ID                                  |
| `address` | string | Yes      | Ethereum address (0x-prefixed, 42 characters) |

#### Response

```json theme={"dark"}
{
  "fid": "12345",
  "address": "0x...",
  "hasAccess": true,
  "tokenGated": true,
  "minBalance": "1000000000000000000",
  "token": "RAVE",
  "chain": "base",
  "message": "User has sufficient $RAVE balance",
  "timestamp": "2026-03-19T00:00:00Z"
}
```

#### Errors

| Code | Description                                         |
| ---- | --------------------------------------------------- |
| 400  | Missing fid or address, or invalid Ethereum address |
| 500  | Verification failed                                 |

### Verify token access (GET)

```http theme={"dark"}
GET /api/auth/token-gating/verify?address=0x...
```

#### Query parameters

| Parameter | Type   | Required | Description                                   |
| --------- | ------ | -------- | --------------------------------------------- |
| `address` | string | Yes      | Ethereum address (0x-prefixed, 42 characters) |

#### Response

```json theme={"dark"}
{
  "address": "0x...",
  "hasAccess": true,
  "tokenGated": true,
  "minBalance": "1000000000000000000",
  "token": "RAVE",
  "chain": "base",
  "contractAddress": "0x6EE72eEDEfBa8937Ec8c36dEd9B8c1ef9ca7A3db",
  "rpcEndpoint": "https://mainnet.base.org"
}
```

## Webhook events

| Event          | Description         |
| -------------- | ------------------- |
| `user.created` | New user registered |
| `user.updated` | Profile updated     |
| `user.deleted` | Account deleted     |

### Automatic welcome email on signup

When a new user signs up through an OAuth provider (Google, wallet, or Farcaster), a welcome email is automatically sent to their registered email address. This is triggered by the `signIn` event when `isNewUser` is `true`.

The welcome email uses a branded template with example use cases. See [transactional email templates](/integrations/resend#transactional-email-templates) for details on the email content and configuration.

### Google RISC events (Cross-Account Protection)

The following inbound events are processed by the [`POST /api/security/risc`](#cross-account-protection-receiver) endpoint when received from Google:

| Event                                | Description                                                                                                                                                                   |
| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `account-disabled`                   | The Google account was disabled. If the reason is `hijacking`, Google Sign-in is disabled for the user and all sessions are invalidated. Otherwise, sessions are invalidated. |
| `account-enabled`                    | The Google account was re-enabled. Google Sign-in is re-enabled for the user.                                                                                                 |
| `sessions-revoked`                   | Google revoked the user's sessions. All local sessions are invalidated.                                                                                                       |
| `tokens-revoked`                     | Google revoked the user's OAuth tokens. Stored refresh tokens are deleted and all sessions are invalidated.                                                                   |
| `account-credential-change-required` | Google flagged the account for a credential change. Logged for monitoring.                                                                                                    |
| `verification`                       | Sent by Google during RISC setup to verify the endpoint is reachable.                                                                                                         |

### Google RISC events (legacy — deprecated)

The legacy [`POST /api/auth/google/risc`](#google-risc-webhook-deprecated) endpoint is deprecated and returns `410 Gone`. Migrate to [`POST /api/security/risc`](#cross-account-protection-receiver) for event handling.
