Verafirma logo Verafirma

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

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):

{
  "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 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), 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). 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:

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:

# 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.
  • Discover via MCP: an MCP-compatible client can hit the /mcp endpoint and call tools like verafirma.create_signing_request directly.
  • Read the OpenAPI spec: /openapi.json 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.