Overview

Verafirma is a high-throughput eSignature API built for developers. Send legally binding documents for signature, track who has signed, and download completed PDFs — all via a clean REST API. It is designed for production use cases like HR onboarding, contract management, insurance enrollment, and any AI agent that needs to collect signatures programmatically.

Base URL: https://api.verafirma.com

Cost: $0.10 per envelope. Pay with a credit card via Stripe, or with ETH on Base via x402 — no account required for crypto payments.

Key concepts

Envelope — One PDF sent to one or more recipients for signature. The fundamental unit. Each envelope costs $0.10.

Recipient — A person who interacts with an envelope. Signers must complete fields and sign. Approvers must approve before signers are notified. CC and Viewer roles receive copies but take no action.

Field — A signature, date, name, text box, or other input placed at a specific location on the PDF. Fields tell recipients exactly where to act.

Template — A saved envelope definition. Create once from a PDF with fields already placed. Reuse forever — just supply recipient emails each time. Essential for any document sent repeatedly at scale.

Webhook — A URL you register. Verafirma calls it whenever something happens to one of your envelopes — signed, completed, rejected, expired. Your app reacts instantly without polling.

Placing fields on a PDF

You have two options:

Coordinate-based. Specify x/y positions and dimensions in the API call. All coordinates are percentages (0–100) of the page width/height. positionX: 0 is the left edge, positionY: 0 is the top.

PDF placeholders (recommended for fixed-format documents). Embed markers like {{signature, r1}} directly in your Word or Google Doc before exporting to PDF. Verafirma detects and places fields automatically on upload — no coordinates needed. r1, r2, etc. map to recipients in the order they appear in the API call.

Employee Signature:  {{signature, r1}}
Employee Name:       {{name, r1}}
Date Signed:         {{date, r1}}

HR Approval:         {{signature, r2}}
HR Name:             {{name, r2}}

Envelope statuses

StatusMeaning
PENDINGCreated but not yet sent to recipients
SENTDistributed — recipients have received signing emails
PARTIALLY_SIGNEDSome but not all recipients have signed
COMPLETEDAll recipients have signed — document is sealed
REJECTEDA recipient actively declined to sign
EXPIREDSigning deadline passed
CANCELLEDVoided by the originator

Field types

TypeDescriptionAuto-filled?
SIGNATUREDrawn, typed, or uploaded signatureNo
INITIALSRecipient's initialsNo
NAMERecipient's full nameYes
EMAILRecipient's email addressYes
DATEDate the document was signedYes
TEXTFree-form text inputNo
NUMBERNumeric inputNo
CHECKBOXOne or more checkboxesNo
RADIOSingle choice from optionsNo
DROPDOWNSingle choice from a dropdown listNo

Recipient roles

RoleDescription
SIGNERMust complete all assigned fields and sign
APPROVERMust approve before signers are notified
CCReceives a copy — no action required
VIEWERCan view during signing — no action required

Authentication

Verafirma supports two authentication methods. Use whichever fits your use case.

API Key — Sign up at app.verafirma.com, purchase prepaid credits with a credit card, and generate an API key. Pass it as a Bearer token. Credits are deducted automatically when envelopes are sent.

Authorization: Bearer vf_live_your_api_key_here

Wallet / x402 — No account or signup required. Connect a MetaMask wallet, sign a challenge to prove ownership, and receive a JWT. Or pay per-envelope using the x402 protocol with a payment proof in the x-payment header. Designed for AI agents that need to operate autonomously.

GET /v1/auth/challenge

Get wallet challenge

Step 1 of wallet authentication. Returns a unique nonce and a message for the wallet to sign. The nonce expires after 10 minutes. Call this first, sign the returned message with MetaMask, then submit the signature to /verify.

Query params: wallet — Ethereum address starting with 0x

curl "https://api.verafirma.com/v1/auth/challenge?wallet=0xabc..."
{
  "nonce": "a3f8c2d1e4b5...",
  "message": "Sign this message to authenticate with Verafirma.\nNonce: a3f8c2d1e4b5...",
  "expiresAt": "2026-04-11T03:00:00.000Z"
}

