Abundera Sign API

Create the most provable e-signatures with cryptographic proof. Hash-chained audit trails, RFC 3161 timestamps, GitHub evidence anchoring, signer evidence scoring, signing ceremony proof, AI contract summaries, webcam identity capture, browser GPS, court-ready declarations, and public document verification, all through a simple REST API.

Base URL:https://sign.abundera.ai

Quick Start

Send a document for signature in one API call:

POST /api/v1/envelopes
Authorization: Bearer <jwt_token>

{
  "template_id": "nda-simple",
  "signers": [
    { "email": "signer@contoso.com", "name": "Jane Doe", "role": "recipient" }
  ],
  "fields": {
    "disclosing_party_name": "Acme Corp",
    "effective_date": "2026-03-15"
  }
}

The signer receives an email with a secure link. When they sign, you get an HMAC-signed webhook. The completed PDF includes a Certificate of Completion, cryptographic proof page with hash-chain visualization, signer evidence scores, RFC 3161 timestamp, and a tamper-evident seal on every page.

API Resources

Interactive API Reference →OpenAPI YAMLOpenAPI JSONPostman CollectioncURL Examples

Well-known discovery

Auto-discovery endpoints under /.well-known/ (RFC 8615):

/.well-known/ indexCapabilitiesFamily API catalog →Trust posture →

Authentication

The API supports two authentication methods for authenticated endpoints: JWT Bearer tokens and product-scoped API keys.

JWT Tokens

Obtain a JWT from the Abundera auth service. JWTs are verified against the JWKS endpoint at https://abundera.ai/v1/auth/jwks.

Authorization: Bearer eyJhbGciOiJSUzI1NiIs...

API Keys

API keys use the abnd_sign_* prefix and are passed as Bearer tokens. Create and manage API keys in the Abundera dashboard.

Authorization: Bearer abnd_sign_xxx...

API keys are product-scoped, each key is tied to Abundera Sign specifically. Keys are validated server-side against abundera.ai/v1/auth/validate-key and cached in KV for 60 seconds. Authenticated users (JWT or API key) receive 2× the per-IP rate limits. API keys are available on all tiers.

Scopes and presets

Every key carries a scope list; each endpoint requires the matching scope or the request returns 403. Scopes follow resource:action (write implies read; envelopes:manage implies write). Sensitive scopes — evidence:export, audit:export, branding:write, notarizations:create, declarations:write, envelopes:manage — are never implied and must be granted directly. The full catalog is published at /.well-known/abundera-capabilities.json.

Four presets cover the common integration shapes and expand to a plain scope list when the key is created:

PresetScopesUse it for
read_onlyenvelopes:read templates:read signers:read contacts:readReporting and dashboards
send_and_trackenvelopes:write templates:read contacts:read evidence:exportSend envelopes, track status, download signed documents
automationenvelopes:write templates:write signers:write webhooks:write contacts:read evidence:exportHeadless pipelines and embedded signing
full_accesssign:full_accessAccount-owner keys only; grants every scope including sensitive ones

Token Refresh

Access tokens are short-lived. Use the refresh endpoint to obtain new tokens without re-authenticating:

POST/api/v1/auth/refresh

Exchange a refresh token for a new access token and refresh token pair.

Request Body
FieldTypeDescription
refresh_token requiredstringThe refresh token from your last authentication
Response
{
  "ok": true,
  "data": {
    "access_token": "eyJhbGciOi...",
    "refresh_token": "rt_abc123...",
    "expires_in": 900,
    "token_type": "Bearer"
  },
  "request_id": "req_…"
}

Plan Tiers

Some endpoints and features require a specific plan tier. If your plan doesn't include access, you'll receive a 403 response with an upgrade_url.

PlanTier LevelKey Features
StarterstarterCore API, webhooks, hash-chained audit trails, RFC 3161 timestamps, Certificate of Completion, identity scoring, ceremony proof, access codes, auto reminders, public verification, bot detection
Professionalprofessional+ Bulk send, signing order, SMS OTP, AI summaries, geo-lock, identity photo capture, browser GPS, comments, court-ready declarations, custom email branding
Businessbusiness+ Template CRUD, audit export, VPN/proxy blocking, custom retention (99yr), white label
Tier in JWT: Your Sign plan tier is included in the JWT payload under products.sign (per-product tiers, since each Abundera product has an independent subscription). The API checks this automatically, no extra headers needed. If products.sign is absent, the API falls back to Starter.

Auth Types

TypeUsed ByDescription
JWTEnvelope managementBearer token in Authorization header
API KeyEnvelope managementabnd_sign_* Bearer token in Authorization header
TokenSigning flowSigning token in request body or URL
PublicVerificationNo authentication required
SecretCron jobsAuthorization: Bearer <CRON_SECRET> header

