# Verafirma — Full reference _Compiled 2026-05-14. 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/verification # Verification Verafirma verification checks identity in one HTTPS call. POST a verification request, get back a hosted URL the end-user opens to upload their ID and selfie. We return a status webhook when the check completes. **$0.10 per verification — pay per call, no minimum, no commitment.** ## Why pay per call The incumbents — DocuSign, ID.me, Entrust — bundle ID verification with liability coverage, KYC pipelines, anti-fraud scoring, AML screening, and enterprise compliance reporting. They charge $1-$2/call for the whole bundle. **Most developers and agents only need one thing in that bundle: verified ID.** Cable-TV pricing, where you pay for sports when all you want is news. Verafirma sells the channel you actually want, at the cost of running it. When you need the full bundle (regulated KYC, AML scoring, liability transfer), the incumbents are still your answer. When you just need "this person's ID checked out," we're 10-20x cheaper because we're not making you pay for the bundle. ## How it works ```sh curl -X POST https://api.verafirma.com/v1/verifications \ -H "Authorization: Bearer vf_live_..." \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "type": "document" }' ``` Returns `201` with `{ id, status: "PENDING", hostedUrl, ... }`. The end-user opens `hostedUrl`, uploads their ID and selfie. We POST a webhook to your registered URL when the check completes. The `type` field selects depth: `document` (ID image + checks), `liveness` (selfie + liveness probe), `biometric` (full identity binding). Soft-disableable per type via the `verafirma.verification.allowed_types` tunable. [See the API reference](/api-reference#tag/Verification) for the full surface. ## Authentication Four options — same as envelope signing: - **API key** (`Authorization: Bearer vf_live_...`) — sign up via Google or GitHub OAuth, top up your balance via Stripe. - **x402** (`PAYMENT-SIGNATURE` header with a signed EIP-3009 USDC authorization) — no signup, no account, pay per call directly. The cleanest path for AI agents. - **Wallet JWT** (`Authorization: Bearer ` from `/v1/auth/verify`) — for crypto-native developers building dashboards. - **Session cookie** (`vf_session`) — for browser-based dashboard flows. Same wallet identity across all four; if you start with x402 and later sign up via OAuth, the dashboard surfaces the same verification history. ## Pricing $0.10 per verification, charged at creation. Refunded on terminal failure (`verification.failed`) up to `verafirma.verification.refund_cap_per_call` (default 1). See [`/pricing`](/pricing) for the per-mode breakdown. ## When you outgrow us Verafirma is built to be replaceable. We host battle-tested open-source verification software ourselves on cheap infrastructure and charge you what it costs to run plus a markup. When your volume reaches the point where running it yourself makes sense, ask us — we'll point you at exactly what we use and how we configured it. No vendor lock-in. No proprietary client SDK you'd have to throw away. The on-the-wire contract is standard HTTPS + x402; you can swap us out without changing client code. ## /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 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 call — for both envelope signing and ID verification. Pay per call — no minimum, no commitment. Envelopes refund automatically on terminal failure (`REJECTED`, `EXPIRED`, `CANCELLED`) up to a per-envelope cap. Verifications refund on `verification.failed` up to a per-call cap. The full breakdown by payment mode is at [`/pricing`](/pricing). ## What's verification? A one-call API for checking that someone is who they claim to be. POST to `/v1/verifications`, we return a hosted URL the customer's end-user opens to upload their ID and selfie. We webhook you the result. **$0.10 per check.** ## How is verification different from ID.me or Entrust? Same underlying ID-check capability; different pricing model. ID.me and Entrust charge $1-$2 per call because they bundle the check with liability coverage, AML screening, KYC pipelines, anti-fraud scoring, and enterprise compliance reporting. Most developers don't need that whole bundle — they need the ID check. We sell just that, at the cost of running it. If you need the full bundle (you're a bank, an exchange, a regulated entity that needs liability transfer), the incumbents are still your answer. If you just need "is this person who they say they are," we're 10-20x cheaper. ## Do I need a separate account for envelopes and verification? No. One verafirma account, one API key, one balance, both products. Use whichever calls you need; the balance draws down at $0.10 per call regardless of which product. Same on the x402 path — the wallet that signs the payment authorization is the identity across both products. ## 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. Two products, same shape — each is one HTTPS request. **Send a contract for signature: `POST /v1/envelopes`.** 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. **$0.10 per envelope.** **Verify an ID: `POST /v1/verifications`.** JSON body with `{ type }`; same `PAYMENT-SIGNATURE` header shape. The response is a hosted URL the end-user opens to upload their ID and selfie. We webhook you the result. **$0.10 per verification.** No signup is required for the per-call path on either product; the wallet that signs the payment is the identity for any later history reads. Same wallet → same balance → same history across both products. **Pricing:** $0.10 per call, both products. Pay per call — no minimum, no subscription. Envelopes refund automatically on terminal failure (`REJECTED` / `EXPIRED` / `CANCELLED`) up to a per-envelope cap; verifications refund on `verification.failed` up to `verafirma.verification.refund_cap_per_call` (default 1). **Wire format authority:** the x402 payment shape is defined by the protocol document at `relaystation/docs/protocols/x402.md`. The two creation surfaces are 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 V1 tools wrap the REST handlers for both products: `verafirma.create_signing_request`, `verafirma.get_envelope_status`, `verafirma.list_envelopes`, `verafirma.register_webhook` (envelope surface), plus the verification handlers as they roll into MCP. The authoritative list is the `tools/list` response. **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. - The two products share the SAME billing rails. There is no per-product entitlement gate; every authed customer can call both `/v1/envelopes` and `/v1/verifications`. x402 callers were never gated. - `/health` is for ops, not customers. The OpenAPI surface intentionally hides it from customer-facing renders. **Anti-lock-in posture:** Verafirma hosts open-source software ourselves and charges what it costs to run plus a markup. When your volume grows past the point where running it yourself makes sense, ask us — we'll point you at the upstream we use so you can replace us cleanly. No proprietary SDK to throw away; the on-the-wire contract is standard HTTPS + x402. 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 in one line **$0.10 per call** — for both envelope signing and ID verification. No minimum, no commitment, no monthly retainer for usage you didn't make. | Product | Verafirma | Incumbent | Cheaper by | |---|---|---|---| | Send a contract for signature | **$0.10/envelope** | DocuSign ~$3/envelope | ~30x | | Verify an ID | **$0.10/verification** | ID.me / Entrust $1-$2 | ~10-20x | We can charge this because we host the underlying open-source software ourselves on cheap infrastructure and pass the cost to you with a markup. Not pass-through SaaS licensing. Not enterprise bundle pricing. Pay for what it costs us to run + our margin. ## How you pay Three options: - **Pay per call with x402** — sign an EIP-3009 USDC authorization off-chain, attach it as a `PAYMENT-SIGNATURE` header, send the call. No account, no signup, no balance management. The wallet that signs IS the identity for any later history reads. The cleanest path if you're crypto-native. - **Pre-funded balance via Stripe** — sign up at `app.verafirma.com` with Google or GitHub, top up $5 (or any amount), mint a `vf_live_*` key. We draw down at $0.10/call. You only ever pay for what you use; nothing recurring, no account-maintenance charge. When your balance hits zero you top up again or you don't — your account stays. - **Pay per call via Stripe (V2 — coming as payment rails mature)** — Stripe/Visa/Mastercard charges don't yet support sub-cent per-transaction fees at the rail level (≥$0.30 + 2.9% per transaction makes $0.10 individual charges economically nonsensical), so today fiat means a pre-funded balance. As payment networks adapt, we'll be among the first to pass on the better rates. Both products share the same balance and the same wallet identity. Use whichever calls you need; the balance draws down at $0.10 per call regardless of product. ## Refunds Envelope charges refund automatically on terminal failure (`REJECTED`, `EXPIRED`, `CANCELLED`) up to a per-envelope cap. Verification charges refund on `verification.failed` up to `verafirma.verification.refund_cap_per_call` (default 1). x402 customers get a USDC transfer back to the signing wallet; API-key / balance customers get a credit on the balance. Refunds happen through the same billing layer that takes the charges; you don't file a ticket. ## What's NOT in the price The $0.10 covers everything you'd expect for the call: PDF upload + storage for envelopes, hosted signing UI, signing-link emails, completed-PDF retention, webhook deliveries (including retries — failed deliveries don't double-bill); ID image upload + the hosted verification UI + the status webhook for verifications. V1 has no premium tier, no rate-limit-tuning fee, no enterprise bundle. 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.