# Quickstart

_Send a contract for signature in five lines. PDF + recipient + payment, one HTTPS call._

_Last updated: 2026-05-07_

---

# 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=[component]]}'
```

Response (`201`):

```json
[component]
  ],
  "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=[component]'
```

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..."
# → [component]

# 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 '[component]'
# → [component]

# 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).