Error responses: INVALID_WALLET_ADDRESS.

POST /v1/auth/verify

Verify signature and issue JWT

Step 2 of wallet authentication. Submit the wallet address, nonce, and the signature. Returns a JWT valid for 1 hour. Use this as a Bearer token on subsequent requests.

Body: JSON — { walletAddress, nonce, signature }

curl -X POST "https://api.verafirma.com/v1/auth/verify" \
  -H "Content-Type: application/json" \
  -d '{"walletAddress":"0xabc...","nonce":"a3f8c2...","signature":"0x..."}'
{
  "token": "eyJhbGciOiJIUzI1NiJ9...",
  "walletAddress": "0xabc...",
  "expiresIn": 3600,
  "expiresAt": "2026-04-11T03:00:00.000Z"
}

Error responses: MISSING_FIELDS, INVALID_NONCE, CHALLENGE_EXPIRED, NONCE_ALREADY_USED, INVALID_SIGNATURE.

Envelopes

An envelope is a PDF document sent to one or more recipients for signature. When sent, each recipient receives an email with a secure link. If you set signingOrder, recipients sign in sequence — recipient 2 is only notified after recipient 1 signs.

There are two ways to create an envelope. The simple workflow creates and sends in one call. The draft workflow lets you create first, add fields programmatically, then send — useful when field placement depends on data not known at creation time.

POST /v1/envelopes

Create and send envelope

Creates an envelope and immediately sends signing emails to all recipients in one call. This is the standard workflow for most use cases. Supports idempotency via Idempotency-Key — safe to retry without creating duplicates. Use X-Verafirma-Test-Mode: true for free test sends with no real emails.

Leave fields as an empty array and embed {{signature, r1}} markers in your PDF to use placeholder-based field placement instead of coordinates.

Body: multipart/form-datapdf (the file) + payload (JSON string)

curl -X POST "https://api.verafirma.com/v1/envelopes" \
  -H "Authorization: Bearer vf_live_your_key" \
  -H "Idempotency-Key: hire-john-2026-04-11" \
  -F "pdf=@./contract.pdf" \
  -F 'payload={
    "title": "Employment Agreement — John Smith",
    "storageExpiryDays": 90,
    "recipients": [
      {
        "email": "john.smith@example.com",
        "name": "John Smith",
        "role": "SIGNER",
        "signingOrder": 1,
        "fields": [
          { "type": "SIGNATURE", "page": 1, "positionX": 10, "positionY": 80, "width": 30, "height": 8 },
          { "type": "DATE",      "page": 1, "positionX": 60, "positionY": 80, "width": 20, "height": 8 }
        ]
      },
      {
        "email": "hr@example.com",
        "name": "HR Manager",
        "role": "SIGNER",
        "signingOrder": 2,
        "fields": [
          { "type": "SIGNATURE", "page": 1, "positionX": 10, "positionY": 90, "width": 30, "height": 8 }
        ]
      }
    ]
  }'
{
  "envelopeId": "cmntpqhvf0001owr2wu8i4jza",
  "status": "PENDING",
  "title": "Employment Agreement — John Smith",
  "recipients": [
    { "email": "john.smith@example.com", "name": "John Smith", "role": "SIGNER", "signingOrder": 1, "status": "PENDING" },
    { "email": "hr@example.com", "name": "HR Manager", "role": "SIGNER", "signingOrder": 2, "status": "PENDING" }
  ],
  "storageExpiresAt": "2026-07-10T02:25:49.562Z",
  "createdAt": "2026-04-11T02:25:49.563Z"
}

Error responses: MISSING_PDF, MISSING_PAYLOAD, INVALID_JSON, VALIDATION_ERROR, INSUFFICIENT_CREDITS, DOCUMENSO_UNAVAILABLE.

POST /v1/envelopes/draft

Create draft envelope

Creates an envelope without sending it. Use this when you need to add fields programmatically after creation. The draft workflow is: create draft → add fields → send. No emails are sent until you call POST /v1/envelopes/{id}/send.

Body: Same multipart/form-data format as POST /v1/envelopes. Pass empty fields: [] for each recipient.