Signing IDs

Signing request emails use opaque sgn_xxx IDs instead of raw hex tokens for improved security. These IDs are used in signing URLs:

https://sign.abundera.ai/sign/?token=sgn_xxxxxxxxxxxxxxxx
PropertyDetails
Formatsgn_ prefix + 16 alphanumeric characters (20 chars total)
Entropy~4.7×1028 possible values
LifetimeOne-time use, automatically revoked after signer completes signing
Storageverification_ids table with owner_type = 'signing'
Backward compatibilityLegacy raw hex tokens are still accepted

Verification IDs

Public documents use opaque vrf_xxx IDs instead of raw UUIDs. QR codes embedded in sealed PDFs link to the verification page using these IDs:

https://sign.abundera.ai/verify/?id=vrf_xxxxxxxxxxxxxxxx

The /api/v1/verify endpoint supports three lookup methods:

MethodParameterAuth Required
vrf_xxx ID?id=vrf_xxxNone (public)
Raw UUID?id=e3f8a1b2-...JWT required
SHA-256 hash?hash=a1b2c3...None (self-gating, requires knowledge of the hash)
Permanent IDs: Unlike signing IDs, verification IDs are never revoked, they provide a permanent public link to the document's verification record.

Errors

All errors return JSON with an error field. HTTP status codes follow standard conventions:

CodeMeaning
400Invalid request, check required fields
401Missing or invalid JWT
403Forbidden, not the envelope sender, or gate required
404Resource not found
409Conflict, already signed/declined
410Gone, document voided or expired
429Rate limited or tier limit exceeded
500Internal server error
// Every JSON response uses the same envelope.
// Success (2xx):
{
  "ok": true,
  "data": { /* the resource */ },
  "request_id": "req_…"
}

// Error (4xx/5xx):
{
  "ok": false,
  "error": {
    "code": "validation_error",
    "message": "envelope_id is required"
  },
  "request_id": "req_…"
}

The error.code is a stable machine-readable value (validation_error, unauthorized, forbidden, not_found, conflict, gone, rate_limited, payment_required, internal_error); branch on it rather than the human-readable message. The request_id (also returned as the X-Request-ID header) is present on every response. Quote it in support requests. Binary downloads (signed PDF, evidence, timestamp) return the file directly, not the envelope.

Rate Limiting

API requests are rate-limited per IP address. Authenticated requests (JWT or API key) automatically receive 2× the per-IP limits using the user's identity as the rate-limit key. When rate-limited, you'll receive a 429 response.

EndpointLimit
General (all endpoints)30/min per IP (60/min for authenticated users)
POST /api/v1/envelopes10/min
POST /api/v1/orgs5/min
POST /api/v1/demo-envelope1/min
GET /api/v1/document-summary5/min
GET /api/v1/verify10/min
GET /api/v1/document-pdf3/min
Tip: Authenticated users (JWT or API key) get 2× the listed limits. For example, POST /api/v1/envelopes allows 20/min for authenticated users.

Monthly API Key Allowance

On top of the burst limits above, each API key has a monthly call allowance tied to your plan. Every authenticated call with the key counts toward it; the counter resets at the start of each calendar month (UTC). When the allowance is exhausted you'll receive a 429 with Retry-After set to the next reset.

PlanCalls per key per month
Starter10,000
Professional100,000
Business1,000,000

Every API-key response carries the allowance state in the standard rate-limit headers: the "api_key" entry in RateLimit-Policy / RateLimit shows the cap, remaining calls, and seconds to reset, and X-RateLimit-Limit / X-RateLimit-Remaining / X-RateLimit-Reset reflect the tightest active bucket. Need a higher allowance? Contact support.

Spend Controls

Charges beyond your subscription follow two models, and each has its own guard:

Read or change the budget with GET/PUT /api/v1/billing/budget. Set it to 0 for strict prepaid mode (no metered overage at all), or null to return to the plan default.

Both guards alert before they bite: you get an email at 80% of the overage budget, and another when prepaid verification credits run low, so signers are never blocked mid-ceremony by surprise.

What Gets Hashed — and How to Verify It Yourself

Every sealed document ships with an evidence package designed so that anyone can independently recreate every hash and verify every signature — no trust in us required. The structure:

Diagram of the six-layer hash structure: artifact hashes feed the evidence manifest, whose snapshot hash is externally anchored; the final certified PDF hash is what the verify page matches

