# Verafirma — Full reference _Compiled 2026-05-12. Source pages tagged for: esignature, identity._ ## /docs/quickstart # Quickstart The fastest path: an agent signs an x402 payment authorization off-chain, attaches it as a header on a `POST /v1/envelopes` request, and gets back a signing URL. No account, no signup, no dashboard required. ## The lodestone, in five lines ```sh curl -X POST https://api.verafirma.com/v1/envelopes \ -H "PAYMENT-SIGNATURE: $(cat payment.b64)" \ -F 'pdf=@contract.pdf' \ -F 'payload={"title":"NDA","recipients":[{"email":"counterparty@example.com","name":"Counterparty","role":"SIGNER"}]}' ``` Response (`201`): ```json { "envelopeId": "9c5e...-...", "documensoEnvelopeId": "envelope_...", "status": "SENT", "title": "NDA", "recipients": [ { "email": "counterparty@example.com", "role": "SIGNER", "signingUrl": "https://sign.verafirma.com/sign/...", "signingStatus": "NOT_SIGNED" } ], "createdAt": "2026-05-07T..." } ``` The recipient gets an email with the signing link; the agent's job is done. Settlement happens on-chain via Relaystation's facilitator; the response is returned synchronously. The PAYMENT-SIGNATURE header carries a base64-encoded EIP-3009 `transferWithAuthorization` payload (USDC v2 on Base). See [`/concepts/x402`](/concepts/x402) for the wire format and signing flow. **Note:** x402 is not EIP-191 / SIWE / `personal_sign`. Those are for wallet-as-identity (the [wallet JWT path](/concepts/authentication)), which is a different mode. ## The three payment modes, briefly The lodestone above is one of three. Pick whichever fits the call site. ### 1. x402 per-call — no account The example above. One HTTPS request carries payload + payment. Wallet IS the identity for any later history queries (via the [wallet JWT path](/concepts/authentication)). Best for autonomous agents and one-shot integrations. ### 2. API key — Stripe-funded wallet Sign up with Google or GitHub on `app.verafirma.com`, top up via Stripe, mint a `vf_live_*` key: ```sh curl -X POST https://api.verafirma.com/v1/envelopes \ -H "Authorization: Bearer vf_live_a1b2c3..." \ -F 'pdf=@contract.pdf' \ -F 'payload={...}' ``` Calls debit the balance per envelope. Best for developers building products on top of the API. ### 3. Wallet JWT — wallet-as-identity for dashboard reads For wallet customers who want to see their history without a per-request signature, exchange a wallet signature for a JWT: ```sh # Step 1: get a challenge. curl "https://api.verafirma.com/v1/auth/challenge?wallet=0x..." # → { nonce, message, expiresAt } # Step 2: sign the message off-chain (EIP-191 personal_sign), POST the signature. curl -X POST https://api.verafirma.com/v1/auth/verify \ -d '{"walletAddress":"0x...","nonce":"...","signature":"0x..."}' # → { token, expiresIn: 3600 } # Step 3: use the JWT for read paths. curl https://api.verafirma.com/v1/account \ -H "Authorization: Bearer $JWT" ``` Note that the wallet JWT path uses **EIP-191** for the signin signature; the per-call x402 path uses **EIP-3009** for the payment authorization. Different cryptographic primitives, different flows, same wallet identity at the end. ## Common next steps - **Track envelope status**: `GET /v1/envelopes/{id}/status` returns the live state from Documenso (the underlying signing engine), reconciling any cached state if a webhook was missed. - **Subscribe to webhook events**: `POST /v1/webhooks` registers a callback URL for envelope state transitions. See [`/concepts/webhooks`](/concepts/webhooks). - **Discover via MCP**: an MCP-compatible client can hit the [`/mcp`](/concepts/mcp) endpoint and call tools like `verafirma.create_signing_request` directly. - **Read the OpenAPI spec**: [`/openapi.json`](/api-reference) is the machine-readable surface; generate a typed client from it in your language of choice. ## What can go wrong - `INSUFFICIENT_BALANCE` (402) — the wallet doesn't have enough funds to settle the call. Body carries `priceMicros`, `balanceMicros`, and a `topUpUrl` for funding. - `MISSING_PDF` / `MISSING_PAYLOAD` (400) — multipart upload missing one of the two required parts. - `VALIDATION_ERROR` (400) — payload JSON parsed, but a field failed validation. Body carries the offending field path. - `OUT_OF_SCOPE_V1` (400) — request exceeded the per-envelope cap (≤10 signers AND ≤10 documents). - `DOCUMENSO_UNAVAILABLE` (503) — the underlying signing engine is reachable but errored. Retry-safe with the same idempotency key. The full error catalog is at [`/guides/errors`](/guides/errors). ## /docs/lodestone # The lodestone path > An AI agent makes ONE HTTPS call to `POST /v1/envelopes` with a PDF, a recipient email, and an x402 payment authorization, gets back a signing URL, and never created an account. This is the canonical use case the API is designed around. Every architectural decision is judged against it: *does this make the lodestone harder?* If yes, it doesn't ship. ## Why this matters Most APIs assume a customer relationship: account, dashboard, billing portal, monthly invoice. That assumption breaks for two cases: 1. **AI agents** running on someone else's machine, with a process and a config and possibly a private key — but no email, no password, no phone number, no ability to click confirmation links or fill captchas. 2. **One-shot integrations** where the developer wants to use the API once, never again, and never get billed for nothing in between. The lodestone path serves both. There is no account-creation flow. There is no JWT mint. There is no "wallet auth challenge." The agent constructs one HTTPS request that carries everything, and the request is the entire interaction. ## What "one call" actually means Three pieces are bundled into the request: 1. **The payload** — a multipart upload with a PDF and a JSON object describing the recipient(s), title, optional fields, optional storage expiry. 2. **The payment authorization** — a `PAYMENT-SIGNATURE` header carrying a base64-encoded EIP-3009 `transferWithAuthorization` signed off-chain by the wallet that's paying. Off-chain means no gas; settlement happens server-side via Relaystation's facilitator. 3. **An idempotency key** — `Idempotency-Key` header. Mandatory on billable operations. Re-submissions with the same key return the original response without double-billing. The response is a `201 Created` with the envelope id, its current Documenso state, and per-recipient signing URLs. Settlement is synchronous: by the time the response returns, the wallet has been debited. ## Wallet IS the identity The lodestone has no signin step. So how does the wallet customer ever see their own history later? The wallet address that signed the EIP-3009 authorization IS the identity. Relaystation upserts a wallet-kind customer record on the first x402 admit and reuses it on subsequent calls. If the same wallet wants to read its envelope history later, it goes through the [wallet JWT path](/concepts/authentication): exchange an EIP-191 signature for a JWT, then use the JWT against `/v1/account/*` reads. The two flows use different cryptographic primitives — EIP-3009 for the payment authorization (per-call), EIP-191 for the wallet-as-identity signin (returning visits) — but they bind to the same wallet address, and the customer record persists across both. (See the [drift hazards section](#drift-hazards) below for why this distinction matters.) ## What's wrapped, what's not The lodestone is a thin wrapper: - **Documenso** owns the signing engine: PDFs, recipients, fields, signing UI, audit log, completed-PDF generation. The wrapper translates payload shape, calls Documenso, returns the result. - **Relaystation** owns billing: x402 settlement, wallet customer records, balance tracking, refund primitives. The wrapper hands the payment header to the Relaystation SDK and gets back a charged context. The wrapper itself does input validation, idempotency, terminal-event refund coordination, and outbound webhook fan-out. Nothing in the lodestone path requires any persistent verafirma-side state for the caller — wallet customers don't get a row in the verafirma database. ## When NOT to use the lodestone - **You want a single key for many calls.** Mint a `vf_live_*` API key after OAuth signup. Same envelope creation; bearer auth instead of x402. - **You want pre-funded calls without per-call gas.** Top up via Stripe. Same key, debits from balance. - **You want a dashboard.** OAuth signup at `app.verafirma.com` gives you the key plus the customer surface. These three modes share one identity (Relaystation's customer record) and one billing model (Relaystation's wallet). The lodestone is one call shape; the others are key-based variants of the same underlying flow. ## Drift hazards A few substitutions look reasonable but are wrong on the per-call path. Don't make them. - **Not EIP-191 / SIWE / `personal_sign` on the per-call path.** Those are wallet-as-identity primitives, used for the wallet JWT signin (returning visits). The per-call payment authorization is EIP-3009, not EIP-191. Different signing scheme, different domain separator, different recovered address. - **Not a separate signin step before the call.** That's the API-key-with-Stripe-wallet mode (mode 2/3). The lodestone is one call, period. - **Not a deposit-then-withdraw exchange model.** That's a deferred Relaystation V2 deliverable. Today the lodestone settles per-call; deposits onto a wallet are not a V1 surface. - **Not a custom x402 variant.** x402 is a real spec; standard client libraries work against the endpoint. Don't invent a header shape that locks customers in. ## Reference reading - The wire format is at [`/concepts/x402`](/concepts/x402); Relaystation's `docs/protocols/x402.md` is the authoritative source for the protocol. - The pricing surface is at [`/pricing`](/pricing). - The authentication modes (lodestone vs API key vs wallet JWT vs same-origin cookie) are summarized at [`/concepts/authentication`](/concepts/authentication). - The full machine-readable surface is at [`/openapi.json`](/api-reference). ## /docs/x402 # x402 wire format x402 is the per-call payment standard the lodestone path uses. An agent signs an EIP-3009 `transferWithAuthorization` for the call's exact amount, attaches the signature as a request header, and the API settles synchronously through a payment facilitator. No on-chain transaction from the agent (no gas), no pre-funded balance, no relationship beyond the single call. ## What it is, what it isn't x402 is **not**: - Not EIP-191. Not SIWE. Not `personal_sign`. Those are wallet-as-identity primitives — useful for signing into a dashboard. The per-call payment uses a different signing scheme entirely. - Not a custom variant. x402 is a real spec; the wire shape below matches what standard x402 client libraries produce. - Not a deposit-then-withdraw exchange. Each call is a complete payment; the wallet is debited synchronously and there's no pre-funded balance to draw down. x402 **is**: - An off-chain signed authorization for an on-chain stablecoin transfer. The agent signs; a facilitator (Relaystation) submits. - EIP-3009 `transferWithAuthorization` over USDC v2 on Base (Sepolia or mainnet, configured at the facilitator). - A single request-header payload — base64-encoded JSON of the signed payment object. - Idempotent at the facilitator level: a replayed authorization with the same `nonce` settles once, then errors on subsequent submission. ## The payment header ```http PAYMENT-SIGNATURE: ``` The decoded JSON looks roughly like: ```json { "version": "1", "scheme": "exact", "network": "base-sepolia", "payTo": "0xFacilitator...", "asset": "0xUsdcAddress...", "maxAmountRequired": "100000", "maxTimeoutSeconds": 600, "extra": { "name": "USDC", "version": "2" }, "payload": { "signature": "0x<132-hex>", "authorization": { "from": "0xWalletAddress...", "to": "0xFacilitator...", "value": "100000", "validAfter": "1714000000", "validBefore": "1714003600", "nonce": "0x<32-bytes-hex>" } } } ``` `100000` micros = $0.10 (USDC's six-decimal scale). The exact field set, version, and signing rules are owned by Relaystation's protocol document — see the [authoritative reference](#authoritative-reference) below. Don't construct the payload by hand from this page; use a client library that targets the spec. ## Why EIP-3009 and not EIP-191 EIP-191 (`personal_sign`) signs an arbitrary message scoped to one wallet. SIWE (Sign-In With Ethereum) is an EIP-191 application: sign "I claim to be wallet X at this domain" and the relying party verifies by recovering the signing address. EIP-191 doesn't sign value. It can't tell anyone "I authorize transferring $0.10 to address Y, valid only from time T to T+10 minutes, with replay-protected nonce Z." That's what EIP-3009's `transferWithAuthorization` does, and that's what an x402 payment needs: a verifiable instruction with a value, a recipient, a validity window, and a replay nonce. The two coexist on the same wallet: | Primitive | What it signs | Where it appears in this API | |---|---|---| | EIP-3009 `transferWithAuthorization` | Token transfer with value, recipient, validity, nonce | The per-call payment header (lodestone path) | | EIP-191 `personal_sign` | Arbitrary message string | The wallet JWT signin (returning-visit history reads) | Both primitives recover the same wallet address from a given private key, so the customer record at Relaystation collapses to one row regardless of which path the wallet uses first. ## What the API does with the header When `PAYMENT-SIGNATURE` arrives on a billable route, the SDK middleware: 1. Decodes the base64 payload. 2. Verifies the signature recovers the claimed `from` address. 3. Resolves or creates a wallet-kind customer record at Relaystation keyed by `from`. 4. Hands the call to the wrapper's `withCharge` handler with a charged context. 5. After the wrapper returns successfully, the facilitator submits the EIP-3009 authorization on-chain. Settlement is synchronous-from-the-caller's-perspective; the response includes the result. If the signature is invalid, the wallet has insufficient USDC, or the validity window has expired, the call fails before any work runs. The wrapper never sees an unbacked request. ## Authoritative reference The wire format, version semantics, supported networks, and the exact `PaymentPayload` schema are owned by Relaystation's protocol document at `relaystation/docs/protocols/x402.md`. This page is intentionally a summary; if you need to construct payloads or interpret error codes that come back from the facilitator, read the protocol doc. The `/openapi.json` surface registers x402 as a `PAYMENT-SIGNATURE`-named API-key security scheme; tooling that consumes the OpenAPI spec gets the header name and a brief description automatically. ## Common error codes When something goes wrong on the x402 path, the response body's `code` field narrows the cause: - `INSUFFICIENT_BALANCE` (402) — wallet has fewer USDC than `maxAmountRequired`. Body carries `priceMicros`, `balanceMicros`, and a `topUpUrl`. (For x402 the top-up is an on-chain transfer to the wallet, not a Stripe top-up.) - `INVALID_SIGNATURE` — the recovered address doesn't match `from`, the signature isn't well-formed, or the EIP-712 domain separator is wrong for the configured network. - `CHALLENGE_EXPIRED` / `NONCE_ALREADY_USED` — these surface on the wallet-JWT path, not the x402 path. If you see them on a `POST /v1/envelopes` call, the request is using the wrong auth shape. The full error catalog is at [`/guides/errors`](/guides/errors). ## /docs/authentication # 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: ``` 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..." # → { "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": "", "expiresIn": 3600, "expiresAt": "..." } # Step 4: use the JWT for read paths. curl https://api.verafirma.com/v1/account \ -H "Authorization: Bearer " ``` 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). ## /docs/mcp # MCP (Model Context Protocol) MCP is the protocol AI clients use to call tools across a network boundary. The API exposes a streamable-HTTP MCP server; an MCP-compatible client can connect, list available tools, and call them with the same auth modes the REST surface uses. ## The endpoint ``` POST https://api.verafirma.com/mcp ``` Streamable-HTTP transport. JSON-RPC over HTTP. Methods: `initialize`, `tools/list`, `tools/call`. Auth flows through the same modes as `/v1/*`: API key (`Authorization: Bearer vf_live_*`), x402 (`PAYMENT-SIGNATURE` header), or wallet JWT (`Authorization: Bearer `). The tools wrap the REST handlers, not duplicate them — so a `tools/call` against `verafirma.create_signing_request` runs through the same auth, validation, billing, and Documenso wrap as `POST /v1/envelopes`. There's no second handler to keep in sync. ## V1 tools The current tool set, exposed via `tools/list`: - **`verafirma.create_signing_request`** — wraps `POST /v1/envelopes`. Send a PDF + recipient(s) + payment authorization; get back a signing URL. - **`verafirma.get_envelope_status`** — wraps `GET /v1/envelopes/{id}/status`. Live status from the underlying signing engine; reconciles cached state. - **`verafirma.list_envelopes`** — wraps `GET /v1/envelopes`. Paginated history. - **`verafirma.register_webhook`** — wraps `POST /v1/webhooks`. Register a callback URL for envelope state events. Additional tools land as new endpoints stabilize. The `tools/list` response is authoritative; this page summarizes a snapshot. ## Configuring Claude Desktop To wire the API's tools into Claude Desktop, edit the desktop config (`claude_desktop_config.json`): ```json { "mcpServers": { "verafirma": { "url": "https://api.verafirma.com/mcp", "transport": "http", "headers": { "Authorization": "Bearer vf_live_" } } } } ``` Restart Claude Desktop after editing. The tools appear in the tool palette; calls debit the same wallet the API key is bound to. ## Configuring other MCP clients The endpoint speaks the standard streamable-HTTP MCP transport. Any client that targets the spec works against the URL above. The pattern is the same: point the client at `https://api.verafirma.com/mcp`, supply the auth header your account uses, and the client picks up the tools list automatically. For agents using the lodestone path (no API key), the auth header is `PAYMENT-SIGNATURE` instead. The exact header is supplied per `tools/call`, not at session level — each call carries its own payment authorization, since the per-call billing model doesn't have a session concept. ## Discovery surfaces Two well-known files advertise the MCP endpoint to crawlers and clients that don't already know to look: - **`/.well-known/mcp`** — SEP-1960 lightweight advertisement: endpoint URL + accepted auth methods. Tiny JSON. - **`/.well-known/mcp/server-card.json`** — SEP-1649 rich server-card: endpoint, homepage, contact, tool summaries. Both shapes follow in-flight MCP spec proposals. They're stable enough to ship; if the spec moves to a different shape, this site refreshes the well-known files in a follow-up. The `/.well-known/mcp.json` route on the API (distinct from these marketing-side ones) is the runtime advertisement that clients hitting the API origin would look for; the `/openapi.json` discovery surface tags it under the `discovery` operation set. ## What MCP doesn't change - **Auth is the same.** API key, x402, wallet JWT all work; the cookie path doesn't (MCP clients don't run in a same-origin browser context). - **Billing is the same.** Per-call price applies to billable tool calls, not to discovery (`tools/list`) or `initialize`. - **Idempotency is the same.** A `tools/call` for a billable operation accepts an idempotency key in arguments and treats it the same way the REST surface does. - **Errors are the same shape.** Tool errors carry the same `code` set as the REST API; clients can branch on them without a second translation layer. ## Pointing humans at it If you're writing a docs page or onboarding flow that mentions MCP, point at the endpoint URL plus the V1 tool list above. The agent-discovery surfaces (`/llms.txt`, `/.well-known/mcp*`) cover the AI-client side; humans configure their client manually using one of the patterns shown above. ## /docs/envelopes # Envelopes An **envelope** is one PDF sent to one or more recipients for signature. It's the fundamental unit of work: when callers think about "send a contract" or "collect a signature on this document," they're thinking about an envelope. ## The pieces ### Envelope The container. Carries: - **A PDF.** Either uploaded directly (multipart form on `POST /v1/envelopes`) or instantiated from a [template](#templates). - **A title.** Free-text label for the customer side; appears in dashboards and notification emails. - **A status.** Tracks the envelope's lifecycle. - **A storage expiry.** Optional; the underlying signing engine retains the completed PDF for the configured number of days, after which it's purged. ### Recipient A person or role that interacts with the envelope. One envelope can have up to ten recipients in V1. Each recipient has: - **Email.** Where the signing-link notification is sent. - **Name.** Displayed in the signing UI. - **Role.** One of: - `SIGNER` — adds a binding signature. - `APPROVER` — must approve before signers can sign (sequenced workflows). - `VIEWER` — can read the envelope but doesn't sign. - `CC` — receives a copy of the completed envelope but doesn't interact. - `ASSISTANT` — fills fields on behalf of a signer (delegated data entry, no signature authority). ### Field A placeable element on the PDF. Coordinate-based (percentage of page) OR PDF-placeholder-marker-based (`{{signature, r1}}` literals embedded in the source PDF). Field types: `SIGNATURE`, `INITIALS`, `NAME`, `EMAIL`, `DATE`, `TEXT`, `NUMBER`, `CHECKBOX`, `RADIO`, `DROPDOWN`. Each field is bound to one recipient via their email; only that recipient can fill the field. ### Template A saved envelope definition. PDFs + field placements + recipient roles, parameterized so a new envelope can be instantiated from the template with just-the-recipients-changed. Templates are esignature-specific; they don't surface on the lodestone path (which is single-shot by design). Created via `POST /v1/templates`; instantiated via `POST /v1/templates/{id}/from-template`. ## Lifecycle ``` DRAFT → PENDING → SENT → PARTIALLY_SIGNED → COMPLETED ``` With three alternative terminal states: ``` SENT → REJECTED (a signer rejected; flow halts; refund-on-terminal triggers) SENT → EXPIRED (the signing window closed without all signers; refund-on-terminal triggers) SENT → CANCELLED (the customer or operator cancelled; refund-on-terminal triggers) ``` The four terminal states (`COMPLETED`, `REJECTED`, `EXPIRED`, `CANCELLED`) are the only states from which an envelope cannot transition. `DRAFT` is the initial status when `POST /v1/envelopes` is called without a `fields` array. Mutations on the draft (`POST /:id/fields`, `DELETE /:id/fields/{fid}`) only work while the envelope is `DRAFT`. Once `POST /:id/send` distributes the envelope, no more field changes are allowed. For the simple workflow — one PDF, fields known up front — call `POST /v1/envelopes` with the fields array included in the payload, and the envelope skips `DRAFT` and goes straight to `PENDING`/`SENT`. ## Sending and signing ### Distribution When an envelope transitions to `SENT`, each recipient gets a signing-link email from the underlying signing engine. The link carries a token bound to that recipient; opening it loads the signing UI with their fields pre-targeted. ### Signing The signing UI is hosted by the underlying signing engine (Documenso) at `sign.verafirma.com`. Signers fill their fields, click sign, and the engine generates a signed-PDF artifact bound to the envelope. When all `SIGNER` recipients have signed, the envelope transitions to `COMPLETED`. The signing UI reflects the operator's brand — the API consumer doesn't supply a signing UI; the wrapped engine handles it. ### Reminders Recipients who haven't signed get periodic reminder emails (cadence configured at the engine). The customer can also chase manually: ```sh curl -X POST https://api.verafirma.com/v1/envelopes/{id}/resend \ -H "Authorization: Bearer vf_live_..." ``` This re-sends the signing link to NOT_SIGNED recipients without changing the envelope's state. ### Cancellation A SENT envelope can be cancelled before all signatures land: ```sh curl -X DELETE https://api.verafirma.com/v1/envelopes/{id} \ -H "Authorization: Bearer vf_live_..." ``` Cancellation transitions the envelope to `CANCELLED` and triggers the refund-on-terminal flow (the original charge is refunded up to the per-envelope cap). ## Reading state ### Cached vs. live Two read endpoints with different freshness: - `GET /v1/envelopes/{id}` — returns the wrapper's cached state. Fast (sub-100ms typically); reflects whatever the wrapper last persisted from a webhook event or read. - `GET /v1/envelopes/{id}/status` — fetches authoritative state from the underlying signing engine, reconciles the cache if it had drifted, and returns the live shape. Use `/status` when accuracy matters (e.g. before completing a downstream action that depends on signature completion). Use the cached read for high-volume polling where eventual consistency is fine. ### Listing `GET /v1/envelopes?limit=&offset=&status=` returns paginated envelopes for the authenticated customer. The `status` filter is exact-match against the V1 status enum. ### Downloading `GET /v1/envelopes/{id}/download?version=signed` streams `application/pdf`. Only available when the envelope is `COMPLETED`. The `version` query is `signed` (default — the signed artifact) or `original` (the source PDF before signing). ## Limits V1 caps each envelope at: - **≤10 signers** AND **≤10 documents.** Anything larger returns `400 OUT_OF_SCOPE_V1`. These limits are tunable post-deploy (per the operator's configuration surface) but the V1 defaults won't move without explicit operator action. If you have a use case that needs >10 of either, contact the operator. ## Pricing $0.10 per envelope, charged at creation. Refunded on terminal failure (`REJECTED`, `EXPIRED`, `CANCELLED`) up to a configurable per-envelope cap. See [`/pricing`](/pricing) for the per-mode breakdown. ## /docs/webhooks # Outbound webhooks Webhooks are how the API tells you about state changes that happen between your calls. Register a URL, the wrapper POSTs to it whenever a subscribed event fires, your handler verifies the HMAC signature and reacts. This page covers the customer-side outbound webhooks (the API calling your URL). It does not cover inbound webhooks the wrapper handles internally (Documenso → wrapper, Relaystation → wrapper); those don't surface to API consumers. ## Registering a webhook ```sh curl -X POST https://api.verafirma.com/v1/webhooks \ -H "Authorization: Bearer vf_live_..." \ -H "Content-Type: application/json" \ -d '{ "url": "https://your-app.example.com/webhooks/verafirma", "events": ["envelope.completed", "envelope.signed"], "description": "production handler" }' ``` Response (`201`): ```json { "id": "wh_", "url": "https://your-app.example.com/webhooks/verafirma", "events": ["envelope.completed", "envelope.signed"], "active": true, "secret": "whsec_<32-hex>", "consecutiveFailures": 0, "createdAt": "..." } ``` **The `secret` field is returned ONCE.** Subsequent reads of the registration omit it. Store it on registration; if it's lost, delete the registration and re-register. ## V1 events The seven events the wrapper emits: | Event | When | |---|---| | `envelope.sent` | Envelope distributed; signing-link emails sent. | | `envelope.viewed` | A recipient opened the signing link. | | `envelope.signed` | A `SIGNER` recipient completed their signature. Fires once per signer. | | `envelope.completed` | All `SIGNER` recipients have signed; envelope is terminal at `COMPLETED`. | | `envelope.rejected` | A `SIGNER` rejected; envelope is terminal at `REJECTED`. | | `envelope.expired` | Signing window closed without all signers; envelope is terminal at `EXPIRED`. | | `envelope.cancelled` | Envelope cancelled by the customer or an operator; envelope is terminal at `CANCELLED`. | Subscribe with `events: ["*"]` to receive all events. Subscribe with a narrower list to receive only those. ## Delivery shape Each delivery is a POST with a JSON body and signing headers: ```http POST /webhooks/verafirma HTTP/1.1 Content-Type: application/json X-Verafirma-Signature: t=,v1= X-Verafirma-Event: envelope.completed X-Verafirma-Delivery-Id: dlv_ { "event": "envelope.completed", "deliveredAt": "2026-05-07T...", "envelope": { "envelopeId": "...", "documensoEnvelopeId": "...", "status": "COMPLETED", "title": "...", "recipients": [...], "createdAt": "..." } } ``` The body is the same shape `GET /v1/envelopes/{id}` would return for that envelope, plus the event metadata at the top level. ## Verifying signatures The `X-Verafirma-Signature` header carries an HMAC-SHA256 over `${timestamp}.${raw_body}`, signed with the registration secret. Verify before trusting the body. ```js import { createHmac, timingSafeEqual } from 'node:crypto'; function verifyWebhook(rawBody, signatureHeader, secret) { const parts = Object.fromEntries( signatureHeader.split(',').map((kv) => kv.split('=')), ); const timestamp = parts.t; const v1 = parts.v1; if (!timestamp || !v1) return false; const signed = `${timestamp}.${rawBody}`; const expected = createHmac('sha256', secret).update(signed).digest('hex'); // constant-time compare const a = Buffer.from(v1, 'hex'); const b = Buffer.from(expected, 'hex'); if (a.length !== b.length) return false; if (!timingSafeEqual(a, b)) return false; // optional but recommended: reject deliveries older than 5 min. const age = Math.abs(Date.now() / 1000 - Number(timestamp)); if (age > 300) return false; return true; } ``` The `t=` timestamp is unix seconds. Including it in the signed payload prevents an attacker who captures one delivery from replaying it later. The 5-minute freshness window above is a recommended client-side check; the API doesn't enforce it. ## Retry behavior On non-2xx responses (or no response within the request timeout), the delivery is queued for retry. The default retry schedule: ``` 1m → 5m → 15m → 1h → 6h → 24h → done (terminal failure logged) ``` The schedule is configurable on the operator side; the cap on retries is too. If your handler returns 2xx within the schedule, the delivery is marked successful; otherwise it transitions to terminal failure and stops retrying. After enough consecutive failures the registration's `consecutiveFailures` counter reaches a threshold and the operator can choose to disable the registration. Successful deliveries reset the counter to zero. ## Idempotency on your side The `X-Verafirma-Delivery-Id` header is unique per delivery. Two deliveries with the same id are rare (network blips at the SQS layer can occasionally produce them) but possible. The recommended pattern: persist the delivery id when you process it; on a future delivery with the same id, return 2xx without re-running the side effect. This is a one-row, one-index pattern — cheap, prevents duplicate downstream actions. ## Managing registrations ```sh # List your registrations: GET /v1/webhooks # Read a single registration (no secret in the response): GET /v1/webhooks/{id} # Update events list, URL, description, or active flag: PUT /v1/webhooks/{id} # Delete (deliveries history retained for audit): DELETE /v1/webhooks/{id} # Page through delivery history: GET /v1/webhooks/{id}/deliveries?limit=&offset=&status= ``` The `status` filter on the deliveries list is one of `PENDING`, `SUCCESS`, `FAILED`. Use the deliveries log when debugging why your handler isn't seeing an event you expected. ## What NOT to do - **Don't trust the body without verifying the signature.** A handler that processes the body without signature verification is the standard webhook footgun; an attacker who knows your URL can POST anything they want. - **Don't store the secret in client-side code.** It's a server-side credential. If your URL is reachable from the client side, your handler still runs server-side; keep the secret there. - **Don't return 2xx until the side effect is durable.** A 2xx response tells the wrapper "delivery acknowledged"; if your downstream side effect failed but you returned 2xx anyway, the wrapper won't retry. - **Don't poll instead of subscribing.** `GET /v1/envelopes/{id}/status` is for ad-hoc checks, not for pub-sub. Webhooks scale; polling doesn't. ## /docs/errors # Errors The API uses standard HTTP status codes plus a machine-readable `code` field in the body so callers can branch programmatically. ## Response shape Every error response carries this shape: ```json { "error": "Human-readable message", "code": "MACHINE_READABLE_CODE", "...": "additional context per code" } ``` The `error` field is for logs and operator eyes. The `code` field is what your error-handling code branches on. Some codes carry extra fields specific to the error (e.g. `INSUFFICIENT_BALANCE` carries `priceMicros` and `topUpUrl`). ## Common codes by category ### Auth - `UNAUTHORIZED` (401) — no auth header present, or the header didn't resolve to a customer. - `TOKEN_INVALID` (401) — the API key or wallet JWT signature didn't verify. - `INVALID_SIGNATURE` (401) — for x402, the recovered EIP-3009 signing address didn't match the claimed `from`. - `INVALID_NONCE` / `CHALLENGE_EXPIRED` / `NONCE_ALREADY_USED` (400/401) — wallet-JWT challenge/verify path; the nonce flow expects a single-use, TTL-bounded nonce. - `INVALID_WALLET_ADDRESS` (400) — wallet address parameter wasn't well-formed. - `MISSING_FIELDS` (400) — the verify body was missing required fields. ### Billing - `INSUFFICIENT_BALANCE` (402) — the wallet doesn't have enough funds for the call. Body carries: ```json { "error": "Insufficient balance", "code": "INSUFFICIENT_BALANCE", "priceUsd": "0.10", "priceMicros": 100000, "balanceUsd": "0.05", "balanceMicros": 50000, "topUpUrl": "https://app.verafirma.com/...", "retryable": false } ``` `retryable: false` means re-issuing the same call won't succeed; the customer needs to top up first. For x402 callers, the top-up is an on-chain transfer; for API-key callers, the top-up is the Stripe portal link in `topUpUrl`. ### Validation - `MISSING_PDF` (400) — multipart upload had no `pdf` part. - `MISSING_PAYLOAD` (400) — multipart upload had no `payload` part. - `INVALID_JSON` (400) — `payload` was not valid JSON. - `VALIDATION_ERROR` (400) — payload parsed, but a field failed validation. Body carries the offending field path under `details`. - `OUT_OF_SCOPE_V1` (400) — request exceeded the per-envelope cap (>10 signers OR >10 documents). ### Envelope state - `ENVELOPE_NOT_FOUND` (404) — no envelope with that id is owned by the calling customer. The wrapper scopes lookups to the authenticated customer; an envelope that exists for someone else returns 404, not 403. - `CANNOT_DELETE_COMPLETED` (400) — DELETE on a `COMPLETED` envelope is rejected; completed envelopes are terminal. - `ENVELOPE_ALREADY_COMPLETE` (400) — resend on a `COMPLETED` envelope. - `ENVELOPE_CANCELLED` (400) — resend on a `CANCELLED` envelope. - `ALL_SIGNED` (400) — resend when every recipient has already signed (no NOT_SIGNED recipients to chase). - `ENVELOPE_NOT_COMPLETE` (400) — download attempted on a non-`COMPLETED` envelope. - `DOWNLOAD_FAILED` (502) — the underlying signing engine returned an unexpected response on the PDF stream. ### Templates - `TEMPLATE_NOT_FOUND` (404). - `INVALID_TEMPLATE_ID` (400) — template id wasn't a UUID. - `RECIPIENT_NOT_FOUND` (404) — `from-template` recipient mapping referenced a recipient role that doesn't exist on the template. - `INVALID_TEMPLATE_RECIPIENT_ID` (400). ### Fields - `FIELD_CREATION_FAILED` (502) — fields-add call to the engine failed. - `INVALID_FIELD_ID` (400) — field id wasn't a valid identifier. - `DELETE_FAILED` (502) — field-delete call to the engine failed. ### Webhooks - `INVALID_WEBHOOK_URL` (400) — URL wasn't well-formed, or was on the deny-list (private IP ranges, the API's own origin, etc.). - `WEBHOOK_NOT_FOUND` (404). ### Upstream - `DOCUMENSO_UNAVAILABLE` (503) — the underlying signing engine is reachable but errored. Retry with exponential backoff against the same idempotency key. - `RELAYSTATION_UNAVAILABLE` (503) — billing facilitator errored. Same retry guidance. - `SEND_FAILED` (502) — `POST /v1/envelopes/{id}/send` failed at the engine layer. - `UPDATE_FAILED` (502) — template update failed at the engine layer. ## Idempotency and retries Billable operations require an `Idempotency-Key` header. A retry with the same key returns the original response (success OR error) without re-running the side effect. For 5xx upstream errors, retry with the same key is the right move. For 4xx errors, the retry will return the same 4xx — fix the request, then retry with a fresh key. ## What you don't see A few things that look like errors but aren't surfaced as error responses: - **Webhook delivery failures** — when the wrapper's POST to your URL fails, your registration's `consecutiveFailures` counter increments, but no error appears on a customer-side API call. Check `GET /v1/webhooks/{id}/deliveries` to debug. - **Refund failures** — if a refund-on-terminal flow fails partway through, the refund is logged as failed and the operator is alerted. The terminal state on the envelope (`REJECTED` etc.) doesn't change; the customer doesn't see an API error from the original `POST /v1/envelopes` (it succeeded, then later failed terminally). - **Backfill / migration errors** — operator-side internal flows. Customers don't see them. ## Audit log Every customer-side mutation that persists state writes to an audit log row owned by the customer. There's no customer-facing API for the audit log in V1; it's an operator surface. If you need to investigate why an envelope ended up in an unexpected state, the operator can pull the audit history and explain. In V1.x the operator may expose a per-customer audit-read API; that's not a V1 commitment. ## Shared: faq # FAQ Five questions a developer or agent operator typically asks first. The full surface is in the documentation; this is the entry point. ## Do I need an account? No, not for the lodestone path. Sign an EIP-3009 payment authorization off-chain, attach it as a `PAYMENT-SIGNATURE` header, send a single `POST /v1/envelopes` request, and you get back a signing URL. No signup, no dashboard, no email confirmation. If you want history reads later, the same wallet address can sign in via the wallet JWT path and see its own envelopes. If you want a single API key for many calls without per-request signing, sign up with Google or GitHub at `app.verafirma.com` and mint one. Both are optional layers on top of the per-call path. ## How does x402 work? The wallet signs an EIP-3009 `transferWithAuthorization` for the call's exact amount, off-chain (no gas). The signature carries the wallet address, the recipient (the facilitator), the amount, a validity window, and a replay-protected nonce. The signature lands as a request header; the API verifies it, settles synchronously through the facilitator, and runs the wrapped operation. By the time the response returns, the wallet has been debited. x402 is **not** EIP-191, SIWE, or `personal_sign` — those are for wallet-as-identity signin (the wallet JWT path), which is a different flow. See [`/concepts/x402`](/concepts/x402) for the wire format. ## What's the price? $0.10 per envelope. Pay per call — no minimum, no commitment. Refunded automatically on terminal failure (`REJECTED`, `EXPIRED`, `CANCELLED`) up to a per-envelope cap. The full breakdown by payment mode is at [`/pricing`](/pricing). ## Where's the OpenAPI spec? `https://api.verafirma.com/openapi.json` — full machine-readable surface. The web-rendered version with audience-augmented metadata is at [`/api-reference`](/api-reference). Generate a typed client from the JSON in your language of choice; the spec is OpenAPI 3.1. ## Where's the MCP endpoint? `https://api.verafirma.com/mcp` — streamable-HTTP MCP server. Tools wrap the same handlers as the REST API; auth flows through the same modes (API key, x402, wallet JWT). The current V1 tool set: `verafirma.create_signing_request`, `verafirma.get_envelope_status`, `verafirma.list_envelopes`, `verafirma.register_webhook`. The authoritative list is in the `tools/list` response from the endpoint. See [`/concepts/mcp`](/concepts/mcp) for client config patterns. ## Shared: llms-instructions This API is built for AI agents. The fastest path is one HTTPS request: `POST /v1/envelopes` carries a multipart upload (PDF + JSON payload describing recipients) plus a `PAYMENT-SIGNATURE` header (base64-encoded EIP-3009 `transferWithAuthorization` over USDC v2 on Base). The response is a signing URL the recipient can use immediately. No signup is required for the per-call path; the wallet that signs the payment is the identity for any later history reads. **Pricing:** $0.10 per envelope. Pay per call — no minimum. Refunded automatically on terminal failure (REJECTED / EXPIRED / CANCELLED) up to a per-envelope cap. **Wire format authority:** the x402 payment shape is defined by the protocol document at `relaystation/docs/protocols/x402.md`. The envelope creation surface is documented in the OpenAPI spec at `/openapi.json`. Use a standard x402 client library to construct the payment payload — don't construct the JSON by hand. **Discovery surfaces (use these to navigate, not just to find this page):** - `/openapi.json` — full machine-readable API surface. - `/llms-full.txt` — concatenated raw markdown of every page below, no token cap. - `/.well-known/mcp` — MCP endpoint advertisement (SEP-1960 shape). - `/.well-known/mcp/server-card.json` — MCP rich card with tool summaries (SEP-1649 shape). - `https://api.verafirma.com/mcp` — streamable-HTTP MCP server. The four V1 tools (`verafirma.create_signing_request`, `verafirma.get_envelope_status`, `verafirma.list_envelopes`, `verafirma.register_webhook`) wrap the REST handlers. **Drift hazards to know:** - The per-call payment uses **EIP-3009**, NOT EIP-191 or SIWE or `personal_sign`. EIP-191 IS used elsewhere — for the wallet-JWT signin on the dashboard-read path — but that's a different flow. - Three documented payment modes exist (x402 per-call, API-key Stripe-funded wallet, wallet-JWT for reads). Don't substitute one for another based on apparent similarity; the auth header shape and billing primitive differ. - `/health` is for ops, not customers. The OpenAPI surface intentionally hides it from customer-facing renders. If something doesn't work, the error response carries a `code` field. The full code catalog is at `/docs/errors`. For 5xx upstream errors, retry with the same `Idempotency-Key` header — duplicates collapse to the original response. ## Shared: pricing-snippet # Pricing **$0.10 per envelope.** Pay per call. No minimum. No commitment. Refunded automatically on terminal failure (`REJECTED`, `EXPIRED`, `CANCELLED`) up to a per-envelope cap. That's the entire pricing surface. Everything below is detail on how the $0.10 gets paid. ## Three ways to pay ### x402 per-call The wallet signs an EIP-3009 authorization for $0.10 (in USDC v2 micros) and attaches it to the request. Settlement is synchronous; the wallet is debited as the response returns. No account, no signup, no relationship beyond the single call. The wallet IS the identity for any later history queries — see [`/concepts/lodestone`](/concepts/lodestone). ### API key (Stripe-funded wallet) Sign up at `app.verafirma.com` with Google or GitHub. Top up the wallet via Stripe in any amount (no minimum). Mint a `vf_live_*` key. Every call debits $0.10 from the balance. No per-call gas, no per-call signing. Usable from automation, server-side code, or the dashboard. Same wallet identity as the x402 path; if you start with x402 and later sign up via OAuth, the dashboard surfaces the same envelope history. ### Crypto-funded wallet (deferred) A single on-chain deposit that funds many subsequent calls without per-call gas. **Not available in V1.** Listed here for completeness; the per-call x402 path covers the agent use case today. ## Refunds When an envelope reaches a terminal failure state — a recipient rejected, the signing window expired, or the customer cancelled — the original $0.10 charge is automatically refunded to the wallet that paid for it. Refunds happen through the underlying billing layer and apply up to a per-envelope cap (default: 3 refunds per envelope, configurable by the operator). Anything beyond the cap is held as a no-op so a flapping cancel/re-create loop can't drain the operator's float. For x402 customers, the refund lands as a USDC transfer back to the wallet that paid. For API-key customers, the refund lands as a balance credit on the wallet. ## What's NOT in the price The $0.10 covers: - The PDF upload and storage at the underlying signing engine. - Signing-link emails to recipients. - The hosted signing UI. - Webhook deliveries (on retry too — failed deliveries don't double-bill). - The completed-PDF retention window. Optional extras that may carry separate pricing in future: - Storage extensions beyond the default retention (currently inherited from the underlying engine; not separately priced in V1). - Premium audit features (advanced audit reports, retention beyond the V1 default). V1 has no premium tier, no rate-limit-tuning fee, no support tier. If those land they'll appear on this page. ## Currency All prices are USD-denominated. The on-chain settlement asset for x402 is USDC v2 (Base Sepolia for dev, Base mainnet for prod). On-chain transfers carry no separate facilitator fee at V1 volume; the $0.10 is what the wallet pays, period.