curl -X POST "https://api.verafirma.com/v1/envelopes/draft" \
  -H "Authorization: Bearer vf_live_your_key" \
  -F "pdf=@./contract.pdf" \
  -F 'payload={
    "title": "Employment Agreement — Jane Doe",
    "recipients": [
      { "email": "jane.doe@example.com", "name": "Jane Doe", "role": "SIGNER", "signingOrder": 1, "fields": [] }
    ],
    "storageExpiryDays": 90
  }'
{
  "envelopeId": "cmnuduv3c0001pf0glp8g0979",
  "status": "DRAFT",
  "title": "Employment Agreement — Jane Doe",
  "documensoNumericId": "21",
  "recipients": [
    { "email": "jane.doe@example.com", "name": "Jane Doe", "role": "SIGNER", "signingOrder": 1 }
  ],
  "createdAt": "2026-04-11T02:25:49.563Z"
}
GET /v1/envelopes/{id}/status

Get live envelope status

The most important status endpoint. Fetches real-time status directly from the signing service. Shows exactly who has signed, who has opened the document, and precise timestamps for each action. Also automatically corrects Verafirma's database if a webhook was missed — so this is always authoritative.

Use this to power a dashboard showing "John signed at 9:15am — HR Manager has not signed yet."

curl "https://api.verafirma.com/v1/envelopes/cmntpqhvf0001owr2wu8i4jza/status" \
  -H "Authorization: Bearer vf_live_your_key"
{
  "envelopeId": "cmntpqhvf0001owr2wu8i4jza",
  "title": "Employment Agreement — John Smith",
  "status": "COMPLETED",
  "createdAt": "2026-04-11T02:25:49.563Z",
  "completedAt": "2026-04-11T02:26:20.440Z",
  "recipients": [
    {
      "name": "John Smith",
      "email": "john.smith@example.com",
      "role": "SIGNER",
      "signingOrder": 1,
      "status": "SIGNED",
      "signingStatus": "SIGNED",
      "readStatus": "OPENED",
      "signedAt": "2026-04-11T02:26:20.424Z",
      "viewedAt": "2026-04-11T02:26:10.413Z",
      "rejectedAt": null,
      "rejectionReason": null
    }
  ]
}

Error responses: ENVELOPE_NOT_FOUND.

GET /v1/envelopes/{id}

Get envelope

Returns envelope details and signing event history from Verafirma's database. For real-time accuracy use /status instead — this endpoint returns cached data.

curl "https://api.verafirma.com/v1/envelopes/cmntpqhvf0001owr2wu8i4jza" \
  -H "Authorization: Bearer vf_live_your_key"
{
  "envelopeId": "cmntpqhvf0001owr2wu8i4jza",
  "status": "COMPLETED",
  "title": "Employment Agreement — John Smith",
  "createdAt": "2026-04-11T02:25:49.563Z",
  "updatedAt": "2026-04-11T02:26:20.440Z",
  "recipients": [
    { "email": "john.smith@example.com", "name": "John Smith", "role": "SIGNER", "status": "SIGNED" }
  ],
  "events": [
    { "eventType": "DOCUMENT_SENT", "recipientEmail": "john.smith@example.com", "occurredAt": "2026-04-11T02:25:50.000Z" },
    { "eventType": "DOCUMENT_COMPLETED", "recipientEmail": "john.smith@example.com", "occurredAt": "2026-04-11T02:26:20.000Z" }
  ]
}

Error responses: ENVELOPE_NOT_FOUND.

GET /v1/envelopes

List envelopes

Returns a paginated list of envelopes. Filter by status to find envelopes that need chasing, or by wallet address to retrieve all envelopes for a specific originator.

Query params: status, wallet, limit (max 100, default 20), offset

curl "https://api.verafirma.com/v1/envelopes?status=PARTIALLY_SIGNED&limit=20" \
  -H "Authorization: Bearer vf_live_your_key"
{
  "data": [
    {
      "envelopeId": "cmntpqhvf0001owr2wu8i4jza",
      "status": "PARTIALLY_SIGNED",
      "title": "Employment Agreement — John Smith",
      "createdAt": "2026-04-11T02:25:49.563Z",
      "recipients": [
        { "email": "john.smith@example.com", "status": "SIGNED" },
        { "email": "hr@example.com", "status": "PENDING" }
      ]
    }
  ],
  "total": 1,
  "limit": 20,
  "offset": 0
}
DELETE /v1/envelopes/{id}