To recreate it yourself: download any artifact from the evidence package and hash it with SHA-256 (and SHA-512) — it must equal the entry in evidence-manifest.json. Hash the manifest snapshot and compare it against the external anchors (the GitHub/GitLab commits, the Sigstore Rekor entry, both RFC 3161 timestamp tokens, and the OpenTimestamps Bitcoin attestation all carry the same manifest_hash). Hash the final PDF and check it with GET /api/v1/verify?hash= — the same check the public verify page runs. The audit trail is internally hash-chained, so any inserted, removed, or altered event breaks the chain. The Personal Document Seal's rolling codes are an independent RFC 6238 (TOTP) layer verified at /seal/. The Quick Start's “Verify a signed document” operation shows this end-to-end, with copy-paste snippets in every major language.

The manifest lists every artifact, so the verify loop covers the full history of the agreement, not just the final page — there is no fixed set to remember:

Page and Line Citations

The document a signer reads in the browser and the sealed PDF break pages at the same points, verified template-by-template across Chrome, Safari, and Firefox. A citation like "page 3, paragraph 2" refers to the same content whether someone is looking at the signing preview or the certified PDF.

For line-level citations, the evidence package includes line-map.json: a record of where every body line of signed.pdf sits — its line number, page, vertical position, and the source line in original.md it was rendered from. The map is hashed in evidence-manifest.json like every other artifact, so "page 3, line 14" resolves against the sealed document itself, the way line numbers work on pleading paper. Documents created with metadata.line_numbers also print every fifth line number in the margin, in both the signing preview and the PDF.

Webhooks

Abundera Sign sends Standard Webhooks to your callback_url for envelope and signer lifecycle events. Each delivery carries the webhook-id, webhook-timestamp, and webhook-signature headers and a { id, type, timestamp, data } body. Configure per-envelope callbacks or use global webhooks for all envelopes.

POST {callback_url}
Content-Type: application/json
webhook-id: msg_2b1c...
webhook-timestamp: 1717900000
webhook-signature: v1,K5oZ...

{
  "id": "msg_2b1c-...",
  "type": "envelope.completed",
  "timestamp": "2026-03-05T12:00:00Z",
  "data": {
    "envelope_id": "abc-123-...",
    "completed_at": "2026-03-05T12:00:00Z"
  }
}

Webhook Event Types

EventDescription
signer.viewedSigner opened the signing page
signer.signedSigner completed their signature
signer.declinedSigner declined to sign with a reason
signer.delegatedSigner delegated signing to another person
envelope.completedAll signers completed, sealed PDF and evidence package generated
envelope.voidedSender voided the envelope
envelope.expiredEnvelope passed its expiry date without completion
envelope.extendedSigning deadline extended; an expired envelope is revived and active signers are re-notified
payment.completedStripe payment gate completed by signer
clause.flaggedSigner flagged a clause for negotiation
clause.respondedSender responded to a clause negotiation flag
clause.acceptedClause negotiation accepted by both parties
clause.appliedAgreed clause text applied to the document; signatures re-collected
clause.counter_proposedSigner submitted a counter-proposal to a clause response
clause.withdrawnSigner withdrew their own open clause flag
notarization.completedRemote online notarization session completed
notarization.failedRemote online notarization session failed or was cancelled
notarization.expiredRemote online notarization session expired before completion
signer.idv_verifiedSigner passed provider ID verification
signer.idv_failedSigner's ID verification was declined or not approved
signer.kba_failedSigner failed knowledge-based authentication (KBA)
signer.otp_lockedSigner locked out after repeated SMS OTP failures
signer.blockedSigner blocked by a signing gate (geo/VPN, access-code lockout, SMS send cap, KBA/IDV attempt caps, or exhausted verification credits) — payload carries gate and reason
signer.email_bouncedAn email to the signer (invitation, reminder, or completion copy) bounced or drew a spam complaint — payload carries bounce_type and template_alias
envelope.retention_expiringA completed envelope's evidence purges within 30 days (retention boundary warning) — payload carries purge_date
envelope.purgedRetention period completed; the signed PDF, evidence package, and signer data were permanently erased
signer.attachment_uploadedSigner uploaded a supporting attachment
payment.declinedPay-to-sign or overage charge was declined
envelope.expiringEnvelope nearing expiration without completing (sender digest)

Verifying Webhook Signatures

Sign the content {webhook-id}.{webhook-timestamp}.{rawBody} with HMAC-SHA256 using your whsec_ secret (base64-decoded), then compare against each space-delimited token in the webhook-signature header using a constant-time comparison. Reject deliveries whose webhook-timestamp is more than 5 minutes from now, and dedupe on webhook-id.

