Verafirma logo Verafirma

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:

{
  "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:

    {
      "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.