Delete envelope

Voids and cancels an envelope. Unsigned recipients can no longer sign. Completed envelopes cannot be deleted. Use this if you sent to the wrong recipient or need to start over.

curl -X DELETE "https://api.verafirma.com/v1/envelopes/cmntpqhvf0001owr2wu8i4jza" \
  -H "Authorization: Bearer vf_live_your_key"
{ "success": true }

Error responses: ENVELOPE_NOT_FOUND, CANNOT_DELETE_COMPLETED.

POST /v1/envelopes/{id}/resend

Resend to unsigned recipients

Sends a reminder email to everyone who hasn't signed yet. Use this to chase signers automatically without voiding and recreating the envelope — the original audit trail is preserved.

curl -X POST "https://api.verafirma.com/v1/envelopes/cmntpqhvf0001owr2wu8i4jza/resend" \
  -H "Authorization: Bearer vf_live_your_key"
{
  "success": true,
  "resentTo": [
    { "name": "HR Manager", "email": "hr@example.com" }
  ]
}

Error responses: ENVELOPE_NOT_FOUND, ENVELOPE_ALREADY_COMPLETE, ENVELOPE_CANCELLED, ALL_SIGNED.

GET /v1/envelopes/{id}/download

Download signed PDF

Streams the completed signed PDF. Only available once all recipients have signed (status: COMPLETED). Download and store in your own system — Verafirma storage expires per your storageExpiryDays setting. Pass ?version=original for the unsigned original.

Query params: versionsigned (default) or original

curl -o signed-agreement.pdf \
  "https://api.verafirma.com/v1/envelopes/cmntpqhvf0001owr2wu8i4jza/download" \
  -H "Authorization: Bearer vf_live_your_key"

Returns: application/pdf binary stream.

Error responses: ENVELOPE_NOT_FOUND, ENVELOPE_NOT_COMPLETE, DOWNLOAD_FAILED.

POST /v1/envelopes/{id}/send

Send a draft envelope

The final step of the draft workflow. After creating a draft and adding fields, call this to send to all recipients. You can optionally override the email subject and message at send time.

Body: JSON — optional meta with subject, message, redirectUrl

curl -X POST "https://api.verafirma.com/v1/envelopes/cmnuduv3c0001pf0glp8g0979/send" \
  -H "Authorization: Bearer vf_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{"meta":{"subject":"Your offer letter is ready","message":"Please sign within 5 business days."}}'
{
  "envelopeId": "cmnuduv3c0001pf0glp8g0979",
  "status": "SENT",
  "recipients": [
    { "name": "Jane Doe", "email": "jane.doe@example.com", "signingUrl": "https://sign.verafirma.com/sign/abc123" }
  ]
}

Error responses: ENVELOPE_NOT_FOUND, ALREADY_COMPLETED, ENVELOPE_CANCELLED, SEND_FAILED.

Fields

Fields define where recipients need to sign, date, or enter information on a PDF. In the simple workflow, fields are specified at envelope creation time. In the draft workflow, fields are added after creation but before sending. Fields cannot be changed after an envelope has been sent.

Each field is assigned to a specific recipient by email address and placed at a percentage-based x/y position on a specific page.

POST /v1/envelopes/{id}/fields

Add fields to a draft envelope

Adds one or more fields to a draft envelope before it is sent. Specify which recipient each field belongs to by their email address — Verafirma looks up their internal ID automatically.

Body: JSON with a fields array

