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)
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)
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 for the wire format and /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.
# Step 1: get a one-shot challenge.
curl "https://api.verafirma.com/v1/auth/challenge?wallet=0x..."
# → { "nonce": "...", "message": "Sign this to authenticate...", "expiresAt": "..." }
# 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 '{"walletAddress":"0x...","nonce":"...","signature":"0x..."}'
# → { "token": "<JWT>", "expiresIn": 3600, "expiresAt": "..." }
# 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, 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 thevf_sessioncookie (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 outsidevalidAfter/validBefore. - Wallet JWT signin →
INVALID_NONCE,CHALLENGE_EXPIRED,NONCE_ALREADY_USED,INVALID_WALLET_ADDRESS. Eachnonceis single-use and TTL-bounded; replay is rejected.
The full error catalog is at /guides/errors.