// Node.js verification example (Standard Webhooks)
const crypto = require('crypto');
const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64');
const signedContent = `${webhookId}.${webhookTimestamp}.${rawBody}`;
const expected = 'v1,' + crypto
  .createHmac('sha256', key)
  .update(signedContent)
  .digest('base64');
const valid = webhookSignature
  .split(' ')
  .some((sig) => sig.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected)));

Testing Webhooks Locally

Your callback_url has to be reachable from the public internet, so http://localhost:3000 will never receive a delivery. Put a tunnel in front of your dev server and point callback_url at the public URL it gives you.

Pair that with a test key and you get the whole loop without sending anything to a real person. Mint one in the dashboard under Settings then API keys; it carries the abnd_sign_test_ prefix. Envelopes created with a test key and test_mode: true skip every email, are not billed, store outside WORM, and auto-void after 24 hours. Because no email is sent, the create response hands you a signing_url for each signer, so you can drive a full signing ceremony yourself and watch the events land.

A production key that sends test_mode: true is rejected with a validation_error rather than quietly sending live. That is deliberate: nobody should discover they were in live mode by getting a call from a real signer.

# 1. Expose your local receiver. cloudflared needs no account:
cloudflared tunnel --url http://localhost:3000
#   → https://your-tunnel.trycloudflare.com
#   ngrok http 3000 works the same way.

# 2. Create a test envelope pointed at the tunnel.
curl -X POST https://sign.abundera.ai/api/v1/envelopes \
  -H "Authorization: Bearer abnd_sign_test_..." \
  -H "Content-Type: application/json" \
  -d '{
    "template_id": "nda-simple",
    "test_mode": true,
    "callback_url": "https://your-tunnel.trycloudflare.com/webhooks/sign",
    "signers": [{ "name": "Dev Tester", "email": "dev@example.com", "role": "recipient" }]
  }'

# 3. The response carries a signing_url per signer, since no email was sent.
#    Open it, sign, and every event above arrives at your tunnel.

Every delivery from a test envelope carries test_mode: true in its payload, so your receiver can tell a rehearsal from the real thing:

{
  "type": "envelope.completed",
  "data": {
    "envelope_id": "abc-123-...",
    "test_mode": true
  }
}

If you only want to see the payload shape and have not written a receiver yet, point callback_url at webhook.site or Svix Play. Both give you a public URL and a live view of what arrived, with nothing to install. Svix Play can also return an error code on demand, which is the quickest way to see our retry behaviour without breaking your own server.

Two things worth knowing before you debug a missing delivery. Reject any delivery whose webhook-timestamp is more than 5 minutes old, which means a tunnel you left running overnight will start refusing replays. And every attempt is recorded in the delivery ledger in the dashboard under Settings then Webhooks, so you can confirm we sent it before you go looking in your own logs.

Embedded Signing

Embed the signing experience directly in your app using an iframe. No redirects, signers complete the flow within your UI.

How it works

  1. Create an envelope with embedded: true, this skips invitation emails and returns signing_url per signer
  2. Embed the signing_url in an iframe in your app
  3. Listen for completion via webhook (signer.signed, envelope.completed) or poll the status endpoint

JavaScript SDK (recommended)

Load https://sign.abundera.ai/js/sdk.js and let it manage the iframe, the origin handshake, and the event callbacks:

<script src="https://sign.abundera.ai/js/sdk.js"></script>

AbunderaSign.open({
  url: signing_url,               // from POST /api/v1/envelopes
  container: '#signing-container', // omit for a modal overlay
  onReady() { /* iframe loaded */ },
  onDocumentLoaded(data) { /* { template_name, signer_name, signer_role, envelope_id } */ },
  onSigned(data) { /* { envelope_id, all_signed, download_url } */ },
  onDeclined(data) { /* { reason } */ },
  onError(data) { /* { message } */ },
});

The SDK appends parent_origin=<your origin> to the signing URL. The signing page pins its frame-ancestors policy to that exact origin and addresses every postMessage to it, and the SDK in turn only accepts messages from the signing iframe's own window and origin. Hand-rolled iframes work too (add &parent_origin= yourself); without it, framing falls back to any HTTPS host, with the unguessable signing token as the access control.

postMessage events

Every message has the shape { type: "abundera:sign", event, data }:

EventDataWhen
ready{}Signing page initialized inside the iframe
document_loadedtemplate_name, signer_name, signer_role, envelope_idDocument fetched and rendered
signedenvelope_id, all_signed, download_urlThis signer completed
declinedreasonSigner declined
errormessageA load-time error surfaced (bad token, expired, voided)
submit_errormessageThe signing submission failed after the signer pressed Finish (validation, gate, or server error) — the host can offer its own retry UI