curl -X POST "https://api.verafirma.com/v1/envelopes/cmnuduv3c0001pf0glp8g0979/fields" \
  -H "Authorization: Bearer vf_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "fields": [
      { "type": "SIGNATURE", "recipientEmail": "jane.doe@example.com", "page": 1, "positionX": 10, "positionY": 80, "width": 30, "height": 8 },
      { "type": "DATE",      "recipientEmail": "jane.doe@example.com", "page": 1, "positionX": 60, "positionY": 80, "width": 20, "height": 8 }
    ]
  }'
{
  "fields": [
    { "fieldId": 101, "type": "SIGNATURE", "page": 1, "positionX": 10, "positionY": 80, "width": 30, "height": 8 },
    { "fieldId": 102, "type": "DATE", "page": 1, "positionX": 60, "positionY": 80, "width": 20, "height": 8 }
  ]
}

Error responses: ENVELOPE_NOT_FOUND, RECIPIENT_NOT_FOUND, FIELD_CREATION_FAILED.

DELETE /v1/envelopes/{id}/fields/{fid}

Remove a field

Removes a field from a draft envelope. The fid is the numeric field ID returned when the field was created. Only works before the envelope is sent.

curl -X DELETE "https://api.verafirma.com/v1/envelopes/cmnuduv3c0001pf0glp8g0979/fields/101" \
  -H "Authorization: Bearer vf_live_your_key"
{ "success": true }

Error responses: ENVELOPE_NOT_FOUND, INVALID_FIELD_ID, DELETE_FAILED.

Templates

Templates are how you achieve high throughput. Instead of re-uploading your health insurance form and re-specifying field positions for every new employee, you create a template once and reuse it forever. Each use just needs the recipient's name and email — the PDF and all field placements are already saved.

Recommended workflow

1. Embed {{signature, r1}} markers in your Word or Google Doc, then export to PDF.

2. Upload via POST /v1/templates — fields are detected automatically. The response shows which fields were found so you can verify before using in production.

3. Call POST /v1/envelopes/from-template with just recipient emails whenever you need to send. The template handles everything else.

POST /v1/templates

Create template

Uploads a PDF and creates a reusable template. If the PDF contains {{placeholder}} markers, fields are detected and placed automatically. The response includes all detected fields — no second call needed to verify. The detectedFromPlaceholders flag tells you whether auto-detection ran.

Body: multipart/form-datafile (PDF) + payload (JSON string)

curl -X POST "https://api.verafirma.com/v1/templates" \
  -H "Authorization: Bearer vf_live_your_key" \
  -F "file=@./health-insurance-form.pdf" \
  -F 'payload={
    "title": "Health Insurance Enrollment Form",
    "recipients": [
      { "email": "employee@example.com", "name": "Employee", "role": "SIGNER", "signingOrder": 1 },
      { "email": "hr@example.com",       "name": "HR Manager", "role": "SIGNER", "signingOrder": 2 }
    ]
  }'
{
  "templateId": "42",
  "title": "Health Insurance Enrollment Form",
  "recipients": [
    { "id": 1, "email": "employee@example.com", "name": "Employee", "role": "SIGNER", "signingOrder": 1 },
    { "id": 2, "email": "hr@example.com", "name": "HR Manager", "role": "SIGNER", "signingOrder": 2 }
  ],
  "fields": [
    { "fieldId": 101, "type": "SIGNATURE", "page": 1, "positionX": 10, "positionY": 75, "width": 30, "height": 8, "recipientId": 1 }
  ],
  "detectedFromPlaceholders": true,
  "createdAt": "2026-04-11T02:25:49.563Z"
}

Error responses: UNAUTHORIZED, MISSING_FILE, MISSING_PAYLOAD, VALIDATION_ERROR, DOCUMENSO_UNAVAILABLE.

GET /v1/templates

List templates

Returns all templates for your account or wallet.

Query params: page (default 1), perPage (max 100, default 20)

curl "https://api.verafirma.com/v1/templates" \
  -H "Authorization: Bearer vf_live_your_key"
{
  "data": [{ "templateId": "42", "title": "Health Insurance Enrollment Form", "type": "PRIVATE", "createdAt": "2026-04-11T02:25:49.563Z" }],
  "total": 1, "page": 1, "perPage": 20
}
GET /v1/templates/{id}

Get template

Returns full template detail including all recipients and field positions. Use this to verify placeholder detection worked correctly after template creation.

curl "https://api.verafirma.com/v1/templates/42" \
  -H "Authorization: Bearer vf_live_your_key"

