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}/statusreturns the live state from Documenso (the underlying signing engine), reconciling any cached state if a webhook was missed. - Subscribe to webhook events:
POST /v1/webhooksregisters a callback URL for envelope state transitions. See/concepts/webhooks. - Discover via MCP: an MCP-compatible client can hit the
/mcpendpoint and call tools likeverafirma.create_signing_requestdirectly. - Read the OpenAPI spec:
/openapi.jsonis 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 carriespriceMicros,balanceMicros, and atopUpUrlfor 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.