JavaScript (hand-rolled iframe)

// 1. Create envelope via your backend
const res = await fetch('/your-api/create-signing', { method: 'POST' });
const { signing_url } = await res.json();

// 2. Embed in iframe (parent_origin pins framing + events to YOUR origin)
const iframe = document.createElement('iframe');
iframe.src = signing_url + '&parent_origin=' + encodeURIComponent(location.origin);
iframe.style.cssText = 'width:100%;height:800px;border:none;border-radius:8px';
iframe.allow = 'camera;microphone';  // if using photo/video identity
document.getElementById('signing-container').appendChild(iframe);

React

function SigningEmbed({ signingUrl }) {
  return (
    <iframe
      src={signingUrl}
      style={{ width: '100%', height: '800px', border: 'none', borderRadius: '8px' }}
      allow="camera;microphone"
      title="Sign Document"
    />
  );
}

Backend: Create Embedded Envelope

// Node.js / Python / any HTTP client
const response = await fetch('https://sign.abundera.ai/api/v1/envelopes', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer abnd_sign_...',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    template_id: 'nda-simple',
    embedded: true,               // ← key flag
    signers: [{ name: 'Jane Doe', email: 'jane@contoso.com', role: 'recipient' }],
    fields: { disclosing_party_name: 'Acme Corp' },
  }),
});

const { data: envelope } = await response.json(); // canonical { ok, data, error, request_id }
// envelope.signers[0].signing_url → embed this in your iframe

Security Notes

See code examples and framework guides →

A signing link is a reusable URL that lets anyone sign a template-based document without you sending individual envelopes: share it on a careers page, in a kiosk, or over chat. Each claim creates a fresh envelope for that signer.

Lifecycle

  1. POST /api/v1/signing-links (Professional+) with a template_id, optional pre-filled fields, max_uses (1-100,000), and expiry_days (1-365). The response carries the full URL once — store it; only a hash is kept server-side.
  2. A visitor opens the link, enters their name and email, and claims it. Claims are atomic against max_uses (concurrent claims can never overshoot the cap) and consume your envelope allowance like any other envelope.
  3. The signer gets the standard signing experience and email; you get the standard webhooks and dashboard visibility.

Statuses

StatusMeaning
activeClaimable
exhaustedReached max_uses; create a new link to keep collecting signatures
expiredPast expires_at
deactivatedRevoked from the dashboard or via PUT /api/v1/signing-links

MCP Server (AI Agents)

The Abundera Sign MCP Server lets AI coding assistants interact with the Abundera Sign API through the Model Context Protocol. Works with any MCP-compatible client (Cursor, Windsurf, VS Code, and more). Send documents for signature, check envelope status, manage templates, and review audit trails, all through natural language.

Available Tools

ToolDescription
list_templatesList templates with optional category, tag, or search filters
get_templateGet template details including fields and roles
create_envelopeCreate and send a document for e-signature
list_envelopesList envelopes with status, template, and pagination filters
get_envelopeGet full envelope details including signers and audit counts
void_envelopeVoid (cancel) an envelope and notify unsigned signers
send_reminderSend a reminder to unsigned signers
get_audit_trailGet the hash-chained audit trail for an envelope
check_usageCheck current month's envelope usage and plan limits

Setup

Add to your MCP client configuration (e.g., mcp.json or equivalent):

{
  "mcpServers": {
    "sign-abundera": {
      "command": "node",
      "args": ["/path/to/mcp/index.js"],
      "env": {
        "ABUNDERA_SIGN_API_KEY": "abnd_sign_..."
      }
    }
  }
}

Setup for Cursor / Windsurf

Add the same configuration to .cursor/mcp.json (Cursor) or your Windsurf MCP settings file.

Example

Ask your AI assistant: "Send an NDA to jane@contoso.com using the nda-simple template with disclosing party Acme Corp"

The agent calls create_envelope:

{
  "template_id": "nda-simple",
  "signers": [{ "name": "Jane Doe", "email": "jane@contoso.com", "role": "recipient" }],
  "field_values": { "disclosing_party_name": "Acme Corp" }
}
Prerequisites: Node.js 18+ and an Abundera Sign API key (abnd_sign_* prefix). MCP server source and README ship in the @abundera/sign-mcp-server npm package.

Full endpoint reference

This page covers integration guides (authentication, webhooks, embedded signing, MCP). The complete REST API reference for every endpoint lives in our interactive docs:

Interactive API Reference →OpenAPI YAMLOpenAPI JSONPostmancURL Examples
PricingSecurity architectureManage API keys