Returns the full template object with recipients and fields arrays.

Error responses: UNAUTHORIZED, INVALID_ID, TEMPLATE_NOT_FOUND.

PUT /v1/templates/{id}

Update template

Updates a template's title or default email settings. All fields are optional.

curl -X PUT "https://api.verafirma.com/v1/templates/42" \
  -H "Authorization: Bearer vf_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{"title":"Health Insurance Form v2","meta":{"subject":"Sign your health insurance form"}}'
{ "templateId": "42", "title": "Health Insurance Form v2", "updatedAt": "2026-04-11T03:00:00.000Z" }

Error responses: UNAUTHORIZED, INVALID_ID, UPDATE_FAILED.

DELETE /v1/templates/{id}

Delete template

Permanently deletes a template. Envelopes already sent from this template are not affected.

curl -X DELETE "https://api.verafirma.com/v1/templates/42" \
  -H "Authorization: Bearer vf_live_your_key"
{ "success": true }
POST /v1/envelopes/from-template

Create envelope from template

The core high-throughput call. Uses a saved template to create and send an envelope. Supply only recipient details — the PDF and all field positions come from the template. Map real people to template recipient slots using the id values returned when the template was created.

curl -X POST "https://api.verafirma.com/v1/envelopes/from-template" \
  -H "Authorization: Bearer vf_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "templateId": "42",
    "title": "Health Insurance — Jane Doe",
    "sendImmediately": true,
    "storageExpiryDays": 90,
    "recipients": [
      { "id": 1, "email": "jane.doe@example.com", "name": "Jane Doe" },
      { "id": 2, "email": "hr@example.com",       "name": "HR Manager" }
    ],
    "meta": {
      "subject": "Action required: sign your health insurance form",
      "message": "Please complete this within 5 business days."
    }
  }'
{
  "envelopeId": "cmo1abc2def3ghi4jkl5",
  "status": "SENT",
  "title": "Health Insurance — Jane Doe",
  "recipients": [
    { "name": "Jane Doe", "email": "jane.doe@example.com", "role": "SIGNER", "signingUrl": "https://sign.verafirma.com/sign/abc123" }
  ],
  "createdAt": "2026-04-11T02:25:49.563Z"
}

Error responses: UNAUTHORIZED, VALIDATION_ERROR, INVALID_TEMPLATE_ID, DOCUMENSO_UNAVAILABLE.

Webhooks

Webhooks let your application react to signing events in real time. Register a URL and Verafirma calls it automatically whenever something happens to one of your envelopes. When an employee signs their offer letter, your HR app webhook fires instantly — trigger the next onboarding step, notify a manager, or update your database without polling.

Webhooks work for both API key accounts and wallet-based users. Register once and receive events for all future envelopes.

Verifying webhook signatures

Every webhook request includes an X-Verafirma-Signature header with an HMAC-SHA256 signature. Always verify this before processing — it proves the call came from Verafirma.

import { createHmac } from 'crypto'

function verifyWebhook(rawBody: string, signature: string, secret: string): boolean {
  const expected = 'sha256=' + createHmac('sha256', secret).update(rawBody).digest('hex')
  return expected === signature
}

app.post('/webhooks/verafirma', (req, res) => {
  const sig = req.headers['x-verafirma-signature'] as string
  if (!verifyWebhook(req.rawBody, sig, process.env.WEBHOOK_SECRET!)) {
    return res.status(401).send('Invalid signature')
  }
  const { event, envelopeId, data } = req.body
  res.status(200).send('ok')
})

Events

EventWhen it fires
envelope.sentEnvelope distributed to recipients
envelope.viewedA recipient opened the document
envelope.signedOne recipient signed (others may still be pending)
envelope.completedAll recipients signed — document sealed and downloadable
envelope.rejectedA recipient declined to sign
envelope.expiredSigning deadline passed
envelope.cancelledOriginator voided the envelope

Payload format

