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 YAML OpenAPI JSON Postman Collection cURL 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 portal.

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 5 minutes. 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)60/min per IP
POST /api/v1/envelopes10/min
POST /api/v1/orgs5/min
POST /api/v1/demo-envelope3/min
GET /api/v1/document-summary10/min
GET /api/v1/verify20/min
GET /api/v1/document-pdf5/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

When all signers complete an envelope, Abundera Sign sends an HMAC-SHA256 signed webhook to your callback_url.

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"
}

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)
);

Create Envelope

POST /api/v1/envelopes JWT

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. 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/envelopes JWT

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/:id JWT

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-status JWT / 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-envelope JWT

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/bulk JWT

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" }
  ]
}

Demo Envelope

POST /api/v1/demo-envelope Public

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/sign Token

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/decline Token

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/remind JWT

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-signer JWT

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/delegate Token

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-envelope JWT

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

Download Signed PDF

GET /api/v1/download JWT / 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/verify Public

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-summary Token

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": "claude-haiku-4-5-20251001",
  "cached": false
}

Get Document Data

GET /api/v1/document-data Token

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/templates JWT

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/templates JWT

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/:id JWT

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

Update Template

PUT /api/v1/templates/:id JWT

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/:id JWT

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

List Comments

GET /api/v1/envelopes/:id/comments JWT

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

Add Comment

POST /api/v1/envelopes/:id/comments JWT

Add a comment to an envelope. Max 2000 characters.

Request Body
FieldTypeDescription
commentrequiredstringComment text (max 2000 chars)

Audit Trail

GET /api/v1/envelopes/:id/audit JWT

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.

Court-Ready Declaration

GET /api/v1/envelopes/:id/declaration JWT Business+

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.

Request Court Declaration

POST /api/v1/declaration-request Public

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/approve JWT

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/orgs JWT Business+

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/orgs JWT

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/:id JWT

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/:id JWT

Update organization name. Requires admin+ role.

Request Body
FieldTypeDescription
namerequiredstringNew organization name. Max 100 characters.

Delete Organization

DELETE /api/v1/orgs/:id JWT

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/members JWT

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/:uid JWT

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/:uid JWT

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

Invite Member

POST /api/v1/orgs/:id/invitations JWT

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/invitations JWT

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/:iid JWT

Revoke a pending invitation. Requires admin+ role.

Accept Invitation

POST /api/v1/orgs/accept-invite JWT

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/branding JWT

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/branding JWT

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/logo JWT

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

List Clauses

GET /api/v1/clauses JWT

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/clauses JWT

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/:id JWT

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/:id JWT

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/:id JWT

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/negotiate Token

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"
    }
  ]
}

Update Negotiation

PUT /api/v1/negotiate JWT

Sender responds to a negotiation flag. Can accept the objection, reject it, or provide a counter-proposal.

Request Body
FieldTypeDescription
envelope_idrequiredstringEnvelope ID
clause_idrequiredstringClause ID to respond to
actionrequiredstringResponse action: accept, reject, or counter
response_textoptionalstringCounter-proposal or explanation (max 2000 chars)
Response 200
{
  "negotiation_id": "neg_x1y2z3-...",
  "status": "accepted",
  "action": "accept",
  "updated_at": "2026-03-11T12:00:00Z"
}
Error Responses

400 Invalid action   403 Not the envelope sender   404 Negotiation not found

Withdraw Negotiation Flag

DELETE /api/v1/negotiate Token

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/respond JWT

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-links JWT

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-links JWT

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/claim Public

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

Create Document Package

POST /api/v1/packages JWT

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/packages JWT

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/send Token

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/verify Token

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-profile Token

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/session Token

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/verify Token

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/start Token

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/verify Token

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/checkout Token / 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-sender Token

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-compare Token

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-log JWT

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/me JWT / 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/health Public

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/usage JWT

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/waitlist Public

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/contact Public

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
}

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.