Verafirma logo Verafirma

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.

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

CallerModeWhy
Autonomous AI agent, one shotx402 (mode 2)No account, no signup, single call carries everything
Autonomous AI agent, returningx402 + wallet JWTx402 for billable creates, JWT for history reads
Developer integrating from a serverAPI key (mode 1)Bearer auth, single key, balance-funded
Developer or end-user browsing the dashboardCookie (mode 3b)Set automatically after OAuth signin
Wallet user reading history without payingWallet 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.