# Authentication modes

_Three documented modes — API key, x402 wallet, wallet JWT — plus a same-origin cookie fallback for browser sessions._

_Last updated: 2026-05-07_

---

# Authentication modes

There are three documented authentication modes, plus a fourth de facto mode for browser sessions on the customer dashboard. All four resolve to the same Relaystation customer record and bill against the same wallet balance; only the auth shape on the wire differs.

## Mode 1 — API key (Stripe-funded wallet)

```http
Authorization: Bearer vf_live_<32-hex>
```

Mint after OAuth signup at `app.verafirma.com`. Top up via Stripe; calls debit per envelope. Best for developers building products on top of the API: one credential, dashboard, server-side bearer auth.

Test keys (`vf_test_*`) work identically against the dev environment.

## Mode 2 — x402 wallet (per-call)

```http
PAYMENT-SIGNATURE: <base64 PaymentPayload>
```

The lodestone path. The wallet signs an EIP-3009 `transferWithAuthorization` off-chain, the facilitator settles synchronously, the wrapper returns the result. No account, no signup, no dashboard. The wallet is the identity for any later history queries.

See [`/concepts/x402`](/concepts/x402) for the wire format and [`/concepts/lodestone`](/concepts/lodestone) for the design rationale.

## Mode 3 — Wallet JWT (returning visits, dashboard reads)

For wallet customers who want to read their own history without re-signing per request, exchange an EIP-191 wallet signature for a short-lived JWT.

```sh
# Step 1: get a one-shot challenge.
curl "https://api.verafirma.com/v1/auth/challenge?wallet=0x..."
# → [component]

# Step 2: sign the message with the wallet (EIP-191 personal_sign).
# Step 3: submit the signature.
curl -X POST https://api.verafirma.com/v1/auth/verify \
  -d '[component]'
# → [component]

# Step 4: use the JWT for read paths.
curl https://api.verafirma.com/v1/account \
  -H "Authorization: Bearer <JWT>"
```

JWT is HS256, signed by the API. The claims describe the wallet customer: address, customer id, expiry. Tokens are stateless; no server-side session store. Wallet-JWT-bound routes include `/v1/account/*` reads and `/v1/account/summary` (which IS wallet-JWT-only and rejects API key auth in-handler).

This is the wallet-as-identity mode. Note that **mode 2 (x402) and mode 3 (wallet JWT) use different signing primitives** — EIP-3009 for the per-call payment authorization, EIP-191 for the wallet-JWT signin — but they bind to the same wallet address, so a wallet that paid via x402 can later sign in via the JWT path and see its own envelope history.

## Mode 3b — Same-origin session cookie (browser dashboard)

The customer dashboard at `app.verafirma.com` uses a fourth de facto mode: an HttpOnly `vf_session` cookie set after OAuth signin, scoped to the dashboard origin. From the dashboard's browser context, calls to `/v1/*` ride the cookie and resolve to the same Stripe-funded wallet customer mode 1 uses.

This isn't a separate billing primitive — it's a transport convenience for human dashboard users so they don't have to copy-paste their API key into a browser fetch. Per the [D33 architectural decision](https://github.com/anthropics/...), the cookie is HttpOnly + Secure + SameSite=Lax + scoped to the dashboard origin; it can't be carried cross-site by a malicious page (SameSite=Lax blocks the cookie on cross-site sub-resources).

For non-browser callers, ignore the cookie path. Use mode 1, 2, or 3.

## Choosing a mode

| Caller | Mode | Why |
|---|---|---|
| Autonomous AI agent, one shot | x402 (mode 2) | No account, no signup, single call carries everything |
| Autonomous AI agent, returning | x402 + wallet JWT | x402 for billable creates, JWT for history reads |
| Developer integrating from a server | API key (mode 1) | Bearer auth, single key, balance-funded |
| Developer or end-user browsing the dashboard | Cookie (mode 3b) | Set automatically after OAuth signin |
| Wallet user reading history without paying | Wallet JWT (mode 3) | Signin once per hour; no per-request signature |

Server-to-server calls between Verafirma-family products use a different shape entirely (`X-S2S-Key` header, free internal hops); that's not a customer-facing mode.

## What's NOT a separate mode

- **OAuth signin** is not an auth mode for API requests. It's the bootstrap that mints a `vf_live_*` key (mode 1) and sets the `vf_session` cookie (mode 3b). Once the key or cookie exists, OAuth doesn't appear on the wire again until the customer signs out and signs back in.
- **Documenso direct auth** is not exposed. The wrapper holds the Documenso API key; callers never see it.
- **Refund authorization** is not a customer-facing auth shape. Refunds are administrative operations on the operator side; customers don't authenticate to issue them.

## What goes wrong, where

- API key → `UNAUTHORIZED` (401), `TOKEN_INVALID` (401). Re-mint via the dashboard if the key is lost.
- x402 → `INVALID_SIGNATURE`, `INSUFFICIENT_BALANCE` (402). Validity windows are tight; clock drift on the signing machine can land authorizations outside `validAfter`/`validBefore`.
- Wallet JWT signin → `INVALID_NONCE`, `CHALLENGE_EXPIRED`, `NONCE_ALREADY_USED`, `INVALID_WALLET_ADDRESS`. Each `nonce` is single-use and TTL-bounded; replay is rejected.

The full error catalog is at [`/guides/errors`](/guides/errors).