{
  "event": "envelope.signed",
  "envelopeId": "cmntpqhvf0001owr2wu8i4jza",
  "timestamp": "2026-04-11T02:26:20.424Z",
  "data": {
    "envelope": { "title": "Employment Agreement", "status": "PARTIALLY_SIGNED", "createdAt": "2026-04-11T02:25:49.563Z" },
    "recipient": { "name": "John Smith", "email": "john.smith@example.com", "role": "SIGNER", "signedAt": "2026-04-11T02:26:20.424Z" }
  }
}

Retry policy

Failed deliveries retry automatically: immediately, then after 1 min, 5 min, 30 min, and 2 hours. After 5 failed attempts the delivery is marked FAILED. Use the deliveries endpoint to inspect failures.

POST /v1/webhooks

Register webhook

Registers a URL to receive event notifications. Choose which events you want. The secret is shown only once — store it immediately. A secure secret is generated if you don't provide one.

curl -X POST "https://api.verafirma.com/v1/webhooks" \
  -H "Authorization: Bearer vf_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-app.com/webhooks/verafirma",
    "events": ["envelope.signed", "envelope.completed", "envelope.rejected"],
    "description": "HR onboarding — production"
  }'
{
  "webhookId": "clwh1abc2def3ghi",
  "url": "https://your-app.com/webhooks/verafirma",
  "events": ["envelope.signed", "envelope.completed", "envelope.rejected"],
  "active": true,
  "secret": "a1b2c3d4... (shown once only — store immediately)",
  "createdAt": "2026-04-11T02:25:49.563Z"
}

Error responses: UNAUTHORIZED, VALIDATION_ERROR.

GET /v1/webhooks

List webhooks

Returns all registered webhooks for your account or wallet.

curl "https://api.verafirma.com/v1/webhooks" \
  -H "Authorization: Bearer vf_live_your_key"
{
  "data": [{ "webhookId": "clwh1abc2def3ghi", "url": "https://your-app.com/webhooks/verafirma", "events": ["envelope.completed"], "active": true }],
  "total": 1
}
GET /v1/webhooks/{id}

Get webhook

Returns details for a single webhook. The secret is not returned after creation.

curl "https://api.verafirma.com/v1/webhooks/clwh1abc2def3ghi" \
  -H "Authorization: Bearer vf_live_your_key"

Error responses: UNAUTHORIZED, WEBHOOK_NOT_FOUND.

PUT /v1/webhooks/{id}

Update webhook

Updates a webhook's URL, events, description, or active status. All fields are optional. Use active: false to pause without deleting.

curl -X PUT "https://api.verafirma.com/v1/webhooks/clwh1abc2def3ghi" \
  -H "Authorization: Bearer vf_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{"active": false}'
{ "webhookId": "clwh1abc2def3ghi", "active": false, "updatedAt": "2026-04-11T03:00:00.000Z" }

Error responses: UNAUTHORIZED, WEBHOOK_NOT_FOUND, VALIDATION_ERROR.

DELETE /v1/webhooks/{id}

Delete webhook

Permanently removes a webhook. No further events will be delivered to its URL.

curl -X DELETE "https://api.verafirma.com/v1/webhooks/clwh1abc2def3ghi" \
  -H "Authorization: Bearer vf_live_your_key"
{ "success": true }
GET /v1/webhooks/{id}/deliveries

Webhook delivery history

Returns the delivery log — essential for debugging missed events in production. Shows each delivery attempt, the HTTP response code your server returned, and when the next retry is scheduled.

Query params: page, perPage (max 100)

curl "https://api.verafirma.com/v1/webhooks/clwh1abc2def3ghi/deliveries" \
  -H "Authorization: Bearer vf_live_your_key"
{
  "data": [
    { "deliveryId": "cldlv1abc2def", "event": "envelope.completed", "status": "SUCCESS", "attempts": 1, "responseCode": 200, "lastAttemptAt": "2026-04-11T02:26:21.000Z", "nextRetryAt": null }
  ],
  "total": 1, "page": 1, "perPage": 20
}

Delivery statuses: PENDING, SUCCESS, FAILED.

Account

Account endpoints return information about the authenticated developer — credit balance, usage history, and envelope history. Both API key holders and wallet-based users have full access to their transaction history.

GET /v1/account

Account info

Returns your account details and current credit balance. API key holders see email, API key prefix, and credit balance. Wallet users see their wallet address and total spending.

