Abundera Sign API

Create legally defensible 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": "nda-standard",
  "signers": [
    { "email": "signer@example.com", "name": "Jane Doe", "role": "recipient" }
  ],
  "fields": {
    "company_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

OpenAPI YAMLOpenAPI JSONPostman CollectioncURL Examples

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.

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
{
  "access_token": "eyJhbGciOi...",
  "refresh_token": "rt_abc123...",
  "expires_in": 900,
  "token_type": "Bearer"
}

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, GitHub anchoring, custom email branding
Businessbusiness+ Template CRUD, audit export, court-ready declarations, VPN/proxy blocking, custom retention (99yr), white label
Tier in JWT: Your plan tier is included in the JWT payload as tier. The API checks this automatically — no extra headers needed.

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 jobsCRON_SECRET in query parameter

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
// Error response format
{
  "error": "envelope_id is required"
}

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.

Webhooks

Abundera Sign sends HMAC-SHA256 signed webhooks to your callback_url for envelope and signer lifecycle events. Configure per-envelope callbacks or use global webhooks for all envelopes.

POST {callback_url}
Content-Type: application/json
X-Signature-256: a1b2c3d4e5...

{
  "event": "envelope.completed",
  "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
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.counter_proposedSigner submitted a counter-proposal to a clause response

Verifying Webhook Signatures

Compute HMAC-SHA256(callback_secret, request_body) and compare with the X-Signature-256 header using constant-time comparison.

// Node.js verification example
const crypto = require('crypto');
const expected = crypto
  .createHmac('sha256', callbackSecret)
  .update(rawBody)
  .digest('hex');
const valid = crypto.timingSafeEqual(
  Buffer.from(expected),
  Buffer.from(signatureHeader)
);

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 (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
const iframe = document.createElement('iframe');
iframe.src = signing_url;
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: 'nda-simple',
    embedded: true,               // ← key flag
    signers: [{ name: 'Jane Doe', email: 'jane@example.com', role: 'signer' }],
    fields: { company_name: 'Acme Corp' },
  }),
});

const envelope = await response.json();
// envelope.signers[0].signing_url → embed this in your iframe

Security Notes

See code examples and framework guides →

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@example.com using the nda-simple template with company name Acme Corp"

The agent calls create_envelope:

{
  "template_id": "nda-simple",
  "signers": [{ "name": "Jane Doe", "email": "jane@example.com", "role": "signer" }],
  "field_values": { "company_name": "Acme Corp" }
}
Prerequisites: Node.js 18+ and an Abundera Sign API key (abnd_sign_* prefix). Full documentation in the MCP server README.

Create Envelope

POST/api/v1/envelopesJWT

Create a new signing envelope. Generates unique signing tokens per signer and sends invitation emails automatically.