curl "https://api.verafirma.com/v1/account" \
  -H "Authorization: Bearer vf_live_your_key"
{
  "accountId": "cmnrqkgwd00023fy5ntu74gzp",
  "email": "you@example.com",
  "name": "Jane Developer",
  "apiKeyPrefix": "vf_live_d7d8",
  "creditsCents": 2000,
  "creditsFormatted": "$20.00",
  "sandboxUsed": 2,
  "sandboxLimit": 5,
  "createdAt": "2026-04-09T17:13:35.629Z"
}

Error responses: UNAUTHORIZED, TOKEN_INVALID.

GET /v1/account/usage

Usage history

Returns your billing ledger — every credit deduction or charge, with the associated envelope title. Useful for reconciliation and usage reporting. Most recent entries first.

Query params: limit (max 100, default 20), offset

curl "https://api.verafirma.com/v1/account/usage" \
  -H "Authorization: Bearer vf_live_your_key"
{
  "data": [
    {
      "id": "cmntpqhyh0003owr2vtx7vawj",
      "direction": "debit",
      "amountCents": 10,
      "amountFormatted": "$0.10",
      "description": "Envelope creation — API key payment",
      "envelopeId": "cmntpqhvf0001owr2wu8i4jza",
      "envelopeTitle": "Employment Agreement — John Smith",
      "txHash": null,
      "createdAt": "2026-04-11T02:25:49.669Z"
    }
  ],
  "total": 5, "limit": 20, "offset": 0
}

Error responses: UNAUTHORIZED, TOKEN_INVALID.

GET /v1/account/summary

Account summary

Returns aggregate spending totals, envelope counts by status, daily and monthly spending breakdowns, and the 10 most recently active pending envelopes. Designed for building dashboard views. Requires wallet JWT authentication.

curl "https://api.verafirma.com/v1/account/summary" \
  -H "Authorization: Bearer YOUR_WALLET_JWT"
{
  "walletAddress": "0xabc...",
  "summary": { "totalEnvelopes": 42, "totalSpentCents": 420, "totalSpentFormatted": "$4.20", "byStatus": { "COMPLETED": 38, "PENDING": 4 } },
  "spending": { "byDay": [{ "date": "2026-04-11", "cents": 50 }], "byMonth": [{ "month": "2026-04", "cents": 420 }] },
  "pendingEnvelopes": []
}

Error responses: UNAUTHORIZED, TOKEN_INVALID.

GET /v1/account/envelopes

Account envelope history

Returns all envelopes for the authenticated account or wallet in reverse chronological order. Supports status filtering and pagination.

Query params: status, limit (max 100, default 20), offset

curl "https://api.verafirma.com/v1/account/envelopes?status=COMPLETED" \
  -H "Authorization: Bearer vf_live_your_key"
{
  "data": [{ "id": "cmntpqhvf0001owr2wu8i4jza", "status": "COMPLETED", "title": "Employment Agreement — John Smith", "createdAt": "2026-04-11T02:25:49.563Z" }],
  "total": 38, "limit": 20, "offset": 0
}

Error responses: UNAUTHORIZED, TOKEN_INVALID.

Discovery

Machine-readable endpoints for tooling, AI agents, and API clients.

GET /openapi.json

OpenAPI schema

Machine-readable OpenAPI 3.1 schema for all REST routes. Import into Postman, Insomnia, or use to generate SDKs.

curl https://api.verafirma.com/openapi.json
GET /llms.txt

LLM-readable service description

Plain text summary of Verafirma's capabilities for AI model discovery and tool context.

curl https://api.verafirma.com/llms.txt
GET /.well-known/mcp.json

MCP discovery

Advertises Verafirma as an MCP-compatible tool server. Claude and other MCP agents use this to discover and call Verafirma automatically without manual configuration.

curl https://api.verafirma.com/.well-known/mcp.json
{
  "name": "verafirma",
  "version": "1.0.0",
  "description": "eSignature API for developers and AI agents",
  "mcpUrl": "https://api.verafirma.com/mcp",
  "pricing": { "per_envelope_cents": 10, "currency": "USD" }
}