Request Body
FieldTypeDescription
templaterequiredstringTemplate ID (e.g., "nda-standard")
signersrequiredarrayArray of signer objects, each with email, name, role, and optional phone
org_idoptionalstringOrganization ID to create the envelope under. Requires membership (member+ role). Envelope will be visible to all org members. Biz+
fieldsoptionalobjectPre-filled field values (key-value pairs matching template fields)
messageoptionalstringPersonal message from sender included in the signing invitation email. Max 1,000 characters.
callback_urloptionalstringWebhook URL called on completion
metadataoptionalobjectCustom metadata stored with envelope
watermarkoptionalstringText watermark on PDF pages (e.g., "CONFIDENTIAL")
footeroptionalobjectCustom footer: { left, center, right, rule }
sign_orderoptionalbooleanEnforce sequential signing order. Default: false
expiry_daysoptionalintegerDays until expiration. Default: 30, max: 365
retention_yearsoptionalintegerDocument retention period. Default: 3, max: 99
reminder_daysoptionalintegerDays between auto-reminders. Default: 3, max: 30
max_remindersoptionalintegerMax auto-reminders per signer. Default: 3, max: 10
access_codeoptionalstringPIN code signers must enter (min 4 chars, stored hashed)
geo_lockoptionalarrayRestrict signing to specific countries. Array of ISO 3166-1 alpha-2 codes (e.g., ["US", "CA"]). Pro+
block_vpnoptionalbooleanBlock signing from VPN/proxy connections. Uses ASN-based detection. Default: false. Biz+
require_photooptionalstringRequire or request a webcam photo of the signer. "required" blocks signing without a photo. "optional" allows skipping. Records camera availability and permission state. Pro+
require_geolocationoptionalstringRequire or request browser GPS location. "required" blocks signing without location access. "optional" allows denying. Records precise coordinates and accuracy. Pro+
locked_fieldsoptionalarrayArray of field names that signers cannot edit (sender pre-filled values are locked). e.g., ["company_name", "effective_date"]
attach_pdfoptionalbooleanAttach signed PDF to completion email. Default: true. Set to false for large documents.
require_audio_statementoptionalstringRequire or request a sworn audio statement. Signer records themselves reading a statement aloud. "required" or "optional". Pro+
require_video_statementoptionalstringRequire or request a sworn video statement. Signer records face + voice reading a statement. "required" or "optional". Pro+
require_id_verificationoptionalstringRequire government-issued ID verification (passport, driver's license) via independent IDV provider (Veriff). "required" or "optional". Biz+
require_kbaoptionalstringRequire Knowledge-Based Authentication — identity questions from public/private records (LexisNexis). US only. "required" or "optional". Biz+
ai_summaryoptionalbooleanEnable AI plain-English document summary for signers. Default: false. Pro+
allow_delegateoptionalbooleanAllow signers to delegate signing responsibility to another person. Max 2 delegation transfers per signer. Default: true
ccoptionalarrayCC recipients who receive the completed document. Array of { email, name } objects. Max 10.
payment_amountoptionalnumberCollect payment before signing (Stripe). Amount in minor units. Biz+
payment_currencyoptionalstringISO 4217 currency code for payment. Default: "usd". Biz+
langoptionalstringLanguage for signing page, emails, and Certificate of Completion. ISO 639-1 code. Supported: en, es, fr, de, pt, ja, ar, zh, hi, bn, ru, ur, ko, it, tr, vi, th, nl, pl, ms. Default: "en". RTL supported for Arabic and Urdu.
require_attachmentsoptionalarrayRequire signers to upload file attachments. Array of objects with name (string, required), description (string, optional), and required (boolean, optional). Max 5 items. Pro+
embeddedoptionalbooleanEmbedded signing mode. Skips invitation emails and returns signing_url per signer for iframe embedding. Default: false
test_modeoptionalbooleanTest mode. Skips emails, does not count against usage limits. Returns signing_url per signer. Default: false
in_personoptionalbooleanIn-person (kiosk) signing mode. Skips invitation emails, returns signing_url per signer for device handoff. Pro+
sourceoptionalstringEnvelope source: "self_service", "abundera", or "api"
Example Request
{
  "template": "nda-standard",
  "signers": [
    {
      "email": "jane@example.com",
      "name": "Jane Doe",
      "role": "recipient",
      "phone": "+15551234567"
    },
    {
      "email": "bob@example.com",
      "name": "Bob Smith",
      "role": "company"
    }
  ],
  "fields": {
    "company_name": "Acme Corp",
    "effective_date": "2026-03-15"
  },
  "cc": [
    { "email": "legal@example.com", "name": "Legal Team" }
  ],
  "callback_url": "https://api.example.com/webhooks/sign",
  "message": "Please review and sign this NDA at your earliest convenience.",
  "sign_order": true,
  "expiry_days": 14,
  "retention_years": 7,
  "reminder_days": 3,
  "max_reminders": 5,
  "access_code": "8472",
  "geo_lock": ["US", "CA"],
  "block_vpn": true,
  "watermark": "CONFIDENTIAL",
  "footer": { "left": "Acme Corp", "right": "Confidential", "rule": true },
  "locked_fields": ["company_name"],
  "require_photo": "optional",
  "require_geolocation": "optional",
  "require_audio_statement": "required",
  "require_video_statement": "optional",
  "require_id_verification": "required",
  "require_kba": "optional",
  "ai_summary": true,
  "allow_delegate": false,
  "attach_pdf": true,
  "lang": "es",
  "embedded": false,
  "require_attachments": [
    { "name": "Government ID", "description": "Upload a photo of your ID", "required": true }
  ]
}
Signer Object
FieldTypeDescription
emailrequiredstringSigner's email address
namerequiredstringSigner's full name
rolerequiredstringRole matching template (e.g., "recipient", "company")
phoneoptionalstringPhone number for SMS OTP verification (E.164 format)
Response 200
{
  "id": "e3f8a1b2-...",
  "status": "sent",
  "template_id": "nda-standard",
  "expires_at": "2026-04-04T12:00:00Z",
  "signers": [
    {
      "id": "s1a2b3c4-...",
      "email": "signer@example.com",
      "name": "Jane Doe",
      "role": "recipient",
      "status": "sent",
      "signing_url": "https://sign.abundera.ai/sign/?token=..."
    }
  ],
  "callback_secret": "a1b2c3..."
}

Note: signing_url is only returned per signer when embedded, test_mode, or in_person is true (email delivery is skipped in these modes).

List Envelopes

GET/api/v1/envelopesJWT

List envelopes for the authenticated user. Without org_id, returns personal envelopes only. With org_id, returns all org envelopes (requires membership).

Query Parameters
ParamTypeDescription
org_idoptionalstringOrganization ID. Returns org-scoped envelopes visible to all members. Also accepted via X-Org-Id header. Biz+
pageoptionalintegerPage number. Default: 1
limitoptionalintegerResults per page. Default: 20, max: 100
statusoptionalstringFilter: sent, completed, voided
templateoptionalstringFilter by template ID
Response 200
{
  "envelopes": [ ... ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "total": 42,
    "pages": 3
  }
}

Get Envelope

GET/api/v1/envelopes/:idJWT

Get full envelope details including signers, audit event count, and comment count.

Response 200
{
  "id": "e3f8a1b2-...",
  "template_id": "nda-standard",
  "template_name": "Standard NDA",
  "status": "sent",
  "sender_email": "you@company.com",
  "sender_name": "Your Name",
  "created_at": "2026-03-05T12:00:00Z",
  "expires_at": "2026-04-04T12:00:00Z",
  "has_access_code": false,
  "sign_order": false,
  "signers": [
    {
      "id": "s1a2b3c4-...",
      "email": "signer@example.com",
      "name": "Jane Doe",
      "role": "recipient",
      "status": "pending",
      "has_phone": true,
      "phone_verified": false,
      "reminder_count": 0
    }
  ],
  "audit_event_count": 3,
  "comment_count": 0
}

Envelope Status

GET/api/v1/envelope-statusJWT / Token / Public (demo)

Check envelope status. Supports JWT (sender), signing token (signer), or public access for demo envelopes.

Query Parameters
ParamTypeDescription
idstringEnvelope ID (for JWT or demo lookup)
tokenstringSigning token (for signer lookup)

Void Envelope

POST/api/v1/void-envelopeJWT

Void/cancel an in-flight envelope. Notifies all unsigned signers via email. Cannot void completed envelopes.

Request Body
FieldTypeDescription
envelope_idrequiredstringEnvelope ID to void
Response 200
{
  "id": "e3f8a1b2-...",
  "status": "voided",
  "notified_signers": 2
}
Errors

400 Cannot void completed envelope   404 Not found

Bulk Create Envelopes

POST/api/v1/envelopes/bulkJWT

Send the same template to multiple recipients in one call. Each recipient gets their own envelope. Max 100 recipients. Only markdown templates are supported.

Request Body
FieldTypeDescription
template requiredstringTemplate ID (must be markdown-based)
recipients requiredarrayArray of {email, name, role, phone?, fields?} — max 100
shared_fields optionalobjectFields shared across all envelopes
sender_role optionalstringSender's role in the template default: party_a
callback_url optionalstringWebhook URL for all envelopes
Response
201
{
  "created": 8,
  "failed": 0,
  "envelopes": [
    { "id": "...", "recipient": "alice@example.com", "status": "sent" }
  ]
}

Bulk CSV Send

POST/api/v1/envelopes/bulk-csvJWT

Send a template to multiple recipients via CSV upload. Each row in the CSV creates a separate envelope. Pro+ Accepts multipart/form-data with a CSV file.

Request Body (multipart/form-data)
FieldTypeDescription
filerequiredfileCSV file with columns: email, name, role, plus any template field columns
templaterequiredstringTemplate ID
callback_urloptionalstringWebhook URL for all envelopes
Response 201
{
  "created": 25,
  "failed": 2,
  "errors": [
    { "row": 12, "error": "Invalid email" }
  ]
}
Error Responses

400 Invalid CSV format or missing required columns   403 Professional plan required

Demo Envelope

POST/api/v1/demo-envelopePublic

Create a demo envelope for testing. No authentication required. Returns signing URLs for immediate use. Max 90-day expiry.

Request Body
FieldTypeDescription
template optionalstringTemplate ID default: demo
name optionalstringSigner name default: Test Signer
email optionalstringSigner email default: test@example.com
fields optionalobjectTemplate field values
expiry_days optionalintegerDays until expiry default: 7, max: 90
Response
201
{
  "envelope_id": "uuid",
  "signers": [
    { "id": "...", "signing_url": "https://sign.abundera.ai/sign/?token=..." }
  ],
  "expires_at": "2026-03-12T00:00:00Z"
}

Submit Signature

POST/api/v1/signToken

Submit a signature for a document. Updates the signer record, checks if all signers have completed, and triggers completion flow (PDF generation, emails, webhook) when the last signer signs.

Request Body
FieldTypeDescription
tokenrequiredstringSigning token from the invitation email
consentrequiredbooleanESIGN Act consent acknowledgement (must be true)
signature_dataoptionalstringBase64-encoded signature image (PNG) or typed name
signature_typeoptionalstringSignature method: "typed", "drawn", or "uploaded"
field_valuesoptionalobjectField values filled in by the signer (key-value pairs)
Response 200
{
  "status": "signed",
  "all_signed": true,
  "download_url": "https://sign.abundera.ai/api/v1/download?envelope=...&token=...",
  "envelope_id": "e3f8a1b2-...",
  "signer_id": "s1a2b3c4-..."
}
Note:download_url is only returned when all_signed is true (all signers have completed). The PDF is generated asynchronously — it may take a few seconds to be available at the download URL.
Error Responses

400 Token/consent missing   404 Invalid token   409 Already signed/declined   410 Voided or expired

Decline to Sign

POST/api/v1/declineToken

Decline to sign a document with a reason. Notifies the sender via email with the decline reason.

Request Body
FieldTypeDescription
tokenrequiredstringSigning token
reasonrequiredstringDecline reason (max 1000 chars)
Response 200
{
  "status": "declined",
  "envelope_id": "e3f8a1b2-...",
  "signer_id": "s1a2b3c4-..."
}

Send Reminder

POST/api/v1/remindJWT

Send a reminder email to unsigned signers. Generates fresh signing tokens. Can target a specific signer or all unsigned.

Request Body
FieldTypeDescription
envelope_idrequiredstringEnvelope ID
signer_idoptionalstringTarget specific signer. If omitted, reminds all unsigned.
Response 200
{
  "envelope_id": "e3f8a1b2-...",
  "reminded": [{ "id": "...", "email": "...", "name": "..." }],
  "count": 1
}

Resend Invitation

POST/api/v1/resend-signerJWT

Resend the signing invitation with a fresh token. Optionally update the signer's email address (reassignment).

Request Body
FieldTypeDescription
envelope_idrequiredstringEnvelope ID
signer_idrequiredstringSigner ID to resend to
new_emailoptionalstringNew email address (reassign signer)
Response 200
{
  "signer_id": "s1a2b3c4-...",
  "email": "new-signer@example.com",
  "status": "sent"
}

Delegate Signing

POST/api/v1/delegateToken

Delegate signing responsibility to another person. The original signer is marked as delegated and a new signing invitation is sent to the delegate. Delegation can be chained up to 2 levels deep. The sender can disable delegation by setting allow_delegate: false in envelope metadata.

Request Body
FieldTypeDescription
tokenrequiredstringSigning token of the original signer
delegate_emailrequiredstringEmail address of the delegate (max 254 chars). Validated against disposable email blocklist.
delegate_namerequiredstringFull name of the delegate (max 200 chars)
reasonoptionalstringReason for delegation (max 1000 chars)
Response 200
{
  "status": "delegated",
  "delegate_email": "delegate@example.com",
  "delegate_name": "Jane Delegate",
  "envelope_id": "e3f8a1b2-..."
}
Error Responses

400 Self-delegation, duplicate signer, max chain limit (2), validation error   403 Delegation disabled for this envelope   409 Already signed/declined/delegated   410 Voided, completed, or expired

Archive Envelope

POST/api/v1/archive-envelopeJWT

Archive or unarchive an envelope. Only envelopes with completed or voided status can be archived. Archived envelopes are excluded from list results by default (use ?include_archived=true to include them).

Request Body
FieldTypeDescription
envelope_idrequiredstringEnvelope ID
actionrequiredstring"archive" or "unarchive"
Response 200 (archive)
{
  "id": "e3f8a1b2-...",
  "status": "archived",
  "archived_at": "2026-03-10T12:00:00Z"
}
Response 200 (unarchive)
{
  "id": "e3f8a1b2-...",
  "status": "unarchived"
}
Error Responses

400 Invalid action, wrong envelope status, already archived/not archived   404 Envelope not found or not owned

Correct Envelope

POST/api/v1/correct-envelopeJWT

Fix a signer's email or name on an active envelope. If the email changes, the old signing token is revoked and a new signing invitation is sent. Only the envelope sender can correct signers. Only signers with sent or viewed status can be corrected (already signed/declined signers cannot).

Request Body
FieldTypeDescription
envelope_idrequiredstringEnvelope ID
signer_idrequiredstringSigner ID to correct
emailoptionalstringNew email address (generates new token, sends new invitation)
nameoptionalstringNew signer name
Response 200
{
  "id": "s1a2b3c4-...",
  "email": "corrected@example.com",
  "name": "Jane Doe",
  "corrected": true,
  "new_invitation_sent": true
}
Error Responses

400 Must provide email or name, signer already signed/declined   404 Envelope or signer not found

Transfer Envelope

POST/api/v1/transfer-envelopeJWT

Transfer ownership of an envelope to another user. The new owner receives all management permissions (void, remind, resend, view audit trail). Only the current sender can transfer. Works on active (sent) envelopes only.

Request Body
FieldTypeDescription
envelope_idrequiredstringEnvelope ID
new_owner_emailrequiredstringEmail of the new owner
new_owner_nameoptionalstringName of the new owner
Response 200
{
  "id": "e3f8a1b2-...",
  "transferred": true,
  "new_owner_email": "newowner@example.com",
  "new_owner_name": "New Owner"
}
Error Responses

400 Cannot transfer to self, envelope not active   404 Envelope not found or not owned

Move Envelope to Folder

POST/api/v1/move-envelopeJWT

Move an envelope into a folder. Set folder_id to null to remove from its current folder.

Request Body
FieldTypeDescription
envelope_idrequiredstringEnvelope ID
folder_idrequiredstring | nullTarget folder ID, or null to remove from folder
Response 200
{
  "id": "e3f8a1b2-...",
  "folder_id": "f1a2b3c4-...",
  "moved": true
}
Error Responses

400 Invalid folder_id   404 Envelope or folder not found

List Folders

GET/api/v1/foldersJWT

List all folders for the authenticated user, with envelope counts per folder. Supports org context via ?org_id=.

Query Parameters
ParamTypeDescription
org_idoptionalstringScope to organization
Response 200
{
  "folders": [
    {
      "id": "f1a2b3c4-...",
      "name": "Q1 Contracts",
      "color": "#3b82f6",
      "envelope_count": 12,
      "created_at": "2026-04-01T10:00:00Z"
    }
  ]
}

Create Folder

POST/api/v1/foldersJWT

Create a new envelope folder. Maximum 50 folders per user (or per organization).

Request Body
FieldTypeDescription
namerequiredstringFolder name (max 100 chars)
coloroptionalstringHex color code (e.g. #3b82f6)
org_idoptionalstringCreate in organization context
Response 201
{
  "id": "f1a2b3c4-...",
  "name": "Q1 Contracts",
  "color": "#3b82f6",
  "created_at": "2026-04-01T10:00:00Z"
}
Error Responses

400 Missing name, invalid color format   409 Folder limit reached (50)

Update Folder

PUT/api/v1/foldersJWT

Update a folder's name or color.

Request Body
FieldTypeDescription
folder_idrequiredstringFolder ID
nameoptionalstringNew folder name
coloroptionalstringNew hex color code
Response 200
{
  "id": "f1a2b3c4-...",
  "name": "Q2 Contracts",
  "color": "#10b981",
  "updated": true
}
Error Responses

400 Must provide name or color   404 Folder not found or not owned

Delete Folder

DELETE/api/v1/foldersJWT

Delete a folder. Envelopes in the folder are moved to no-folder (not deleted).

Request Body
FieldTypeDescription
folder_idrequiredstringFolder ID to delete
Response 200
{
  "deleted": true,
  "envelopes_moved": 5
}
Error Responses

404 Folder not found or not owned

Download Signed PDF

GET/api/v1/downloadJWT / Token

Download the completed signed PDF. Authenticated via JWT (sender) or download token (from completion email).

Query Parameters
ParamTypeDescription
enveloperequiredstringEnvelope ID
tokenoptionalstringDownload token (from completion email)

200 Returns application/pdf binary

Verify Document

GET/api/v1/verifyPublic

Publicly verify a signed document's authenticity. Checks audit chain integrity, manifest hash, RFC 3161 timestamp, and GitHub anchor.

Query Parameters
ParamTypeDescription
idstringVerification ID (vrf_xxx format, public) or raw UUID (requires JWT). See Verification IDs.
hashstringSHA-256 hash of a signed PDF — looks up the envelope by document hash (public, self-gating)
Response 200
{
  "verified": true,
  "envelope": {
    "id": "e3f8a1b2-...",
    "status": "completed",
    "completed_at": "2026-03-05T12:00:00Z"
  },
  "signers": [
    {
      "name": "J****e D****e",
      "email": "j****e@example.com",
      "signed_at": "...",
      "viewing_duration_ms": 45200,
      "scroll_depth_pct": 100,
      "viewport": "1440x900",
      "ceremony_event_count": 12,
      "has_photo": true,
      "photo_permission": "granted",
      "geolocation": { "latitude": 36.1699, "longitude": -115.1398, "accuracy": 12 },
      "geolocation_permission": "granted"
    }
  ],
  "integrity": {
    "audit_chain_valid": true,
    "manifest_hash": "a1b2c3...",
    "document_hash": "d4e5f6...",
    "github_anchor": { "commit_sha": "...", "url": "..." },
    "rfc3161_timestamp": { "tsa_url": "...", "requested_at": "..." }
  }
}
Tip: Emails are privacy-masked in public verification responses (e.g., j***e@example.com).
PDF Upload Verification: The public verification page at /verify also supports drag-and-drop PDF upload. The SHA-256 hash of the uploaded file is computed client-side and compared against the stored document hash — no file data leaves the browser.

Plain-English Document Summary

GET/api/v1/document-summaryToken

Generate an automatically generated plain-English description of the document to help signers understand what they are signing. Results cached for 7 days (document content is immutable per envelope). This is a comprehension aid, not legal advice.

Query Parameters
ParamTypeDescription
tokenrequiredstringSigning token (min 32 chars)
Response 200
{
  "summary": "This is a standard NDA that...",
  "generated_at": "2026-03-05T12:00:00Z",
  "model": "workers-ai",
  "cached": false
}

Get Document Data

GET/api/v1/document-dataToken

Retrieve rendered document HTML and field definitions for the signing page. Enforces access code and OTP gates if configured. Returns the signer's role, fields to fill, and document content.

Query Parameters
ParamTypeDescription
tokenrequiredstringSigning token (min 32 chars)
access_codeoptionalstringPIN code if envelope requires one
Response 200
{
  "html": "<h1>NDA Agreement</h1>...",
  "fields": [
    { "name": "company_name", "type": "text", "required": true }
  ],
  "signer_role": "counterparty",
  "template_name": "Mutual NDA",
  "features": { "ai_summary": true, "require_photo": "optional", "require_geolocation": null }
}
Error Responses
403 Access code required / Phone verification required
409 Already signed
410 Document voided or link expired

List Templates

GET/api/v1/templatesJWT

List available document templates. Returns global templates by default. Pass org_id to also include org-private templates (requires membership). Supports ?category=, ?tag=, and ?search= filters.

Create Template

POST/api/v1/templatesJWT

Create a new document template with markdown content and field definitions.

Request Body
FieldTypeDescription
idrequiredstringUnique template identifier (slug)
namerequiredstringHuman-readable name
org_idoptionalstringOrganization ID. Creates an org-private template (requires admin+ role). Without this, templates are personal. Biz+
descriptionoptionalstringTemplate description
rolesoptionalarraySigner roles (e.g., ["sender", "recipient"])
markdownoptionalstringMarkdown template content with field placeholders
metadataoptionalobjectTemplate metadata (watermark, footer defaults)

Get Template

GET/api/v1/templates/:idJWT

Retrieve a single template by ID, including full markdown content, fields, and metadata.

Update Template

PUT/api/v1/templates/:idJWT

Update an existing template's name, description, fields, markdown, or metadata. All fields are optional — only provided fields are updated.

Delete Template

DELETE/api/v1/templates/:idJWT

Delete a template. Fails with 409 if the template has active (non-voided) envelopes.

List Comments

GET/api/v1/envelopes/:id/commentsJWT

List all comments on an envelope, ordered by creation date.

Add Comment

POST/api/v1/envelopes/:id/commentsJWT

Add a comment to an envelope. Max 2000 characters.

Request Body
FieldTypeDescription
commentrequiredstringComment text (max 2000 chars)

Audit Trail

GET/api/v1/envelopes/:id/auditJWT

Get the full hash-chained audit trail for an envelope with integrity verification.

Response 200
{
  "envelope_id": "e3f8a1b2-...",
  "chain_valid": true,
  "chain_errors": [],
  "event_count": 5,
  "events": [
    {
      "action": "created",
      "actor": "sender@example.com",
      "is_bot": false,
      "entry_hash": "a1b2c3...",
      "previous_hash": "",
      "created_at": "2026-03-05T12:00:00Z"
    }
  ]
}
Hash Chain: Each event's entry_hash is computed from its content + the previous_hash, forming a tamper-evident chain. If any event is modified, all subsequent hashes break.
Bot Detection: Events triggered by email link prefetchers (Gmail, Outlook, etc.) are automatically flagged with "is_bot": true. Detection uses User-Agent pattern matching and Google IP range identification. Bot events are labeled "[Email Prefetch]" in the PDF Certificate of Completion.

List Attachments

GET/api/v1/envelopes/:id/attachmentsJWT / Token

List supplemental documents attached to an envelope. Attachments are non-signable reference documents (e.g. exhibits, appendices, supporting materials). Accessible by the sender (JWT) or signers (token).

Response 200
{
  "attachments": [
    {
      "id": "a1b2c3d4-...",
      "filename": "exhibit-a.pdf",
      "content_type": "application/pdf",
      "size_bytes": 245760,
      "uploaded_by": "sender@example.com",
      "created_at": "2026-04-01T10:00:00Z"
    }
  ]
}

Upload Attachment

POST/api/v1/envelopes/:id/attachmentsJWT

Upload a supplemental document to an envelope. Only the sender can upload attachments. Maximum file size is 5MB. Accepts PDF, images, and common document formats. Signers can view but not modify attachments.

Request Body (multipart/form-data)
FieldTypeDescription
filerequiredfileFile to upload (max 5MB)
Response 201
{
  "id": "a1b2c3d4-...",
  "filename": "exhibit-a.pdf",
  "content_type": "application/pdf",
  "size_bytes": 245760
}
Error Responses

400 No file provided, file too large (5MB limit)   404 Envelope not found or not owned

Delete Attachment

DELETE/api/v1/envelopes/:id/attachmentsJWT

Remove a supplemental document from an envelope. Only the sender can delete attachments.

Request Body
FieldTypeDescription
attachment_idrequiredstringAttachment ID to delete
Response 200
{
  "deleted": true
}
Error Responses

404 Attachment or envelope not found

Court-Ready Declaration

GET/api/v1/envelopes/:id/declarationJWTBusiness+

Generate a court-ready "Declaration of Custodian of Records" PDF for a completed envelope. This legal declaration summarizes all technical measures used to authenticate the signing and can be filed as a court exhibit.

Response 200

Returns application/pdf — a multi-page legal declaration document.

Declaration Contents
  • Platform Description: Security measures, compliance (ESIGN Act, UETA)
  • Document Details: Template, envelope ID, timestamps, geo-restrictions
  • Signer Authentication: IP, country, VPN status, viewing duration, scroll depth, viewport, ceremony events, ESIGN consent, identity photo status, browser GPS coordinates
  • Audit Chain Integrity: SHA-256 hash chain verification result
  • Cryptographic Evidence: Manifest hash, PDF hash, GitHub anchor, RFC 3161 timestamp
  • Public Verification: Link to verification page
  • Records Custodian Statement: Business records declaration under penalty of perjury
Requirement: Envelope must be in completed status. Only the sender (owner) can generate the declaration.

Evidence Package Download

GET/api/v1/envelopes/:id/evidenceJWTBusiness+

Download the full evidence package for a completed envelope as a ZIP archive. Includes all signed artifacts: original template, field values, per-signer signatures, hash-chained audit trail, evidence manifest, signed PDF, RFC 3161 timestamp, and any recordings.

Response 200

Returns application/zip — the complete evidence package archive.

Error Responses

403 Business plan required or not the envelope sender   404 Envelope not found   409 Envelope not completed

Request Court Declaration

POST/api/v1/declaration-requestPublic

Request a Court-Ready Declaration via email OTP verification. This is a two-step process: Step 1 submits the request and sends a verification code to the requester's email. Step 2 verifies the code. Signers on the envelope are auto-approved; third-party requesters (attorneys, courts) are queued for admin review. Supports file attachments (court orders, government ID) via multipart form data.

Step 1 — Request Body (JSON)
FieldTypeDescription
envelope_idrequiredstringEnvelope ID (must be completed)
emailrequiredstringRequester's email address
namerequiredstringRequester's full name (min 2 chars)
relationshiprequiredstringOne of: signer, attorney, court_officer, law_enforcement, other
phoneoptionalstringPhone number
case_nameoptionalstringCourt case name
case_numberoptionalstringCourt case number
jurisdictionoptionalstringJurisdiction (e.g., "US District Court, Nevada")
deadlineoptionalstringFiling deadline
notesoptionalstringAdditional notes
Step 1 — Response 200
{
  "request_id": "a1b2c3d4-...",
  "email_sent": true,
  "email_masked": "j***@example.com",
  "expires_in_seconds": 600
}
Step 2 — Verify OTP
FieldTypeDescription
actionrequiredstringMust be "verify"
request_idrequiredstringRequest ID from Step 1
coderequiredstring6-digit OTP code (max 3 attempts)
Step 2 — Response 200 (signer auto-approved)
{
  "verified": true,
  "status": "approved",
  "message": "Your declaration is ready. A one-time download link has been sent to your email."
}
Step 2 — Response 200 (third-party pending review)
{
  "verified": true,
  "status": "pending_review",
  "message": "Your identity has been verified. Your request is being reviewed."
}
File attachments: Use multipart/form-data with fields attachment_0, attachment_1, attachment_2 (max 3 files, 10MB each). Accepted types: PDF, JPEG, PNG, WebP. An id_document field can also be included for government ID.

Approve / Deny Declaration Request

POST/api/v1/declaration-request/approveJWT

Admin-only endpoint to approve or deny a third-party declaration request. On approval, a one-time burn-on-use download token (72-hour TTL) is generated and emailed to the requester. On denial, the requester is notified with an optional reason.

Request Body (approve)
FieldTypeDescription
request_idrequiredstringDeclaration request ID
Request Body (deny)
FieldTypeDescription
request_idrequiredstringDeclaration request ID
actionrequiredstringMust be "deny"
denial_reasonoptionalstringReason for denial (included in notification to requester)
Response 200 (approved)
{
  "status": "approved",
  "request_id": "a1b2c3d4-...",
  "email": "requester@example.com",
  "message": "Request approved. One-time download link sent to requester."
}
Response 200 (denied)
{
  "status": "denied",
  "request_id": "a1b2c3d4-...",
  "message": "Request denied. Requester has been notified."
}
Error Responses

403 Admin access required   404 Request not found   409 Already approved or denied

Download Declaration

GET/api/v1/declaration-download?token=...Token

One-time burn-on-use download for Court-Ready Declaration PDFs. The token is destroyed after the first successful download. Links expire after 72 hours. The PDF is generated fresh at download time with the latest evidence data and includes a PAdES digital signature.

Query Parameters
FieldTypeDescription
tokenrequiredstringOne-time download token (from approval email)
Response 200

Returns application/pdf with Content-Disposition: attachment. The download token is permanently destroyed after successful generation.

Error Responses

410 Token expired, already used, or envelope no longer available

Create Organization

POST/api/v1/orgsJWTBusiness+

Create a new organization. The authenticated user becomes the owner. Limited to 10 organizations per user.

Request Body
FieldTypeDescription
namerequiredstringOrganization name. Max 100 characters. Control characters are stripped.
Response 201
{
  "id": "org_a1b2c3d4",
  "name": "Acme Corp",
  "owner_user_id": "user_xyz",
  "owner_email": "alice@acme.com"
}

List Organizations

GET/api/v1/orgsJWT

List all organizations the authenticated user belongs to.

Response 200
{
  "orgs": [
    {
      "id": "org_a1b2c3d4",
      "name": "Acme Corp",
      "role": "owner",
      "owner_email": "alice@acme.com",
      "created_at": "2026-03-06T12:00:00Z"
    }
  ]
}

Get Organization

GET/api/v1/orgs/:idJWT

Get organization details. Requires membership (any role).

Response 200
{
  "org": {
    "id": "org_a1b2c3d4",
    "name": "Acme Corp",
    "owner_user_id": "user_xyz",
    "owner_email": "alice@acme.com",
    "created_at": "2026-03-06T12:00:00Z"
  },
  "role": "owner"
}

Update Organization

PUT/api/v1/orgs/:idJWT

Update organization name. Requires admin+ role.

Request Body
FieldTypeDescription
namerequiredstringNew organization name. Max 100 characters.

Delete Organization

DELETE/api/v1/orgs/:idJWT

Delete an organization. Requires owner role. Cannot delete if there are active (sent/partial) envelopes. Existing envelopes and templates are unlinked from the org (not deleted).

List Members

GET/api/v1/orgs/:id/membersJWT

List all members of an organization. Requires membership (any role).

Response 200
{
  "members": [
    {
      "id": "mem_xyz",
      "user_id": "user_xyz",
      "email": "alice@acme.com",
      "name": "Alice",
      "role": "owner",
      "joined_at": "2026-03-06T12:00:00Z"
    }
  ]
}

Update Member Role

PUT/api/v1/orgs/:id/members/:uidJWT

Change a member's role. Requires admin+ role. Only the owner can assign the admin role. Cannot change the owner's role.

Request Body
FieldTypeDescription
rolerequiredstringNew role: "viewer", "member", "admin", or "owner"

Remove Member

DELETE/api/v1/orgs/:id/members/:uidJWT

Remove a member from the organization. Requires admin+ role. Cannot remove the owner.

Invite Member

POST/api/v1/orgs/:id/invitationsJWT

Send an email invitation to join the organization. Requires admin+ role. Only the owner can invite with the admin role. Invitation expires after 7 days. An email with an accept link is sent automatically.

Request Body
FieldTypeDescription
emailrequiredstringEmail address to invite
roleoptionalstringRole to assign on acceptance. Default: "member". Options: "viewer", "member", "admin"

List Invitations

GET/api/v1/orgs/:id/invitationsJWT

List pending invitations for an organization. Requires admin+ role.

Response 200
{
  "invitations": [
    {
      "id": "inv_abc123",
      "email": "bob@acme.com",
      "role": "member",
      "invited_by": "alice@acme.com",
      "status": "pending",
      "created_at": "2026-03-06T12:00:00Z",
      "expires_at": "2026-03-13T12:00:00Z"
    }
  ]
}

Revoke Invitation

DELETE/api/v1/orgs/:id/invitations/:iidJWT

Revoke a pending invitation. Requires admin+ role.

Accept Invitation

POST/api/v1/orgs/accept-inviteJWT

Accept an organization invitation using the token from the invitation email. The authenticated user's email must match the invited email. On success, the user becomes a member with the invited role.

Request Body
FieldTypeDescription
tokenrequiredstringInvitation token from the accept link
Response 200
{
  "message": "Invitation accepted",
  "org_id": "org_a1b2c3d4",
  "role": "member"
}
Role Hierarchy
RoleLevelCapabilities
viewer1Read-only access to org envelopes and templates
member2Create envelopes, modify envelopes, add comments
admin3Manage members, send invitations, manage org templates
owner4Full control — update org, delete org, assign admin role

Get Branding

GET/api/v1/brandingJWT

Retrieve white-label branding configuration for the authenticated user. Returns { "configured": false } if no branding has been set up.

Business+ only: This endpoint requires the Business plan or higher (tier level 3).
Response 200
{
  "company_name": "Acme Corp",
  "logo_url": "https://acme.com/logo.svg",
  "primary_color": "#60a5fa",
  "accent_color": "#a78bfa",
  "custom_domain": "sign.acme.com",
  "email_from_name": "Acme Signing",
  "footer_text": "Powered by Acme Corp",
  "created_at": "2026-03-01T10:00:00Z",
  "updated_at": "2026-03-05T14:30:00Z"
}

Update Branding

PUT/api/v1/brandingJWT

Create or update white-label branding settings. Only include the fields you want to change — omitted fields are not modified.

Business+ only: This endpoint requires the Business plan or higher (tier level 3).
Request Body
FieldTypeDescription
company_nameoptionalstringCompany name (max 100 chars)
logo_urloptionalstringURL to company logo (max 2048 chars)
primary_coloroptionalstringHex color code (e.g. #60a5fa)
accent_coloroptionalstringHex color code (e.g. #a78bfa)
custom_domainoptionalstringCustom signing domain (max 253 chars)
email_from_nameoptionalstringCustom sender name for signing emails
footer_textoptionalstringCustom footer text for signing pages (max 500 chars)
Response 200
{
  "company_name": "Acme Corp",
  "logo_url": "https://acme.com/logo.svg",
  "primary_color": "#60a5fa",
  "accent_color": "#a78bfa",
  "custom_domain": "sign.acme.com",
  "email_from_name": "Acme Signing",
  "footer_text": "Powered by Acme Corp",
  "created_at": "2026-03-01T10:00:00Z",
  "updated_at": "2026-03-05T14:30:00Z"
}
Error Responses

400 Validation error (invalid hex color, empty update, etc.)   403 Business plan required

PUT/api/v1/branding/logoJWT

Upload a logo image for white-label branding. Biz+ Accepts multipart/form-data with an image file. The uploaded image URL is automatically saved to your branding configuration.

Request Body (multipart/form-data)
FieldTypeDescription
filerequiredfileImage file (PNG, JPG, SVG, WebP)
Response 200
{
  "logo_url": "https://sign.abundera.ai/branding/logos/abc123.png"
}
Error Responses

400 No file uploaded or invalid format   403 Business plan required

Verify Custom Domain

POST/api/v1/branding/verify-domainJWTBusiness+

Trigger on-demand verification of a custom signing domain. Checks DNS CNAME records and Cloudflare for SaaS hostname status. Use after configuring DNS records for your custom domain.

Response 200
{
  "domain": "sign.yourcompany.com",
  "status": "active",
  "verified_at": "2026-04-06T10:00:00Z"
}
Error Responses

400 No custom domain configured   403 Business plan required   422 DNS records not found

Verify Email Domain

POST/api/v1/branding/verify-email-domainJWTBusiness+

Trigger on-demand verification of a custom email sending domain. Checks DNS records (SPF, DKIM, DMARC) required for sending signing emails from your own domain via Resend.

Response 200
{
  "domain": "yourcompany.com",
  "status": "verified",
  "dns_records": [
    { "type": "TXT", "name": "_dmarc", "status": "verified" }
  ]
}
Error Responses

400 No email domain configured   403 Business plan required   422 DNS records not found

List Clauses

GET/api/v1/clausesJWT

List reusable clause library entries. Biz+ Supports search and category filtering. Clauses can be inserted into templates during envelope creation.

Query Parameters
FieldTypeDescription
searchoptionalstringSearch by title or content
categoryoptionalstringFilter by category (e.g., indemnification, confidentiality)
Response 200
{
  "clauses": [
    {
      "id": "cl_a1b2c3d4-...",
      "title": "Standard Indemnification",
      "content": "Each party shall indemnify and hold harmless...",
      "category": "indemnification",
      "tags": ["legal", "liability"],
      "created_at": "2026-03-10T08:00:00Z",
      "updated_at": "2026-03-10T08:00:00Z"
    }
  ]
}

Create Clause

POST/api/v1/clausesJWT

Create a reusable clause for your clause library. Biz+ Clauses can be referenced and inserted into templates.

Request Body
FieldTypeDescription
titlerequiredstringClause title (max 200 chars)
contentrequiredstringClause content in markdown (max 10,000 chars)
categoryoptionalstringCategory label (e.g., indemnification, confidentiality, termination)
tagsoptionalarrayArray of string tags for filtering
Response 201
{
  "id": "cl_a1b2c3d4-...",
  "title": "Standard Indemnification",
  "content": "Each party shall indemnify and hold harmless...",
  "category": "indemnification",
  "tags": ["legal", "liability"],
  "created_at": "2026-03-10T08:00:00Z"
}
Error Responses

400 Missing title or content   403 Business plan required

Get Clause

GET/api/v1/clauses/:idJWT

Retrieve a single clause by ID. Returns the full clause content and metadata.

Response 200
{
  "id": "cl_a1b2c3d4-...",
  "title": "Standard Indemnification",
  "content": "Each party shall indemnify and hold harmless...",
  "category": "indemnification",
  "tags": ["legal", "liability"],
  "created_at": "2026-03-10T08:00:00Z",
  "updated_at": "2026-03-10T08:00:00Z"
}
Error Responses

404 Clause not found

Update Clause

PUT/api/v1/clauses/:idJWT

Update an existing clause. Only include the fields you want to change — omitted fields are not modified.

Request Body
FieldTypeDescription
titleoptionalstringUpdated title (max 200 chars)
contentoptionalstringUpdated content in markdown (max 10,000 chars)
categoryoptionalstringUpdated category
tagsoptionalarrayUpdated tags array (replaces existing)
Response 200
{
  "id": "cl_a1b2c3d4-...",
  "title": "Updated Indemnification",
  "content": "The receiving party shall indemnify...",
  "category": "indemnification",
  "tags": ["legal", "updated"],
  "updated_at": "2026-03-11T10:00:00Z"
}
Error Responses

400 Empty update   404 Clause not found

Delete Clause

DELETE/api/v1/clauses/:idJWT

Permanently delete a clause from the library. This does not affect envelopes that already used this clause.

Response 204

No content.

Error Responses

404 Clause not found

Flag Clause for Negotiation

POST/api/v1/negotiateToken

Signer flags a clause for negotiation. The sender is notified and can accept, reject, or counter-propose changes. The envelope remains in sent status until the negotiation is resolved.

Request Body
FieldTypeDescription
tokenrequiredstringSigning token
clause_idrequiredstringID of the clause to flag
reasonoptionalstringExplanation of the objection or requested change (max 2000 chars)
Response 201
{
  "negotiation_id": "neg_x1y2z3-...",
  "clause_id": "cl_a1b2c3d4-...",
  "status": "flagged",
  "created_at": "2026-03-11T10:00:00Z"
}
Error Responses

400 Missing clause_id   404 Clause not found   409 Already flagged

Get Negotiations

GET/api/v1/negotiate?token=...Token

Get all negotiation flags for the current signer's envelope. Returns the status of each flagged clause.

Query Parameters
FieldTypeDescription
tokenrequiredstringSigning token
Response 200
{
  "negotiations": [
    {
      "negotiation_id": "neg_x1y2z3-...",
      "clause_id": "cl_a1b2c3d4-...",
      "status": "flagged",
      "reason": "The indemnification scope is too broad.",
      "response_text": null,
      "created_at": "2026-03-11T10:00:00Z"
    }
  ]
}

Withdraw Negotiation Flag

DELETE/api/v1/negotiateToken

Signer withdraws a previously flagged clause negotiation. Only possible while the flag is still in flagged status (not yet responded to by the sender).

Request Body
FieldTypeDescription
tokenrequiredstringSigning token
clause_idrequiredstringClause ID to withdraw the flag for
Response 204

No content.

Error Responses

404 Flag not found   409 Already responded to — cannot withdraw

List All Negotiations

GET/api/v1/negotiate/list?envelope_id=...JWT

List all clause negotiations for an envelope. Returns flags from all signers with their current status. Only the envelope sender can access this endpoint.

Query Parameters
FieldTypeDescription
envelope_idrequiredstringEnvelope ID
Response 200
{
  "negotiations": [
    {
      "negotiation_id": "neg_x1y2z3-...",
      "clause_id": "cl_a1b2c3d4-...",
      "signer_email": "signer@example.com",
      "signer_name": "Jane Doe",
      "status": "flagged",
      "reason": "The indemnification scope is too broad.",
      "response_text": null,
      "created_at": "2026-03-11T10:00:00Z"
    }
  ]
}
Error Responses

400 Missing envelope_id   403 Not the envelope sender   404 Envelope not found

Respond to Negotiation

POST/api/v1/negotiate/respondJWT

Sender responds to a specific negotiation flag with an action and optional text. The signer is notified of the response via email.

Request Body
FieldTypeDescription
envelope_idrequiredstringEnvelope ID
negotiation_idrequiredstringNegotiation flag ID
actionrequiredstringOne of: accept, reject, counter
textoptionalstringResponse explanation or counter-proposal text (max 2000 chars)
Response 200
{
  "negotiation_id": "neg_x1y2z3-...",
  "status": "countered",
  "action": "counter",
  "text": "We can limit indemnification to direct damages only.",
  "updated_at": "2026-03-11T14:00:00Z"
}
Error Responses

400 Invalid action or missing negotiation_id   403 Not the envelope sender   404 Negotiation not found   409 Already responded

POST/api/v1/signing-linksJWT

Create a reusable signing link for a template. Pro+ Anyone with the link URL can claim it — an envelope and signer are created on claim. Useful for public-facing forms, onboarding flows, or self-service signing.

Request Body
FieldTypeDescription
templaterequiredstringTemplate ID (must have markdown content)
fieldsoptionalobjectPre-filled field values for envelopes created from this link
max_usesoptionalintegerMaximum number of claims (1 to 100,000). Unlimited if omitted.
expiry_daysoptionalintegerDays until link expires (default: 90, max: 365)
metadataoptionalobjectEnvelope metadata applied to all claimed envelopes (e.g., geo_lock, lang)
org_idoptionalstringOrganization ID (requires member+ role)
Response 201
{
  "id": "l1a2b3c4-...",
  "url": "https://sign.abundera.ai/sign/?link=<token>",
  "template": "nda-standard",
  "max_uses": null,
  "use_count": 0,
  "expires_at": "2026-06-08T12:00:00Z",
  "status": "active"
}
Error Responses

400 Missing template, invalid max_uses   403 Professional+ plan required   404 Template not found

GET/api/v1/signing-linksJWT

List signing links created by the authenticated user, with pagination. Pro+ Supports org-scoped queries via X-Org-Id header or ?org_id= parameter.

Query Parameters
FieldTypeDescription
pageoptionalintegerPage number (default: 1)
limitoptionalintegerResults per page (1-100, default: 20)
Response 200
{
  "links": [
    {
      "id": "l1a2b3c4-...",
      "template_id": "nda-standard",
      "status": "active",
      "max_uses": 100,
      "use_count": 12,
      "expires_at": "2026-06-08T12:00:00Z",
      "created_at": "2026-03-10T12:00:00Z"
    }
  ],
  "total": 1,
  "page": 1,
  "limit": 20
}
GET/api/v1/signing-links/info?token=...Public

Get signing link details so the frontend can display the claim form. Returns the template name and link status without requiring authentication.

Query Parameters
FieldTypeDescription
tokenrequiredstringSigning link token
Response 200
{
  "template_name": "Non-Disclosure Agreement",
  "sender_name": "John Smith",
  "status": "active"
}
Error Responses

400 Missing token   404 Invalid link   410 Expired or max uses reached

POST/api/v1/signing-links/claimPublic

Claim a signing link — creates an envelope and signer, then sends a signing invitation email. The signing link's use count is incremented. No authentication required; the link token provides access.

Request Body
FieldTypeDescription
link_tokenrequiredstringSigning link token
namerequiredstringSigner's full name
emailrequiredstringSigner's email address (validated against disposable email blocklist)
Response 201
{
  "envelope_id": "e3f8a1b2-...",
  "signing_url": "https://sign.abundera.ai/sign/?token=<token>",
  "template": "nda-standard",
  "template_name": "Non-Disclosure Agreement",
  "expires_at": "2026-04-09T12:00:00Z"
}
Error Responses

400 Missing fields, invalid email   404 Invalid link token   410 Expired or max uses reached

List Signing Groups

GET/api/v1/signing-groupsJWTProfessional+

List all signing groups for the authenticated user, with member counts. Signing groups let you define reusable sets of signers that can be assigned to envelopes.

Response 200
{
  "groups": [
    {
      "id": "g1a2b3c4-...",
      "name": "Legal Review Team",
      "member_count": 5,
      "created_at": "2026-04-01T10:00:00Z"
    }
  ]
}

Create Signing Group

POST/api/v1/signing-groupsJWTProfessional+

Create a new signing group with members. Maximum 20 groups per user. Each group can have up to 50 members.

Request Body
FieldTypeDescription
namerequiredstringGroup name (max 100 chars)
membersrequiredarrayArray of { email, name } objects
Response 201
{
  "id": "g1a2b3c4-...",
  "name": "Legal Review Team",
  "members": [
    { "email": "alice@example.com", "name": "Alice" },
    { "email": "bob@example.com", "name": "Bob" }
  ],
  "created_at": "2026-04-01T10:00:00Z"
}
Error Responses

400 Missing name/members, too many members (50), invalid email   403 Professional+ tier required   409 Group limit reached (20)

Update Signing Group

PUT/api/v1/signing-groupsJWTProfessional+

Update a signing group's name, add members, or remove members.

Request Body
FieldTypeDescription
group_idrequiredstringSigning group ID
nameoptionalstringNew group name
add_membersoptionalarrayArray of { email, name } objects to add
remove_membersoptionalarrayArray of email strings to remove
Response 200
{
  "id": "g1a2b3c4-...",
  "name": "Legal Review Team",
  "member_count": 6,
  "updated": true
}
Error Responses

400 Must provide at least one update, member limit exceeded (50)   404 Group not found or not owned

Delete Signing Group

DELETE/api/v1/signing-groupsJWTProfessional+

Delete a signing group. Does not affect envelopes already sent to group members.

Request Body
FieldTypeDescription
group_idrequiredstringSigning group ID to delete
Response 200
{
  "deleted": true
}
Error Responses

404 Group not found or not owned

Create Document Package

POST/api/v1/packagesJWT

Create a document package that groups 2 to 10 related envelopes together. Packages let you track multi-document transactions as a single unit. All referenced envelopes must belong to the authenticated user.

Request Body
FieldTypeDescription
namerequiredstringPackage name (max 200 chars)
envelope_idsrequiredarrayArray of 2-10 envelope IDs to group
Response 201
{
  "id": "pkg_a1b2c3d4-...",
  "name": "Series A Closing Documents",
  "envelope_ids": ["e1a2b3c4-...", "e5f6g7h8-..."],
  "envelope_count": 2,
  "created_at": "2026-03-11T10:00:00Z"
}
Error Responses

400 Missing name, fewer than 2 or more than 10 envelopes   404 Envelope not found or not owned by user

List / Get Packages

GET/api/v1/packagesJWT

List all document packages for the authenticated user. Pass ?id= to retrieve a single package with full envelope details.

Query Parameters
FieldTypeDescription
idoptionalstringPackage ID to retrieve a single package
Response 200 (list)
{
  "packages": [
    {
      "id": "pkg_a1b2c3d4-...",
      "name": "Series A Closing Documents",
      "envelope_count": 3,
      "created_at": "2026-03-11T10:00:00Z"
    }
  ]
}
Response 200 (single, with ?id=)
{
  "id": "pkg_a1b2c3d4-...",
  "name": "Series A Closing Documents",
  "envelopes": [
    {
      "id": "e1a2b3c4-...",
      "template_name": "Stock Purchase Agreement",
      "status": "completed"
    }
  ],
  "created_at": "2026-03-11T10:00:00Z"
}
Error Responses

404 Package not found

Send OTP

POST/api/v1/otp/sendToken

Send a 6-digit OTP code to the signer's phone via SMS. Rate limited: max 3 sends per signer, 60-second cooldown between sends.

Request Body
FieldTypeDescription
tokenrequiredstringSigning token
Response 200
{
  "sent": true,
  "phone_masked": "***-***-1234",
  "expires_in_seconds": 600
}

Verify OTP

POST/api/v1/otp/verifyToken

Verify a 6-digit OTP code. Max 3 attempts per code — after that, request a new one.

Request Body
FieldTypeDescription
tokenrequiredstringSigning token
coderequiredstring6-digit OTP code
Response 200
{
  "verified": true
}

// or on wrong code:
{
  "verified": false,
  "attempts_remaining": 2
}

Get Signer Profile

GET/api/v1/signer-profile?token=...Token

Get auto-fill suggestions for the signer based on their previously saved profile. Returns saved name and field values if the signer has opted in to profile storage. Helps returning signers complete forms faster.

Query Parameters
FieldTypeDescription
tokenrequiredstringSigning token
Response 200 (profile exists)
{
  "name": "Jane Doe",
  "fields": {
    "company_name": "Acme Corp",
    "title": "VP Engineering"
  },
  "saved_at": "2026-03-10T08:00:00Z"
}
Response 200 (no profile)
{
  "name": null,
  "fields": {}
}

Save Signer Profile

POST/api/v1/signer-profileToken

Save signer profile data for future auto-fill. Requires explicit consent from the signer. Stored values are scoped to the signer's email and encrypted at rest.

Request Body
FieldTypeDescription
tokenrequiredstringSigning token
fieldsrequiredobjectKey-value map of field names and values to save
consentrequiredbooleanMust be true — explicit consent to store profile data
Response 200
{
  "saved": true,
  "field_count": 3
}
Error Responses

400 Missing fields or consent not true

Delete Signer Profile

DELETE/api/v1/signer-profile?token=...Token

Permanently delete all saved signer profile data (GDPR right to erasure). Removes all stored field values and name associated with the signer's email.

Query Parameters
FieldTypeDescription
tokenrequiredstringSigning token
Response 204

No content.

Geography Locking

Restrict document signing to specific countries. When geo_lock is set on an envelope, signers outside the allowed countries will see a blocked message and cannot access the document.

Professional+ only. Detection uses Cloudflare's CF-IPCountry header — no client-side geolocation or permissions needed.
Usage
// In POST /api/v1/envelopes request body:
{
  "geo_lock": ["US", "CA", "GB"]
}

Country codes use ISO 3166-1 alpha-2 format (e.g., US, CA, GB, DE). The signer's country is recorded in the audit trail regardless of geo-lock settings.

Blocked Response
{
  "error": "Signing is restricted to specific countries",
  "gate": "geo_blocked",
  "country": "RU"
}

VPN / Proxy Blocking

Block signing from VPN, proxy, and datacenter IP addresses. Uses ASN-based detection to identify hosting providers and known VPN services.

Business+ only. Detection uses Cloudflare's ASN data to match known VPN/datacenter providers (AWS, Google Cloud, DigitalOcean, NordVPN, ExpressVPN, etc.).
Usage
// In POST /api/v1/envelopes request body:
{
  "block_vpn": true
}

VPN detection status is recorded in the audit trail and displayed on the Certificate of Completion for all envelopes, regardless of whether blocking is enabled.

Blocked Response
{
  "error": "Signing from VPN/proxy connections is not allowed",
  "gate": "vpn_blocked"
}

Government ID Verification

Verify signer identity using government-issued photo ID via Veriff. Biz+ Enable by setting require_id_verification: true in envelope metadata. Max 3 sessions per signer. Results are stored for the signing session and recorded in the audit trail.

Create IDV Session

POST/api/v1/idv/sessionToken

Create a Veriff ID verification session. The signer is redirected to Veriff's hosted flow to scan their government ID. Session expires in 30 minutes.

Request Body
FieldTypeDescription
tokenrequiredstringSigning token
Response 200
{
  "session_url": "https://magic.veriff.me/v/...",
  "session_id": "abc123...",
  "expires_in_seconds": 1800
}
Error Responses

400 IDV not required for this envelope   409 Already verified or signed   429 Max sessions reached (3)   502 Veriff API error

Verify IDV Result

POST/api/v1/idv/verifyToken

Check the result of an ID verification session. Fetches the decision from Veriff and stores the result. Call this after the signer completes the Veriff flow.

Request Body
FieldTypeDescription
tokenrequiredstringSigning token
Response 200 (verified)
{
  "verified": true,
  "document_type": "DRIVERS_LICENSE",
  "document_country": "US"
}
Response 200 (not verified)
{
  "verified": false,
  "status": "declined",
  "reason": "Document expired"
}
Error Responses

410 Session expired -- start a new one   502 Veriff API error

Knowledge-Based Authentication

Verify signer identity through knowledge-based authentication using personal information and credit-bureau questions. Biz+ Enable by setting require_kba: true in envelope metadata. Max 3 attempts per signer. Supports two providers: Persona (instant database verification) and LexisNexis (quiz-based). The active provider is configured via the KBA_PROVIDER environment variable.

Start KBA Session

POST/api/v1/kba/startToken

Start a KBA session by submitting personal identifying information. With Persona, verification is instant (pass/fail). With LexisNexis, returns multiple-choice questions that must be answered within 2 minutes via the verify endpoint.

Request Body
FieldTypeDescription
tokenrequiredstringSigning token
namerequiredstringFull legal name (first and last, min 2 chars)
dobrequiredstringDate of birth (YYYY-MM-DD, must be 18+)
address_line1requiredstringStreet address (min 3 chars)
address_cityrequiredstringCity (min 2 chars)
address_staterequiredstringUS state code (e.g., CA, NY, DC)
address_ziprequiredstringUS ZIP code (5 digits or ZIP+4)
ssn_last4requiredstringLast 4 digits of SSN
Response 200 (Persona -- instant result)
{
  "passed": true,
  "provider": "persona"
}
Response 200 (LexisNexis -- quiz questions)
{
  "questions": [
    {
      "id": "q1",
      "text": "Which of the following addresses have you been associated with?",
      "choices": ["123 Main St", "456 Oak Ave", "789 Pine Rd", "None of the above"]
    }
  ],
  "expires_in": 120
}
Error Responses

400 Validation error, KBA not required   409 Already verified or signed   422 Unable to generate questions   429 Max attempts reached (3)

Verify KBA Answers

POST/api/v1/kba/verifyToken

Submit answers to KBA questions (LexisNexis provider only). Must be submitted within 2 minutes of the start request. Each answer must reference a valid question_id from the start response.

Request Body
FieldTypeDescription
tokenrequiredstringSigning token
answersrequiredarrayArray of { question_id, answer } objects
Response 200
{
  "passed": true,
  "correct_count": 4,
  "attempts_remaining": 2
}
Error Responses

400 Invalid question_id, KBA not required   410 Session expired (2-minute timeout)

Payment Gate

POST/api/v1/payments/checkoutToken / JWT

Create a Stripe Checkout session for pay-to-sign envelopes. Biz+ The envelope must be completed and have payment_amount configured in its metadata. Accepts either a signing token or JWT for authentication. Returns a Stripe Checkout URL to redirect the payer.

Request Body
FieldTypeDescription
envelope_idrequiredstringEnvelope ID (must be completed with payment configured)
tokenoptionalstringSigning token (alternative to JWT auth). Must belong to a signer on this envelope.
Response 200
{
  "checkout_url": "https://checkout.stripe.com/c/pay/...",
  "session_id": "cs_live_..."
}
Error Responses

400 Envelope not completed, no payment configured   401 No auth provided   403 Not the envelope owner or signer   409 Payment already completed

Bot & Email Prefetch Detection

Audit trail events automatically detect and flag bot traffic, including email link prefetchers from Gmail, Outlook, and other providers. This prevents false "document viewed" events from polluting the audit trail.

Detection methods:

  • User-Agent matching — known bot, crawler, and prefetcher patterns
  • Google IP ranges — Gmail's link prefetcher uses real Chrome user agents but originates from Google IP ranges (172.253.*, 209.85.*, 66.249.*, etc.)

Bot events are:

  • Flagged with "is_bot": true in the audit trail API response
  • Labeled as [Email Prefetch] on the PDF Certificate of Completion
  • Shown with a badge on the public verification page
Included in all plans. No configuration needed — bot detection is automatic for every envelope.

Contact Sender

POST/api/v1/contact-senderToken

Send a message from a signer to the envelope sender. Allows signers to ask questions about the document before signing without leaving the signing flow. The sender receives the message via email.

Request Body
FieldTypeDescription
tokenrequiredstringSigning token
messagerequiredstringMessage text (max 2000 chars)
Response 200
{
  "sent": true,
  "sender_email": "sender@company.com"
}
Error Responses

400 Missing or empty message   409 Envelope already completed or voided   429 Rate limited

Document Compare

POST/api/v1/document-compareToken

AI-powered comparison between the contract being signed and a document the signer uploads (e.g., a previous version or their own template). Returns a plain-English summary of the key differences. Powered by Workers AI.

Request Body
FieldTypeDescription
tokenrequiredstringSigning token
contract_textrequiredstringText of the current contract being signed
uploaded_textrequiredstringText of the document to compare against
langoptionalstringLanguage for the summary (default: en). Supports all 20 platform languages.
Response 200
{
  "summary": "Key differences: 1) The uploaded version has a 2-year non-compete clause while the current contract specifies 1 year. 2) The liability cap is $500K in the current version vs. $1M in the uploaded version. 3) The governing law changed from California to Delaware."
}
Error Responses

400 Missing contract_text or uploaded_text   502 AI service unavailable

Download Unsigned PDF

GET/api/v1/document-pdf?token=...Token

Download an unsigned PDF of the document with all currently filled field values rendered. Allows the signer to review the document offline or share it with legal counsel before signing. The PDF includes a "DRAFT — NOT YET SIGNED" watermark.

Query Parameters
FieldTypeDescription
tokenrequiredstringSigning token
Response 200

Returns the PDF binary with Content-Type: application/pdf and Content-Disposition: attachment; filename="document-draft.pdf".

Error Responses

404 Invalid token or envelope not found   409 Already signed   410 Envelope voided or expired

Email Delivery Log

GET/api/v1/email-logJWT

View the email delivery log for envelopes you own. Shows delivery status, timestamps, and provider details for every email sent as part of the signing workflow (invitations, reminders, completions, etc.).

Query Parameters
FieldTypeDescription
envelope_idoptionalstringFilter by envelope ID
statusoptionalstringFilter by status: sent, delivered, bounced, failed
limitoptionalintegerResults per page (1-100, default: 50)
offsetoptionalintegerPagination offset (default: 0)
Response 200
{
  "emails": [
    {
      "id": "em_a1b2c3d4-...",
      "envelope_id": "e1a2b3c4-...",
      "to": "signer@example.com",
      "type": "signing_invitation",
      "status": "delivered",
      "provider": "zeptomail",
      "sent_at": "2026-03-11T10:00:00Z",
      "delivered_at": "2026-03-11T10:00:02Z"
    }
  ],
  "total": 15,
  "limit": 50,
  "offset": 0
}

Current User

GET/api/v1/auth/meJWT / Cookie

Get the currently authenticated user's profile, sign-specific plan tier, and current month envelope usage. Accepts JWT via Authorization header or __Secure-abundera_session cookie.

Response 200 (authenticated)
{
  "authenticated": true,
  "id": "u1a2b3c4-...",
  "email": "you@company.com",
  "name": "Your Name",
  "role": "owner",
  "sign_tier": "professional",
  "envelope_limit": 200,
  "envelopes_used": 42,
  "year_month": "2026-03"
}
Response 200 (not authenticated)
{
  "authenticated": false
}

Health Check

GET/api/v1/healthPublic

Check platform health — verifies D1 database, KV store, and R2 storage connectivity.

Response 200
{
  "status": "healthy",
  "checks": {
    "d1": "ok",
    "kv": "ok",
    "r2": "ok"
  },
  "timestamp": "2026-03-05T12:00:00Z"
}

503 returned when any check fails (status: "degraded")

Usage Stats

GET/api/v1/usageJWT

Get your current plan tier, monthly envelope limits, and usage for the current billing period.

Response 200
{
  "plan": "professional",
  "monthly_limit": 200,
  "used": 42,
  "remaining": 158,
  "year_month": "2026-03",
  "overage_rate": "$1.00"
}

Join Waitlist

POST/api/v1/waitlistPublic

Join the product waitlist. No authentication required. Protected by honeypot field — bots that fill hidden fields are silently rejected.

Request Body
FieldTypeDescription
emailrequiredstringEmail address to add to the waitlist
Response 200
{
  "ok": true
}

Contact Form

POST/api/v1/contactPublic

Submit a contact form message. No authentication required. Protected by honeypot field — bots that fill hidden fields are silently rejected.

Request Body
FieldTypeDescription
namerequiredstringSender's full name
emailrequiredstringSender's email address
messagerequiredstringMessage content
Response 200
{
  "ok": true
}

List Global Webhooks

GET/api/v1/webhooks/manageJWT

List all global webhook endpoints configured for your account. Global webhooks receive events for all envelopes, unlike per-envelope callback_url.

Response 200
{
  "webhooks": [
    {
      "id": "wh_abc123",
      "url": "https://api.example.com/webhooks/sign",
      "events": ["envelope.completed", "signer.signed"],
      "active": true,
      "created_at": "2026-03-15T10:00:00Z"
    }
  ]
}

Create Global Webhook

POST/api/v1/webhooks/manageJWT

Register a global webhook endpoint to receive events for all envelopes.

Request Body
FieldTypeDescription
urlrequiredstringHTTPS endpoint URL
eventsoptionalarrayEvent types to subscribe to. Default: all events.
secretoptionalstringHMAC secret for signature verification. Auto-generated if not provided.
Response 201
{
  "id": "wh_abc123",
  "url": "https://api.example.com/webhooks/sign",
  "secret": "whsec_...",
  "events": ["envelope.completed", "signer.signed"]
}
Error Responses

400 Invalid URL or event type   409 URL already registered

Delete Global Webhook

DELETE/api/v1/webhooks/manageJWT

Remove a global webhook endpoint.

Request Body
FieldTypeDescription
idrequiredstringWebhook ID to delete
Response 204

No content.

Error Responses

404 Webhook not found

Create Notarization Session

POST/api/v1/notarizeJWTBusiness+

Create a Remote Online Notarization (RON) session for a completed envelope via BlueNotary. The signer will join a live video session with a commissioned notary.

Request Body
FieldTypeDescription
envelope_idrequiredstringCompleted envelope ID
Response 201
{
  "notarization_id": "not_abc123",
  "session_url": "https://app.bluenotary.us/session/...",
  "status": "pending"
}
Error Responses

400 Envelope not completed   403 Business plan required   409 Notarization already exists

Get Notarization Status

GET/api/v1/notarizeJWTBusiness+

Check the status of a notarization session. Returns session details, notary info (when assigned), and download URL for the notarized certificate.

Query Parameters
FieldTypeDescription
envelope_idrequiredstringEnvelope ID
Response 200
{
  "notarization_id": "not_abc123",
  "status": "completed",
  "notary_name": "Jane Smith, Notary Public",
  "completed_at": "2026-04-06T14:30:00Z",
  "certificate_url": "/api/v1/download?envelope_id=...&type=notarization"
}
Error Responses

404 Notarization not found

PAYG Balance

GET/api/v1/payments/payg-balanceJWT

Get the current Pay-As-You-Go envelope balance for the authenticated user. Shows remaining envelopes, pack details, and expiration dates.

Response 200
{
  "balance": 42,
  "packs": [
    {
      "id": "pack_abc123",
      "quantity": 50,
      "remaining": 42,
      "expires_at": "2027-04-06T00:00:00Z"
    }
  ]
}

PAYG Checkout

POST/api/v1/payments/payg-checkoutJWT

Create a Stripe checkout session to purchase a Pay-As-You-Go envelope pack. Redirects the user to Stripe for payment.

Request Body
FieldTypeDescription
quantityrequiredintegerNumber of envelopes to purchase (5, 25, 50, or 100)
Response 200
{
  "checkout_url": "https://checkout.stripe.com/c/pay/cs_..."
}
Error Responses

400 Invalid quantity

Cloud Import Config

GET/api/v1/cloud-import/configJWT

Get configuration for cloud document import providers. Returns available providers (Google Drive, Dropbox, OneDrive, Box, iCloud) and their OAuth/picker configuration.

Response 200
{
  "providers": [
    { "id": "google_drive", "name": "Google Drive", "enabled": true },
    { "id": "dropbox", "name": "Dropbox", "enabled": true },
    { "id": "onedrive", "name": "OneDrive", "enabled": true },
    { "id": "box", "name": "Box", "enabled": true },
    { "id": "icloud", "name": "iCloud", "enabled": true }
  ]
}

Cloud Import Proxy

POST/api/v1/cloud-import/proxyJWT

Proxy a file download from a cloud provider. Fetches the document and returns its content for use in envelope creation. Supports PDF, DOCX, and text files.

Request Body
FieldTypeDescription
providerrequiredstringCloud provider ID (google_drive, dropbox, onedrive, box, icloud)
file_urlrequiredstringProvider-specific file URL or ID
access_tokenrequiredstringOAuth access token for the provider
Response 200
{
  "filename": "contract.pdf",
  "content_type": "application/pdf",
  "size": 245760,
  "content_base64": "JVBERi0xLjQK..."
}
Error Responses

400 Invalid provider or missing access token   422 File too large or unsupported format

User Notifications

GET/api/v1/notificationsJWT

Get recent notifications for the authenticated user. Includes envelope events (signed, completed, declined, expired), team activity, and system notifications.

Query Parameters
FieldTypeDescription
limitoptionalintegerMax notifications to return default: 20, max: 100
offsetoptionalintegerPagination offset default: 0
Response 200
{
  "notifications": [
    {
      "id": "ntf_abc123",
      "type": "signer.signed",
      "message": "Jane Doe signed NDA",
      "envelope_id": "e3f8a1b2-...",
      "read": false,
      "created_at": "2026-04-06T10:00:00Z"
    }
  ],
  "total": 47
}

Founding Members

GET/api/v1/founding-membersPublic

Get the public list of founding members. No authentication required. Returns founding members who opted in to public display.

Response 200
{
  "members": [
    {
      "name": "Acme Corp",
      "joined_at": "2026-03-01T00:00:00Z",
      "badge": "founding"
    }
  ],
  "count": 12
}

Evidence Package

Every completed envelope generates a comprehensive evidence package stored in R2:

ArtifactDescription
original.mdTemplate markdown snapshot (immutable record of what was presented)
field-values.jsonAll field values with metadata and template version
signatures/{role}.jsonPer-signer signature evidence (type, data, consent, IP, UA, timestamp, identity photo, GPS coordinates)
audit-trail.jsonHash-chained audit log snapshot with integrity verification
evidence-manifest.jsonSHA-256 hashes of every artifact + envelope metadata
signed.pdfCertified PDF with Certificate of Completion + Cryptographic Proof pages
timestamp.tsrRFC 3161 trusted timestamp response (ASN.1/DER encoded)
Third-party anchoring: The evidence manifest hash is also published to GitHub (abundera/audit-anchors) as an independent, tamper-evident record outside Cloudflare's control.