ctSignature

Developer Integration Manual

Complete guide to API integration, authentication, webhooks, templates, multi-signer workflows, billing, and dashboard features.

Version 1.3 — August 2026

CozziTech LLC

Table of Contents

  1. Getting Started
    1. Platform Overview
    2. Base URLs & Environments
    3. Quick Start (5 Minutes)
  2. Authentication
    1. API Keys
    2. JWT Bearer Tokens
    3. Platform Admin Tokens
    4. Signing & Placement Tokens
  3. Documents API
    1. Create a Document
    2. Create a Multi-Signer Document
    3. Where Signer Replies Go
    4. Who Emails the Signer
    5. List Documents
    6. Get Document Details
    7. Check Document Status
    8. Resend / Renew a Document
    9. Void a Document
    10. Delete a Document
    11. Archive a Document
    12. Download Signed PDF
    13. Download as Separate Files
    14. Verify a Signed Document
    15. Embedded Signing Sessions
  4. Templates API
    1. Create a Template
    2. Anchored Fields (Mail-Merge Forms)
    3. Send Your Own PDF
    4. Send from a Template
    5. Batch Send
  5. Recipients API (Address Book)
  6. Webhooks
    1. Setting Up Webhooks
    2. Event Types
    3. Payload Format
    4. Verifying Signatures
    5. Retries & Deliveries
  7. Signing Workflow
    1. Single-Signer Flow
    2. Multi-Signer Flow
    3. Consent & OTP
  8. Billing & Subscriptions
  9. Partner API (OEM Provisioning)
    1. Partner Key Auth
    2. One-Call Tenant Onboarding
    3. Endpoint Reference
  10. Dashboard Guide
    1. Documents
    2. Templates
    3. User Management
    4. Settings & Branding
    5. Analytics
  11. Configuration Reference
  12. Error Handling
  13. Security & Compliance

Section 1

Getting Started

Platform Overview

ctSignature is a multi-tenant, API-first document signing platform. It lets you send PDFs for legally binding electronic signatures, track signer progress, and store signed documents with a full audit trail.

Key capabilities:

Base URLs & Environments

EnvironmentBase URLNotes
Production https://ctsign.io Default for hosted ctSignature. Self-hosted deployments set their own via DocumentSigning:ProductionBaseUrl.
Development http://localhost:8080 Test mode enabled, email sending skipped

All API paths in this manual are relative to the base URL.

Quick Start (5 Minutes)

One-time UI setup (do this first — takes ~2 minutes)

Before you can call the API, you need an account and an API key. There’s no API to create either — both require the browser. Once you have a key, every step below can run from your backend.

  1. Sign up. Go to ctsign.ioor create a new account. Fill in Company Name, Your Name, Email → Create Account.
  2. Set your password. Click Continue to password setup (also emailed to you). You’ll be handed off to ctOneAuth — set a password, verify your email, and you’ll bounce back to the dashboard already signed in.
  3. Create an API key. In the dashboard sidebar, click DeveloperAPI Keys tab → type a label (e.g. Production) → Create Key. Copy the ctds_live_… value now — it’s shown once.
  4. (Optional) Register a webhook. Developer → Webhooks tab → Add Webhook. Paste your receiving URL, pick events. Copy the signing secret (shown once).
  5. (Optional) Allow iframe embedding. Developer → Embedded Signing tab → toggle on, add your app’s origin (e.g. https://app.yourbusiness.com), save. Required only if you embed the signing/placement pages in your own UI.

Once you’ve copied the ctds_live_… key, the rest of this guide is API calls only.

With your API key in hand, here’s the canonical document-signing flow:

1
Set your API key in your environment
export CTDS_KEY="ctds_live_aBcDeFgHiJkLm..."

If you prefer to skip the browser entirely and provision the account via API, see POST /api/auth/register in Section 2.2 — it returns a setupUrl the user opens once to set a password, then a subsequent GET /api/auth/oidc/login yields a JWT, after which you can call POST /api/dashboard/api-keys to mint a key programmatically. Most integrators just use the UI one-time setup above.

2
Upload a document
POST /api/v1/documents
Authorization: Bearer ctds_live_aBcDeFgHiJkLm...
Content-Type: multipart/form-data

RecipientName: John Smith
RecipientEmail: john@example.com
File: contract.pdf

You get back a placementUrl (to position signature fields) and a signingUrl (to send to the signer).

Skip placement entirely by using a template with fields already saved — POST /api/v1/templates/{id}/send mails the signer directly.

3
Place signature fields

Open the placementUrl in a browser. Drag and drop the signature, printed name, date, and initials fields onto the document. Click Save & Send.

4
Signer signs

The signer receives an email with a link (or you send them the signingUrl directly). They review the document, give consent, type their signature, and submit.

5
Download the signed PDF
GET /api/documents/signed/{documentId}?token=<secureToken>

Uses the secure token from step 2 — no Authorization header needed. The signed PDF has a Certificate of Completion appended. If you prefer JWT auth, use GET /api/dashboard/documents/{documentId}/download instead.

You're up and running!
That's the core flow. Read on for multi-signer support, templates, webhooks, and embedded signing.

Section 2

Authentication

ctSignature supports four types of authentication, each for a different purpose.

API Keys (Programmatic Access)

API keys are the primary way to call the REST API from your backend code. Each key is tied to a single tenant and gives full read/write access to that tenant's data.

Key Format

Keys follow the pattern ctds_<random-characters>. Example:

ctds_aB3cDeFgH1iJkLmN2oPqRsT3uVwXy

How to Send

Include the key in the Authorization header using the Bearer scheme:

Authorization: Bearer ctds_aB3cDeFgH1iJkLmN2oPqRsT3uVwXy

An X-Api-Key header carrying the same key is also accepted, if that suits your HTTP client better:

X-Api-Key: ctds_aB3cDeFgH1iJkLmN2oPqRsT3uVwXy

Generating a Key

POST /api/dashboard/api-keys
Authorization: Bearer <jwt-token>
Content-Type: application/json

{
  "name": "Production Server"    // optional label
}

// Response
{
  "id": 1,
  "key": "ctds_aB3cDeFgH1iJkLmN2oPqRsT3uVwXy",   // shown only once!
  "prefix": "ctds_aB3",
  "name": "Production Server",
  "createdDate": "2026-04-17T10:00:00Z"
}
Important
The full key is only returned at creation time. Store it securely (e.g., in environment variables or a secrets manager). If you lose it, revoke and create a new one.

Revoking a Key

DELETE /api/dashboard/api-keys/{keyId}
Authorization: Bearer <jwt-token>

Endpoints That Accept API Keys

Path PrefixDescription
/api/v1/documentsCreate, list, get, delete documents
/api/v1/templatesCreate, send, batch-send templates
/api/v1/recipientsManage address book
/api/v1/webhooksManage webhooks

JWT Bearer Tokens (Dashboard & User Sessions)

JWT tokens are used by the web dashboard and by applications that need user-level authentication (for example, if your app lets users log in to manage their own signing settings).

Getting a Token

Identity is managed by ctOneAuth (OIDC). ctSignature does not store passwords. Registration creates the local tenant and provisions the org in ctOneAuth in a single call; sign-in always goes through the OIDC flow at GET /api/auth/oidc/login, which redirects to ctOneAuth and finishes by minting a JWT for the dashboard.

Register a New Account
POST /api/auth/register
Content-Type: application/json

{
  "companyName": "Acme Corp",
  "name": "Jane Developer",
  "email": "jane@acme.com"
}

// Response — no password is set here; user finishes setup on ctOneAuth.
{
  "setupUrl": "https://ctoneauth.example/setup?token=...",
  "tenant": {
    "id": 1,
    "companyName": "Acme Corp",
    "email": "jane@acme.com",
    "isTrial": true
  },
  "user": {
    "id": 1,
    "name": "Jane Developer",
    "role": "Admin"
  }
}
Sign In
GET /api/auth/oidc/login
// 302 → ctOneAuth /oauth2/authorize → /signin-oidc → /api/auth/oidc/complete
// On success, a JWT is delivered to /dashboard/oidc-complete.html via URL fragment.

How to Send

Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Token Details

PropertyValue
AlgorithmHMAC-SHA256
Lifetime24 hours (configurable)
IssuerJwt:Issuer setting
AudienceJwt:Audience setting

JWT Claims

ClaimDescription
subTenant ID (number as string)
user_idTenant user ID (if multi-user tenant)
roleUser role: Admin, User, or Reviewer
emailUser's email address
iatIssued-at timestamp
expExpiration timestamp

Check Current User

GET /api/auth/me
Authorization: Bearer <jwt-token>

// Response
{
  "tenant": { "id": 1, "companyName": "Acme Corp", ... },
  "user": { "id": 1, "name": "Jane Developer", "role": "Admin" }
}

Platform Admin Tokens

Platform admin tokens give access to system-wide management endpoints. These credentials are set in the server configuration, not in the database.

POST /api/admin/auth/login
Content-Type: application/json

{
  "email": "<PlatformAdmin:Email from config>",
  "password": "<PlatformAdmin:Password from config>"
}

// Response
{
  "token": "eyJhbGciOiJIUzI1NiIs...",
  "email": "admin@ctsignature.com",
  "role": "platform_admin"
}

Use this token with /api/admin/* endpoints only.

Signing & Placement Tokens

These are single-use, document-specific tokens embedded in URLs. They do not require any auth header — the token in the URL is the authentication.

Token TypeUsed ForURL PatternExpires
Placement Token Positioning signature fields on the PDF /sign/place/{token} Same as document expiration (default 72 hours)
Secure Token (Signing) Signing the document /sign/document/{token} Same as document expiration (default 72 hours)

For multi-signer documents, each signer gets their own unique secure token and signing URL.

Section 3

Documents API

The Documents API lets you create, track, and manage documents for signing. All endpoints below use API key authentication unless noted otherwise.

Create a Document (Single Signer)

POST /api/v1/documents API Key

Upload a PDF or Word document and specify who should sign it. Word files (.doc/.docx) are converted to PDF on upload — the stored, placed, and signed document is always a PDF.

Request

Content-Type: multipart/form-data

FieldTypeRequiredDescription
FilefileYes*PDF or Word file (max 10 MB). Word files are converted to PDF. *Provide either File or Files.
Filesfile (repeatable)Yes*Multiple PDF/Word files, merged into one document in the order sent (max 10 files).
RecipientNamestringYesSigner's full name
RecipientEmailstringYesSigner's email address
ExpirationHoursintegerNoHours until signing link expires (default: 72)
NotificationEmailstringNoEmail address that receives the "document signed" notifications. If omitted, no notification is sent for API-created documents (there is no fallback to the tenant-level email).
ReplyToEmailstringNoWhere a signer's reply to this document's emails is delivered. See Where signer replies go.
SendInvitebooleanNoWhether ctSignature emails the signer their invite. Omitted keeps this path's default. See Who Emails the Signer.
RemindersEnabledbooleanNoDefault true. ctSignature emails the signer a daily reminder until signed (max 4); each reminder renews the link's expiry and notifies you. Pass false if your app handles all signer communication.
FieldsJsonJSON array (string)NoInline field placement — skips the placement step; the document is created already sent. See "Inline field placement" below.

Example

curl -X POST https://ctsign.io/api/v1/documents \
  -H "Authorization: Bearer ctds_yourApiKey" \
  -F "RecipientName=John Smith" \
  -F "RecipientEmail=john@example.com" \
  -F "ExpirationHours=48" \
  -F "NotificationEmail=sender@acme.com" \
  -F "File=@contract.pdf"

Response (200 OK)

{
  "documentId": 42,
  "placementUrl": "https://ctsign.io/sign/place/abc123...",
  "signingUrl": "https://ctsign.io/sign/document/xyz789...",
  "placementToken": "abc123...",
  "secureToken": "xyz789...",
  "expirationDate": "2026-05-19T10:00:00Z"
}
Next step
Open the placementUrl to position signature fields on the PDF. Every field type can be placed multiple times — signatures, initials, dates, text boxes — and the signer completes each one individually. Once placed, the signing link becomes active and the signer receives an email.

Inline field placement (FieldsJson)

Pass a JSON array of field objects to place fields at creation time and skip the placement page entirely. At least one signature field is required; the response then has "skippedPlacement": true and no placementUrl.

-F 'FieldsJson=[
  {"type":"signature","x":100,"y":620,"width":150,"height":32,"page":2},
  {"type":"date","x":280,"y":635,"width":100,"height":20,"page":2},
  {"type":"initials","x":480,"y":700,"width":100,"height":24,"page":1},
  {"type":"text","x":100,"y":200,"width":180,"height":28,"page":1,"isRequired":false},
  {"type":"sender_text","x":100,"y":100,"width":220,"height":30,"page":1,
   "value":"Please initial every page and sign on page 2."}
]'

Coordinates are PDF points with the origin at the top-left of each page; page is 1-based. Field types: signature, initials, date, datetime, text (signer fills in), name (auto: printed name), stamp (auto: digital stamp block), and sender_text (your own text, stamped into the PDF — requires value). All types are repeatable; isRequired defaults to true and signatures are always required.

Create a Multi-Signer Document

POST /api/v1/documents/multi-signer API Key

Create a document that requires two or more signers.

Request

Content-Type: multipart/form-data

FieldTypeRequiredDescription
FilefileYes*PDF or Word file (max 10 MB). Word files are converted to PDF. *Provide either File or Files.
Filesfile (repeatable)Yes*Multiple PDF/Word files, merged into one document in the order sent (max 10 files).
SignersJSON arrayYesArray of signer objects (see below), sent as a JSON-encoded string. Indexed form fields (Signers[0].SignerName, ...) are also accepted.
WorkflowTypestringYessequential or parallel
ExpirationHoursintegerNoHours until expiration (default: 72)
NotificationEmailstringNoEmail address that receives the "document signed" notifications. If omitted, no notification is sent for API-created documents (there is no fallback to the tenant-level email).
ReplyToEmailstringNoOne value for the whole document — it covers every signer. See Where signer replies go.
RemindersEnabledbooleanNoDefault true. Daily reminders to each pending signer until they sign (max 4 per signer; sequential workflows only remind whoever's turn it is).

Signer Object

FieldTypeRequiredDescription
SignerNamestringYesSigner's full name
SignerEmailstringYesSigner's email
SignerRolestringNoRole label (e.g., "Manager", "Legal")
SignOrderintegerNoSigning order (for sequential workflow)

Example

curl -X POST https://ctsign.io/api/v1/documents/multi-signer \
  -H "Authorization: Bearer ctds_yourApiKey" \
  -F "WorkflowType=sequential" \
  -F 'Signers=[{"SignerName":"Alice","SignerEmail":"alice@acme.com","SignOrder":1},{"SignerName":"Bob","SignerEmail":"bob@acme.com","SignOrder":2}]' \
  -F "File=@agreement.pdf"

Response (200 OK)

{
  "documentId": 43,
  "workflowType": "sequential",
  "placementUrl": "https://ctsign.io/sign/place/abc...",
  "signers": [
    {
      "signerId": 1,
      "signerName": "Alice",
      "signerEmail": "alice@acme.com",
      "signOrder": 1,
      "status": "pending",
      "signingUrl": "https://ctsign.io/sign/document/token_alice..."
    },
    {
      "signerId": 2,
      "signerName": "Bob",
      "signerEmail": "bob@acme.com",
      "signOrder": 2,
      "status": "pending",
      "signingUrl": "https://ctsign.io/sign/document/token_bob..."
    }
  ]
}
Sequential vs. Parallel
Sequential: signers must sign in order. Signer 2 cannot sign until Signer 1 is done. Each signer is emailed when it's their turn.
Parallel: all signers can sign at the same time. Everyone gets their link immediately.

Where Signer Replies Go

Signers reply to signing emails — with questions, corrections, or "is this legitimate?". By default those replies reach ctSignature's own notification mailbox, which is almost never who the signer meant to write to.

ReplyToEmail sets the Reply-To header on every signer-facing email for that document: the signing request, the daily reminders, the next-signer notice, and the verification-code email. Point it at whoever should field the signer's question.

curl -X POST https://ctsign.io/api/v1/documents \
  -H "Authorization: Bearer ctds_yourApiKey" \
  -F "RecipientName=John Smith" \
  -F "RecipientEmail=john@example.com" \
  -F "ReplyToEmail=casemanager@youragency.com" \
  -F "File=@contract.pdf"

Binding. A plain string form field on multipart requests (ReplyToEmail=...), a plain string property on the JSON template endpoints ("replyToEmail": "..."). Not indexed, not JSON-encoded, and one value per document — it covers every signer on a multi-signer document. Invalid addresses are rejected with 400.

The From address never changes
Only Reply-To moves — the same "on behalf of" pattern DocuSign and Adobe Sign use. SPF, DKIM and DMARC all align on the From domain, so this affects neither authentication nor deliverability, and nothing needs configuring on your domain.

Resolution order

When ReplyToEmail is omitted, the first match wins:

  1. ReplyToEmail on the document
  2. the email of the signed-in user who created it (dashboard sends)
  3. the tenant-wide Reply-To Address (Dashboard → Branding, or replyToEmail on the partner tenant API)
  4. the platform default — replies come back to ctSignature
API-key callers: step 2 can never fire
API-key authentication carries no user identity — the server resolves the tenant and an admin-level role, but no user id — so a document created with a ctds_ key is stored with no sender user, by design. It is not inferred from the key’s creator or from the tenant admin. Either send ReplyToEmail on every create call, or set the tenant-wide default once. Doing both is fine; per-document wins.

Reminders, the next-signer notice and the OTP email are always ctSignature’s, so ReplyToEmail always covers those. The invite is the one that varies — see Who Emails the Signer.

Who Emails the Signer Their Invite

Historically this depended on whether a placement step happened, which is not obvious from the outside:

PathPlacementInvite emailed by (SendInvite omitted)
POST /documents, no FieldsJsonhuman opens placementUrlctSignature, at placement time
POST /documents with FieldsJsonskippednobody — you deliver signingUrl
POST /documents/multi-signerhuman opens placementUrlctSignature, at placement time
templates/{id}/send — single-signerskipped (pre-placed)nobody
templates/{id}/send — multi-signerskipped (pre-placed)ctSignature
templates/{id}/batch-send — single-signerskippednobody
templates/{id}/batch-send — multi-signerskippedctSignature
Dashboard sendeitherctSignature

SendInvite overrides all of that:

ValueBehaviour
omitted (default)the per-path behaviour above — nothing changes for existing integrations
truectSignature always emails the signer, on every path
falsectSignature never emails the signer — you deliver signingUrl
curl -X POST https://ctsign.io/api/v1/documents \
  -H "Authorization: Bearer ctds_yourApiKey" \
  -F "RecipientName=Sarah Johnson" \
  -F "RecipientEmail=sarah@example.com" \
  -F "ReplyToEmail=casemanager@youragency.com" \
  -F 'FieldsJson=[{"type":"signature","x":100,"y":620,"width":150,"height":32,"page":1}]' \
  -F "SendInvite=true" \
  -F "File=@contract.pdf"

Binding matches ReplyToEmail: plain form field on multipart, "sendInvite": true on the JSON template endpoints. It is stored on the document, so it still applies when the invite is sent later at placement time.

Why it defaults to “omitted” rather than true
Most existing integrations deliver signingUrl themselves. Had the invite simply been switched on, every one of those signers would get two emails — the integration’s and ctSignature’s. If you adopt SendInvite=true, delete your own invite send in the same change. Conversely SendInvite=false does not stop reminders — pass RemindersEnabled=false too if you own all signer communication.

Owner-facing notifications — "document signed", "all signatures complete", "signing link expired", "reminder sent" — are unaffected. They already arrive with you, so their replies stay with ctSignature.

List Documents

GET /api/v1/documents API Key

Query Parameters

ParameterTypeDefaultDescription
statusstring(all)Filter: pending, sent, signed, expired
includeArchivedbooleanfalseArchived documents are hidden from the list by default; pass true to include them. Each item carries archivedAt (null unless archived) so you can tell them apart. See Archive a Document.
pageinteger1Page number
pageSizeinteger20Results per page (1–100)

Response (200)

{
  "items": [
    {
      "id": 42,
      "originalFilename": "contract.pdf",
      "recipientName": "John Smith",
      "recipientEmail": "john@example.com",
      "status": "signed",
      "expirationDate": "2026-05-19T10:00:00Z",
      "createdDate": "2026-05-17T10:00:00Z",
      "signedDate": "2026-05-17T14:30:00Z",
      "isExpired": false,
      "hasPlacement": true
    }
  ],
  "page": 1,
  "pageSize": 20,
  "totalCount": 1,
  "hasMore": false
}
About the expired filter
status on a document is never literally "expired" — expiration is derived from the timestamp. The list endpoint accepts ?status=expired as a convenience filter (returns unsigned documents past their expirationDate), but each item still reports its underlying status (e.g. sent) along with isExpired: true.

Get Document Details

GET /api/v1/documents/{id} API Key

Returns the document’s status, signer audit entries, and any required-initials field placements.

Response (200)

{
  "id": 42,
  "originalFilename": "contract.pdf",
  "recipientName": "John Smith",
  "recipientEmail": "john@example.com",
  "status": "signed",
  "workflowType": "single",
  "expirationDate": "2026-05-19T10:00:00Z",
  "createdDate": "2026-05-17T10:00:00Z",
  "modifiedDate": "2026-05-17T14:30:00Z",
  "signedDate": "2026-05-17T14:30:00Z",
  "isExpired": false,
  "hasPlacement": true,
  "signedPdfHash": "a1b2c3d4e5f6...",
  "sourceFileNames": ["ISP.pdf", "Goods Form.pdf"],
  "placementUrl": null,
  "signingUrl": null,
  "verifiedBadgeApplied": true,
  "initialsFields": [
    { "id": 1, "x": 450, "y": 700, "width": 60, "height": 30, "page": 2, "isRequired": true }
  ],
  "signatures": [
    {
      "id": 10,
      "signerName": "John Smith",
      "signerEmail": "john@example.com",
      "signatureFont": "Dancing Script",
      "ipAddress": "203.0.113.42",
      "platform": "Windows",
      "timezone": "America/New_York",
      "signatureHash": "a1b2c3d4e5f6...",
      "createdDate": "2026-05-17T14:30:00Z"
    }
  ],
  "signers": []
}

Each signature row names the signer who produced it (signerName / signerEmail). For multi-signer documents the response also carries a signers array — the same per-signer progress entries returned by the signers endpoint below; it is empty for single-signer documents, whose document-level status already tells the whole story.

Signature field coordinates
The detail response does not return the placed signature/printed-name/date-time field coordinates. Those are only used internally for rendering. The hasPlacement flag tells you whether placement has been completed.

Check Document Status

GET /api/v1/documents/{id}/status API Key

A lightweight endpoint to check the current status of a document and its signers.

Document Status Values

StatusMeaning
pendingCreated but fields not yet placed
sentFields placed, signing link is active
partially_signedSome (but not all) signers have signed (multi-signer only)
signedAll signers have signed

Expiration is not a stored status. To detect expired documents, check isExpired on the response, or pass ?status=expired to the list endpoint as a convenience filter.

Check Signer Status

GET /api/v1/documents/{id}/signers API Key

The signing-progress view of one document: who has signed (and when), whose turn it is right now, and who is still waiting to be emailed. Single-signer documents return one synthesized entry for the recipient, so you can render every document the same way.

Response (200 OK)

{
  "documentId": 43,
  "workflowType": "sequential",
  "status": "partially_signed",
  "isExpired": false,
  "signedCount": 1,
  "totalSigners": 2,
  "signers": [
    {
      "signerId": 1,
      "name": "Alice",
      "email": "alice@acme.com",
      "role": "Manager",
      "signOrder": 1,
      "status": "signed",
      "signedDate": "2026-05-17T14:30:00Z",
      "isNext": false
    },
    {
      "signerId": 2,
      "name": "Bob",
      "email": "bob@acme.com",
      "role": null,
      "signOrder": 2,
      "status": "sent",
      "signedDate": null,
      "isNext": true
    }
  ]
}

Resend / Renew a Document

POST /api/v1/documents/{id}/resend API Key

Get a working signing link back for a document that was sent but not yet signed — or whose link has already expired (still unsigned). The v1 API is URL-based (it never emails), so resend hands you a link to redeliver rather than sending mail.

No request body is required. Returns 400 if the document is already signed, or if a single-signer document's fields were never placed; 404 if not found. For multi-signer documents the response lists the actionable pending signers (the next signer for sequential, all pending signers for parallel) each with a fresh signingUrl.

Void a Document

POST /api/v1/documents/{id}/void API Key

Retract a document that hasn’t fully signed — the first half of a correct-and-reissue loop. Optional JSON body: { "reason": "..." } (max 500 chars; recorded in the audit trail and webhook).

Delete a Document

DELETE /api/v1/documents/{id} API Key

Delete a pending document. Signed documents cannot be deleted through the API — that rule is deliberate and permanent. To get a signed document out of your lists (test runs, stale records), use archive instead.

Archive a Document

POST /api/v1/documents/{id}/archive API Key
POST /api/v1/documents/{id}/unarchive API Key

Hide a document from default lists without touching the record — the sanctioned way to declutter signed documents, which can never be deleted or voided. Typical use: integration-test documents that completed for real but have no business value.

// Response (200)
{
  "message": "Document archived",
  "documentId": 42,
  "archivedAt": "2026-08-27T17:30:00Z"
}

Download Signed PDF

GET /api/v1/documents/{id}/download API Key

Downloads the fully signed PDF with the Certificate of Completion appended, once the document status is signed (404 with "Signed document not available yet" before that). The returned bytes are the verification artifact: their SHA-256 matches signedPdfHash from the detail endpoint. The download is logged in the audit trail.

Alternative routes for other callers:

GET /api/documents/signed/{id}?token=<secureToken> Token
GET /api/dashboard/documents/{id}/download JWT

Download as Separate Files

GET /api/v1/documents/{id}/separate-files API Key

For a signed document that was created from two or more uploaded files (multipart files on create), returns a ZIP containing each original file as its own signed PDF — cut along the page boundaries recorded when the files were merged — plus the Certificate of Completion pages as a separate Certificate of Completion.pdf. Signature and field content is flattened onto the pages at signing time, so every split file is a complete signed document.

Verify a Signed Document

ctSignature provides a public verification system. Anyone with the document's public ID can verify its authenticity.

Look Up Verification Info

GET /api/documents/verify/{publicId} Public
// Response
{
  "documentId": "550e8400-e29b-41d4-a716-446655440000",
  "status": "signed",
  "signedPdfHash": "a1b2c3d4e5f6...",
  "signers": [
    { "name": "John Smith", "signedDate": "2026-04-17T14:30:00Z" }
  ]
}

Verify a File's Hash

POST /api/documents/verify/{publicId} Public

Compute the SHA-256 hash of your PDF file and submit it to check whether it matches the original signed document.

POST /api/documents/verify/550e8400-e29b-41d4-a716-446655440000
Content-Type: application/json

{
  "hash": "a1b2c3d4e5f6..."
}

// Response
{
  "match": true,
  "documentId": "550e8400-e29b-41d4-a716-446655440000",
  "completedAt": "2026-04-17T14:30:00Z"
}

Embedded Signing Sessions

If your subscription includes embedded signing, you can host the signing experience inside your app using an iframe.

Step 1: Enable Embedded Signing

PUT /api/dashboard/embedded-signing
Authorization: Bearer <jwt-token>
Content-Type: application/json

{
  "enabled": true,
  "allowedDomains": ["https://app.yourdomain.com"]
}

Step 2: Create an Embedded Session

POST /api/v1/documents/{id}/embedded-session API Key
// Response
{
  "signingUrl": "https://ctsign.io/sign/document/xyz...?embed=true",
  "expiresAt": "2026-04-17T11:00:00Z"
}

Step 3: Embed in Your Page

<iframe
  src="https://ctsign.io/sign/document/xyz...?embed=true"
  width="100%"
  height="800"
  frameborder="0"
></iframe>

<script>
  window.addEventListener('message', function(event) {
    // Verify origin matches your ctSignature domain
    if (event.origin !== 'https://ctsign.io') return;

    if (event.data.type === 'ctSignature:signed') {
      console.log('Document signed!', event.data.documentId, event.data.signedPdfUrl);
      // Close the modal, refresh your record, etc.
    }
    if (event.data.type === 'ctSignature:error') {
      console.warn('Signing error:', event.data.title, event.data.message);
    }
  });
</script>

The iframe posts ctSignature:ready when loaded, ctSignature:signed (with documentId and signedPdfUrl) on completion, and ctSignature:error on a blocking error.

Quick Sign (pre-authenticated users)

For signers already authenticated inside your application, append &quick=true to the embedded signing URL. The signature dialog opens immediately: the signer types their name, confirms intent, and every signature, printed-name and date field completes from that one entry. Only engaged when no field needs per-field input (a text or initials field falls back to the standard click-each flow). Use plain embed=true for a signer who should review the document before signing.

Partners can additionally attest that their application obtains ESIGN/UETA consent from its own users (hostManagedConsent on the Partner API’s embedded-signing endpoint) — signers on host-delivered documents then skip the consent disclosure entirely, and each signature’s audit trail records consent as host-asserted. Email-delivered signers always see the disclosure.

Section 4

Templates API

Templates let you upload a PDF or Word document once, position the signature fields, and then reuse it for many recipients without re-uploading or re-placing fields each time. Word files are converted to PDF on upload.

Create a Template

POST /api/v1/templates API Key
FieldTypeRequiredDescription
FilefileYesPDF or Word file (Word is converted to PDF)
NamestringYesTemplate name (e.g., "Employee NDA")
DescriptionstringNoOptional description

You can also create a template from an existing signed document:

POST /api/v1/templates/from-document/{documentId} API Key

Requires a JSON body with the new template’s name (and optional description). The original (unsigned) PDF and all field placements are copied into a new template.

{
  "name": "Standard NDA",
  "description": "Copied from doc 42"
}

Other Template Endpoints

MethodPathDescription
GET/api/v1/templatesList all active templates
GET/api/v1/templates/{id}Get template details with field coordinates
PUT/api/v1/templates/{id}Update name or description
PUT/api/v1/templates/{id}/signersDefine the template’s signer roles
DELETE/api/v1/templates/{id}Deactivate template (soft delete)

Multi-Signer Templates

PUT /api/v1/templates/{id}/signers API Key

A template can require signatures from two or more parties. The template stores roles — “Client”, “Contractor”, “Witness” — not people. Each role gets its own signature, initials, date and text fields on the PDF; the actual names and email addresses are supplied at send time, along with whether the routing is sequential or parallel.

Send 2–10 roles, or an empty list to turn the template back into a single-signer one. This call replaces the whole role list: omit a role’s id to create it, include the id to keep it, and leave a role out to delete it (its placed fields go with it).

PUT /api/v1/templates/5/signers
Authorization: Bearer ctds_yourApiKey
Content-Type: application/json

{
  "signers": [
    { "roleName": "Client", "signOrder": 1 },
    { "roleName": "Contractor", "signOrder": 2 }
  ],
  "defaultWorkflowType": "sequential"
}

After defining roles, place each role’s fields. PUT /api/v1/templates/{id}/placement takes a per-role payload instead of a flat fields list; signerId is the role’s id. Every role needs at least one signature field, and sender_text boxes belong to documentFields because they are typed once by the sender, not by any signer.

PUT /api/v1/templates/5/placement

{
  "signers": [
    { "signerId": 11, "fields": [ { "type": "signature", "x": 100, "y": 620, "width": 150, "height": 32, "page": 1 } ] },
    { "signerId": 12, "fields": [ { "type": "signature", "x": 340, "y": 620, "width": 150, "height": 32, "page": 1 } ] }
  ],
  "documentFields": [
    { "type": "sender_text", "x": 100, "y": 700, "width": 220, "height": 30, "page": 1, "value": "Contract #4471" }
  ]
}
Reading the placement back
GET /api/v1/templates/{id} returns the roles under signers[], each with its own fields[] and a hasSignatureField flag. The template’s top-level fields[] holds only the sender-text boxes. hasPlacement is false until every role has a signature field.

Anchored Fields (Mail-Merge Forms)

Fixed coordinates assume the form never moves. A mail-merged form does move: the amount of content above a signature line changes per document, so the line drifts down the page — sometimes onto the next page. An anchored field is positioned by finding text in each sent document instead: put an invisible marker (white ~8pt text such as [[ctsig:sc:sign]]) in your source document where the field belongs, and the field is placed wherever that marker lands in every render.

Set anchors in the dashboard template editor (Anchor panel) or in the placement payload — each field object accepts:

PropertyDescription
anchorTextText to find (max 200 chars). Matched case-insensitively, whitespace ignored. When set, the field’s x/y/page are ignored; width/height still apply.
anchorOccurrenceWhich match to use when the text appears more than once (1-based, reading order). Default 1.
anchorColumnWordOptional word within the match that supplies the horizontal position.
anchorOffsetX / anchorOffsetYPoints to shift from the found text. Y grows downward; negative lifts the field.

Anchors resolve once, at send time, against that document’s text layer. A marker that can’t be found fails the send with HTTP 400 naming the field and marker — nothing is created or billed. Scanned (image-only) PDFs cannot be anchored: they have no text to search.

Send Your Own PDF Through a Template

POST /api/v1/templates/{id}/send-document API Key

Send from a Template copies the template’s stored PDF — right when the document never changes. When your application generates a different PDF every time (a mail merge), use this endpoint instead: the template contributes the roles and field definitions (usually anchored), and the document travels with the request. Multipart form-data; requires a multi-role template.

FieldTypeRequiredDescription
filefileYesThe PDF for this send (or files to merge several; Word converted)
SignersJSON stringYesOne entry per role: [{"templateSignerId": 11, "signerName": "...", "signerEmail": "..."}]
WorkflowTypestringYessequential or parallel
SendInviteboolNofalse to deliver signing URLs yourself (in-app flows)

ExpirationHours, RemindersEnabled, NotificationEmail and ReplyToEmail work as on other create endpoints. The response matches Send from a Template: documentId plus a signingUrl per signer.

Send from a Template

POST /api/v1/templates/{id}/send API Key

Creates a new document from the template with pre-placed fields and sends the signing link directly. No placement step needed. notificationEmail and replyToEmail (see Where Signer Replies Go) are accepted here and on batch send; on a batch, one replyToEmail applies to every document in the call.

POST /api/v1/templates/5/send
Authorization: Bearer ctds_yourApiKey
Content-Type: application/json

{
  "recipientName": "Sarah Johnson",
  "recipientEmail": "sarah@example.com",
  "expirationHours": 48,
  "replyToEmail": "casemanager@youragency.com"
}

// Response
{
  "documentId": 44,
  "signingUrl": "https://ctsign.io/sign/document/xyz...",
  "templateId": 5,
  "skippedPlacement": true
}

Sending a multi-signer template

When the template has roles, send a signers array instead of recipientName/recipientEmail, with one entry per role, plus the workflowType:

POST /api/v1/templates/5/send

{
  "workflowType": "sequential",
  "signers": [
    { "templateSignerId": 11, "signerName": "Sarah Johnson", "signerEmail": "sarah@example.com", "signOrder": 1 },
    { "templateSignerId": 12, "signerName": "Dan Reyes", "signerEmail": "dan@contractor.com", "signOrder": 2 }
  ]
}

// Response
{
  "documentId": 44,
  "workflowType": "sequential",
  "templateId": 5,
  "templateName": "Consulting Agreement",
  "signers": [
    { "signerId": 88, "roleName": "Client", "signerName": "Sarah Johnson",
      "signerEmail": "sarah@example.com", "signOrder": 1,
      "signingUrl": "https://ctsign.io/sign/document/abc...", "notified": true },
    { "signerId": 89, "roleName": "Contractor", "signerName": "Dan Reyes",
      "signerEmail": "dan@contractor.com", "signOrder": 2,
      "signingUrl": "https://ctsign.io/sign/document/def...", "notified": false }
  ]
}
Every role must be filled
Fill each role exactly once, or the call returns 400 naming the roles that are missing. notified: false means that signer has not been emailed yet because it is not their turn — their signingUrl is still valid if you would rather deliver it yourself.

Batch Send

POST /api/v1/templates/{id}/batch-send API Key

Send a template to up to 100 recipients in a single call. One document is created per recipient.

POST /api/v1/templates/5/batch-send
Authorization: Bearer ctds_yourApiKey
Content-Type: application/json

{
  "recipients": [
    { "name": "Alice Brown", "email": "alice@acme.com" },
    { "name": "Bob Green", "email": "bob@acme.com" },
    { "name": "Carol White", "email": "carol@acme.com" }
  ]
}

// Response
{
  "templateId": 5,
  "totalSent": 3,
  "documents": [
    { "documentId": 45, "recipientEmail": "alice@acme.com", "signingUrl": "..." },
    { "documentId": 46, "recipientEmail": "bob@acme.com", "signingUrl": "..." },
    { "documentId": 47, "recipientEmail": "carol@acme.com", "signingUrl": "..." }
  ]
}

Batch sending a multi-signer template

Send signerSets instead of recipients — one entry per document, each filling every role. workflowType applies to all documents in the batch. Up to 100 documents per call. Every set is validated before anything is created, so a bad row fails the whole call rather than leaving a half-sent batch.

POST /api/v1/templates/5/batch-send

{
  "workflowType": "parallel",
  "signerSets": [
    { "signers": [
        { "templateSignerId": 11, "signerName": "Alice Brown", "signerEmail": "alice@acme.com" },
        { "templateSignerId": 12, "signerName": "Dan Reyes", "signerEmail": "dan@contractor.com" }
    ]},
    { "signers": [
        { "templateSignerId": 11, "signerName": "Bob Green", "signerEmail": "bob@acme.com" },
        { "templateSignerId": 12, "signerName": "Dan Reyes", "signerEmail": "dan@contractor.com" }
    ]}
  ]
}
Response shape
Each entry in the response’s documents[] carries a signers[] array with one signing URL per person, in the same shape as the single multi-signer send above.

Section 5

Recipients API (Address Book)

Save frequently-used signers so you don't have to re-enter their info each time. Recipients are automatically added when you create multi-signer documents from the dashboard.

MethodPathDescription
GET/api/v1/recipientsList recipients (supports ?search= and ?includeInactive=true)
GET/api/v1/recipients/{id}Get recipient details
POST/api/v1/recipientsCreate recipient
PUT/api/v1/recipients/{id}Update recipient
DELETE/api/v1/recipients/{id}Deactivate recipient
POST/api/v1/recipients/{id}/reactivateReactivate a deactivated recipient

Create Recipient

POST /api/v1/recipients
Authorization: Bearer ctds_yourApiKey
Content-Type: application/json

{
  "name": "John Smith",
  "email": "john@example.com",
  "company": "Example Inc",
  "role": "VP of Sales",
  "phone": "+1-555-123-4567",
  "notes": "Prefers signing on mobile"
}

// Response
{
  "id": 1,
  "name": "John Smith",
  "email": "john@example.com",
  "company": "Example Inc",
  "role": "VP of Sales",
  "phone": "+1-555-123-4567",
  "notes": "Prefers signing on mobile",
  "isActive": true,
  "createdDate": "2026-04-17T10:00:00Z"
}

Section 6

Webhooks

Webhooks let your application receive real-time notifications when events happen in ctSignature — like when a document is signed, viewed, or completed.

Setting Up Webhooks

POST /api/v1/webhooks API Key
POST /api/v1/webhooks
Authorization: Bearer ctds_yourApiKey
Content-Type: application/json

{
  "url": "https://yourapp.com/webhooks/ctsignature",
  "events": ["document.completed", "signer.signed"],
  "description": "Production webhook"
}

// Response
{
  "id": 1,
  "url": "https://yourapp.com/webhooks/ctsignature",
  "events": ["document.completed", "signer.signed"],
  "secret": "a1b2c3d4e5f6...64-hex-chars...",    // shown only once!
  "isActive": true,
  "createdDate": "2026-04-17T10:00:00Z"
}
Save the secret
The secret is only returned when the webhook is created. Store it securely — you need it to verify incoming webhook payloads. If you lose it, delete the webhook and create a new one.

In production, the URL must use HTTPS.

Event Types

Subscribe to any combination of these events. If you don't specify events, you receive all webhook-emitting events.

EventFires When
document.createdA new document is uploaded via the API
document.sentSignature fields are placed and the signing link becomes active
signer.signedA signer submits their signature
document.completedAll signers have signed the document
More events are written to the audit log
The internal audit log records many additional events (consent given, OTP sent/verified, email delivered/bounced/opened, signer viewed, document downloaded, and more). These are visible in the dashboard audit trail but are not currently delivered as outbound webhooks. The four events above are the ones your endpoint will receive.

Payload Format

Webhooks are sent as HTTP POST requests with a JSON body:

{
  "id": "whd_123",
  "timestamp": "2026-05-17T14:30:00Z",
  "event": "signer.signed",
  "data": {
    "documentId": 42,
    "publicId": "550e8400-e29b-41d4-a716-446655440000",
    "filename": "contract.pdf",
    "signerName": "John Smith",
    "signerEmail": "john@example.com",
    "signedAt": "2026-05-17T14:30:00Z"
  }
}

Headers Sent

HeaderDescription
Content-Typeapplication/json
User-AgentctSignature-Webhook/1.0
X-Webhook-IdNumeric ID of the webhook endpoint receiving the event
X-TimestampUnix epoch seconds at the moment the signature was computed (used in the signing input — see below)
X-SignatureHex-encoded HMAC-SHA256. No prefix — just the hex digest.

Verifying Webhook Signatures

Always verify the X-Signature header before processing a webhook.

How it works

  1. Read the raw request body as a UTF-8 string (do not re-serialize the JSON — whitespace matters).
  2. Read the X-Timestamp header.
  3. Build the signing string: {timestamp}.{rawBody} (timestamp, a literal dot, then the body).
  4. Compute HMAC-SHA256 using your webhook secret. The secret is a 64-character hex string; decode it to bytes with hex-decoding before using it as the HMAC key.
  5. Hex-encode the result (lowercase) and compare to X-Signature using a constant-time comparison.
  6. Reject the request if the timestamp is more than a few minutes old to prevent replay.

Node.js Example

const crypto = require('crypto');

// Use express.raw({ type: 'application/json' }) so req.body is a Buffer.
function verifyWebhook(req, secret) {
  const rawBody = req.body.toString('utf8');
  const timestamp = req.headers['x-timestamp'];
  const signature = req.headers['x-signature'];
  if (!timestamp || !signature) return false;

  const expected = crypto
    .createHmac('sha256', Buffer.from(secret, 'hex'))
    .update(`${timestamp}.${rawBody}`)
    .digest('hex');

  const a = Buffer.from(signature, 'hex');
  const b = Buffer.from(expected, 'hex');
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Python Example

import hmac, hashlib

def verify_webhook(raw_body: bytes, secret: str, timestamp: str, signature: str) -> bool:
    signing_input = f"{timestamp}.".encode('utf-8') + raw_body
    expected = hmac.new(
        bytes.fromhex(secret),
        signing_input,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, signature.lower())

C# Example

using System.Security.Cryptography;
using System.Text;

bool VerifyWebhook(string rawBody, string secret, string timestamp, string signature)
{
    var key = Convert.FromHexString(secret);
    using var hmac = new HMACSHA256(key);
    var input = Encoding.UTF8.GetBytes($"{timestamp}.{rawBody}");
    var expected = Convert.ToHexString(hmac.ComputeHash(input)).ToLowerInvariant();
    return CryptographicOperations.FixedTimeEquals(
        Encoding.UTF8.GetBytes(expected),
        Encoding.UTF8.GetBytes(signature.ToLowerInvariant())
    );
}

Retries & Deliveries

Each event is queued and picked up by a background dispatcher that polls every ~15 seconds. If your endpoint returns a non-2xx status code (or times out after 10 seconds), ctSignature retries with the following backoff:

AttemptDelay after previous failure
1— (initial delivery, within ~15s of the event)
230 seconds
32 minutes
415 minutes
51 hour

After 5 failed attempts the delivery is marked as exhausted. You can view delivery history from the dashboard:

GET /api/dashboard/webhooks/{id}/deliveries?limit=25 JWT

limit is clamped to 1–100 (default 25).

To test your webhook endpoint without creating a real document:

POST /api/dashboard/webhooks/{id}/test JWT

Section 7

Signing Workflow

Single-Signer Flow

This is the complete lifecycle of a single-signer document:

1
Create Document — Upload PDF via API. Status: pending. You receive a placementUrl and signingUrl.
2
Place Fields — Open the placement URL in a browser. Drag signature, printed name, date, and initials fields onto the PDF. Click Save. Status changes to sent. An email is sent to the signer automatically.
3
Signer Opens Link — The signer clicks the link in their email. If consent is enabled, they see a disclosure and must accept it. If OTP is enabled, they verify their identity via a code sent to their email.
4
Signer Signs — The signer types their signature, chooses a font, fills in required initials fields, confirms their intent, and submits. Status changes to signed.
5
Completion — The signature is applied to the PDF. A SHA-256 hash is computed. A Certificate of Completion is appended. You receive a document.completed webhook. The signed PDF is available for download.

Multi-Signer Flow

Sequential Workflow

  1. Create multi-signer document with workflowType: "sequential"
  2. Place fields for all signers on the placement page
  3. Signer 1 receives an email. They sign.
  4. Only after Signer 1 completes does Signer 2 receive their email
  5. This continues through all signers in order
  6. When the last signer signs, the document status becomes signed

Parallel Workflow

  1. Create multi-signer document with workflowType: "parallel"
  2. Place fields for all signers
  3. All signers receive emails at the same time
  4. Signers can sign in any order
  5. When the last remaining signer completes, the document is fully signed

These features are configured per-tenant in the dashboard settings.

Consent Disclosure

When enabled, signers must accept a legal disclosure before they can sign. This is required for ESIGN Act compliance.

You can publish custom disclosure text. Each change creates a new version so you have a record of which version each signer accepted.

OTP (One-Time Password)

When enabled, signers must verify their identity by entering a 6-digit code sent to their email before they can sign. This adds an extra layer of identity verification.

Section 8

Billing & Subscriptions

Free Trial

New accounts start with a free trial that includes 3 documents. All features are available during the trial.

Subscription Tiers

GET /api/billing/tiers Public

Returns available subscription plans with pricing and included features.

// Response
[
  {
    "id": 1,
    "name": "Starter",
    "monthlyPriceCents": 2999,
    "includedDocuments": 50,
    "overagePriceCents": 150,
    "featureFlags": { "webhooks": true, "templates": true }
  },
  {
    "id": 2,
    "name": "Professional",
    "monthlyPriceCents": 7999,
    "includedDocuments": 200,
    "overagePriceCents": 100,
    "featureFlags": { "webhooks": true, "templates": true, "embedding": true }
  }
]

Check Usage

GET /api/billing/usage JWT
// Response
{
  "documentsUsedThisCycle": 37,
  "includedDocuments": 50,
  "trialRemaining": 0,
  "billingPeriodStart": "2026-04-01T00:00:00Z",
  "billingPeriodEnd": "2026-04-30T23:59:59Z"
}

Upgrade / Manage Subscription

To start or change a subscription:

POST /api/billing/checkout JWT (Admin)
POST /api/billing/checkout
Authorization: Bearer <jwt-token>
Content-Type: application/json

{ "tierId": 2 }

// Response
{ "checkoutUrl": "https://checkout.stripe.com/pay/cs_..." }

Redirect the user to checkoutUrl to complete payment on Stripe.

To manage payment methods, view invoices, or cancel:

POST /api/billing/portal JWT (Admin)

Returns a Stripe Customer Portal URL.

Section 9

Partner API (OEM Provisioning)

When you don't need this
If you're integrating against a single ctSignature account that you already own (or that one of your customers owns), skip this section and use the tenant API key flow in Section 2.1. The Partner API is specifically for platforms that need to provision and manage other tenants on behalf of their customers — e.g. a SaaS that wants to spin up a fresh ctSignature account for every agency that signs up to its own product.

The Partner API lives at /api/platform/v1/*. A Partner is registered by a platform operator (see Admin → Partners on the platform admin console) and gets a distinct key typectps_live_… — that authorizes them to create, list, update, and disable tenants, plus mint API keys, configure webhooks, and enable embedded signing on behalf of those tenants.

Ownership is enforced by a nullable Tenant.PartnerId column:

Partner Key Authentication

Partner keys follow the pattern ctps_<random-characters> (note: ctps_, not ctds_). They are sent on the Authorization header with the Bearer scheme:

Authorization: Bearer ctps_live_aB3cDeFgH1iJkLmN2oPqRsT3uVwXy
Do not cross the streams
A tenant API key (ctds_live_…) will NOT authenticate at /api/platform/v1/*, and a partner key (ctps_live_…) will NOT authenticate at /api/v1/*. The two key types are deliberately non-overlapping — mixing them up produces a 401.

Where keys come from

A platform operator creates a Partner row and mints keys for it from the platform admin console:

  1. Sign in to the platform admin at https://ctsign.io/dashboard/admin.html.
  2. Open the Partners tab.
  3. Click New Partner, enter name + contact email, save.
  4. Click Keys on the partner’s row, enter a label (e.g. Production), click Create Key.
  5. Copy the ctps_live_… value — it is shown once.

Hand the key to the partner via a secure channel; they store it as an environment variable (e.g. CTSIGN_PARTNER_KEY) and use it server-side.

One-Call Tenant Onboarding

The headline endpoint is POST /api/platform/v1/tenants. A single call can provision the local tenant, provision the matching org in ctOneAuth, mint a tenant API key, register a webhook, and enable iframe embedding — replacing the entire ctsign.io sign-up + password-setup + key-copy flow.

POST /api/platform/v1/tenants Partner Key
POST /api/platform/v1/tenants
Authorization: Bearer ctps_live_aB3cDeFgH1iJkLmN2oPqRsT3uVwXy
Content-Type: application/json

{
  "companyName": "Smart of South Jersey, LLC",
  "adminName":   "Gary Coslop",
  "adminEmail":  "gary@smartsouthjersey.com",
  "mintApiKey":  true,
  "replyToEmail": "documents@smartsouthjersey.com",
  "webhook": {
    "url":    "https://pnbv5.cozzitech.com/webhooks/ctsign",
    "events": ["document.completed", "signer.signed"]
  },
  "embeddedSigning": {
    "enabled":        true,
    "allowedDomains": ["https://pnbv5.cozzitech.com"]
  }
}

// Response — everything shown once is shown ONCE.
{
  "tenantId":   123,
  "companyName": "Smart of South Jersey, LLC",
  "admin": {
    "userId": 456,
    "name":   "Gary Coslop",
    "email":  "gary@smartsouthjersey.com"
  },
  "setupUrl":           "https://auth.cozzitech.com/identity-ui/setup/<one-time-token>",
  "linkedExistingUser": false,
  "apiKey":             "ctds_live_aBcDeFg...",         // shown ONCE
  "webhook": {
    "id":     7,
    "url":    "https://pnbv5.cozzitech.com/webhooks/ctsign",
    "secret": "a1b2c3...64-hex-chars...",                // shown ONCE
    "events": ["document.completed", "signer.signed"]
  },
  "embeddedSigningEnabled": true,
  "createdDate":            "2026-06-09T19:01:23Z"
}

What to do with each field of the response:

Optional fields
mintApiKey, webhook, embeddedSigning and replyToEmail are all optional. If omitted, you can call the corresponding endpoints later: POST /tenants/{id}/api-keys, POST /tenants/{id}/webhooks, PUT /tenants/{id}/embedded-signing, PUT /tenants/{id}. Bundling them inline is just a convenience.
replyToEmail
The tenant-wide default Reply-To for signer emails — see Where Signer Replies Go. Worth setting at provision time: documents created with a tenant API key have no signed-in user to fall back to, so without it a signer’s reply reaches ctSignature rather than the tenant. The From address is unchanged either way.

Endpoint Reference

Every endpoint is scoped to Tenant.PartnerId == <calling partner>. All paths are prefixed with /api/platform/v1.

Tenants

MethodPathPurpose
POST/tenantsCreate tenant (with optional inline mintApiKey, webhook, embeddedSigning).
GET/tenants?page=&pageSize=&search=List tenants this partner owns.
GET/tenants/{tenantId}Tenant detail.
PUT/tenants/{tenantId}Update { companyName?, isActive?, replyToEmail? }. Send replyToEmail: "" to clear it; omit to leave it alone.
DELETE/tenants/{tenantId}Soft-disable. Sets IsActive=false; signed PDFs remain accessible; sign-in and new document creation are blocked.

Users

MethodPathPurpose
GET/tenants/{tenantId}/usersList team members.
POST/tenants/{tenantId}/usersInvite { name, email, role, expiresInHours? }. Role is "Admin" or "User".
PUT/tenants/{tenantId}/users/{userId}Update { name?, email?, role? }.
POST/tenants/{tenantId}/users/{userId}/deactivateDisable a user.
POST/tenants/{tenantId}/users/{userId}/activateRe-enable.
POST/tenants/{tenantId}/users/{userId}/resend-inviteReissue the IdP setup link.

API Keys (on the tenant’s behalf)

MethodPathPurpose
GET/tenants/{tenantId}/api-keysList keys.
POST/tenants/{tenantId}/api-keysMint a new ctds_live_ key. fullKey shown ONCE.
DELETE/tenants/{tenantId}/api-keys/{keyId}Revoke.

Webhooks

MethodPathPurpose
GET/tenants/{tenantId}/webhooksList endpoints.
POST/tenants/{tenantId}/webhooksCreate { url, events[], description? }. secret shown ONCE.
DELETE/tenants/{tenantId}/webhooks/{webhookId}Delete.

Embedded Signing

MethodPathPurpose
GET/tenants/{tenantId}/embedded-signingCurrent allowlist.
PUT/tenants/{tenantId}/embedded-signingUpdate { enabled, allowedDomains[] }. Domains are full origins including https://.

Gotchas

Section 10

Dashboard Guide

The web dashboard gives tenant admins and users a visual interface for managing documents, templates, users, settings, and billing.

Documents

The Documents section lets you:

Dashboard Document Creation Endpoint

POST /api/dashboard/documents JWT

Same as the API v1 endpoint, but also supports creating documents from templates (when a template has pre-placed fields, it skips the placement step and sends immediately).

Stats Overview

GET /api/dashboard/stats JWT

Returns real-time document counts by status and trial information.

Templates

Templates are managed through the same API endpoints described in Section 4. From the dashboard, you can:

User Management

Tenant admins can invite team members and control their access.

User Roles

RolePermissions
AdminFull access: documents, templates, settings, billing, users, webhooks, API keys
UserCreate and manage documents and templates. Cannot change settings, billing, or users.
ReviewerView documents only. Cannot create or modify.

User Management Endpoints

MethodPathDescription
GET/api/dashboard/usersList all users in the tenant
POST/api/dashboard/usersInvite a new user (by email)
PUT/api/dashboard/users/{id}/roleChange a user's role
POST/api/dashboard/users/{id}/deactivateDisable a user's access
POST/api/dashboard/users/{id}/activateRe-enable a disabled user
POST/api/dashboard/users/{id}/reset-passwordForce a password reset

Settings & Branding

Account Settings

PUT /api/dashboard/account JWT (Admin)

Update company name, contact email, or password.

Consent Settings

Read the current consent configuration:

GET /api/dashboard/consent JWT (Admin)

Updates use separate sub-paths:

MethodPathDescription
PUT/api/dashboard/consent/flowSet flow type (modal or page)
POST/api/dashboard/consent/disclosurePublish a new disclosure version
PUT/api/dashboard/consent/otpEnable or disable OTP verification

Branding

MethodPathDescription
GET/api/dashboard/brandingGet current branding settings
PUT/api/dashboard/brandingUpdate colors, brand name and replyToEmail
POST/api/dashboard/branding/logoUpload logo (PNG, JPG, SVG; max 2 MB)
DELETE/api/dashboard/branding/logoRemove logo

replyToEmail is the tenant-wide default Reply-To for signer emails (Branding page → Signer Replies) — see Where Signer Replies Go. Send "" to clear it; omit the field to leave it unchanged.

Document Retention

PUT /api/dashboard/retention JWT (Admin)

Set how long signed documents are kept before automatic deletion. Range: 30 to 36,500 days (default: 2,555 days / ~7 years).

Embedded Signing Settings

PUT /api/dashboard/embedded-signing JWT (Admin)

Enable or disable iframe-based embedded signing and set allowed domains.

Analytics

GET /api/dashboard/analytics/trends?days=30 JWT

Returns daily document creation and signing counts for charting. Supports 7, 30, 60, and 90 day windows.

GET /api/dashboard/analytics/summary JWT

Returns aggregate statistics: total documents, completion rate, average time to sign, and template usage count.

Section 11

Configuration Reference

ctSignature is configured through appsettings.json (and environment-specific overrides). Below are all the settings you need to know.

Database

"ConnectionStrings": {
  "DefaultConnection": "Server=localhost;Database=ctDocSign;User=ctdocsign;Password=YOUR_PASSWORD;Port=3306;"
}

Requires MySQL 8.0 or later. Entity Framework Core handles migrations automatically.

JWT Settings

SettingDefaultDescription
Jwt:SecretSigning key for JWT tokens. Must be at least 32 characters. Change this in production!
Jwt:IssuerctDocSignIssuer claim in JWT tokens
Jwt:AudiencectDocSign-dashboardAudience claim in JWT tokens
Jwt:ExpirationHours24How long JWT tokens are valid

Stripe Settings

SettingDescription
Stripe:SecretKeyStripe API secret key (sk_live_... or sk_test_...)
Stripe:PublishableKeyStripe publishable key (for frontend)
Stripe:WebhookSecretStripe webhook endpoint signing secret (whsec_...)

Platform Admin

SettingDescription
PlatformAdmin:EmailEmail for platform admin login
PlatformAdmin:PasswordPassword for platform admin login
PlatformAdmin:SecretSecret for admin endpoint validation

Document Storage & Security

SettingDefaultDescription
DocumentSigning:Storage:BasePath./Documents/SigningRoot directory for stored PDFs
DocumentSigning:Storage:MaxFileSize10485760Max upload size in bytes (10 MB)
DocumentSigning:Security:TokenExpirationHours72Signing link lifetime
DocumentSigning:Security:AllowedOrigins[]CORS allowed origins
DocumentSigning:ProductionBaseUrlBase URL for signing links in emails

Testing & Development

SettingDefaultDescription
DocumentSigning:Testing:EnableTestModetrueEnables the /api/test/ endpoints
DocumentSigning:Testing:SkipEmailSendingtrueSkips actual email delivery in dev
DocumentSigning:Testing:LocalhostBaseUrlhttp://localhost:8080Base URL used in dev mode

Email Providers

Email delivery is configured at runtime by a platform admin through the Platform Admin UI — not via environment variables. The platform admin sets one of the following system settings:

Setting KeyProvider
PostmarkApiKeyPostmark (recommended for delivery tracking, bounces, opens)
ResendApiKeyResend (alternative provider)

Self-hosted deployments configure these through /admin-panel/ after the first platform-admin login.

Rate Limits

ScopeLimit
General API60 requests/minute
Signature submission10 requests/minute
Test endpoint5 requests/minute
Tenant API (per key)100 requests/minute

Section 12

Error Handling

All errors return a JSON object with an error field containing a human-readable message.

// Example error response
{
  "error": "Document not found"
}

HTTP Status Codes

CodeMeaningCommon Causes
200OKRequest succeeded
201CreatedResource created successfully
400Bad RequestMissing required field, invalid file format, validation error
401UnauthorizedMissing or invalid API key / JWT token
402Payment RequiredBilling quota exceeded (trial or subscription limit reached)
403ForbiddenValid auth but not allowed (wrong tenant, non-admin user)
404Not FoundDocument, template, or recipient doesn't exist
409ConflictEmail already registered, duplicate recipient
429Too Many RequestsRate limit exceeded — slow down and retry
500Internal Server ErrorServer-side error (these are logged and monitored)

Common Error Scenarios

Authentication Errors

// Missing API key
401: { "error": "Authorization header is required" }

// Invalid API key
401: { "error": "Invalid API key" }

// Expired JWT
401: { "error": "Token has expired" }

Document Errors

// File too large
400: { "error": "File size exceeds the maximum allowed (10 MB)" }

// Unsupported file type
400: { "error": "Only PDF, DOC, and DOCX files are accepted" }

// Word file could not be converted
400: { "error": "contract.docx: The Word document could not be converted to PDF. Ensure it is a valid, unencrypted document." }

// Trying to delete a signed document
400: { "error": "Signed documents cannot be deleted" }

// Trial expired
402: { "error": "Trial document limit reached. Please upgrade." }

Signing Errors

// Token expired
400: { "error": "This signing link has expired" }

// Consent not given
400: { "error": "Consent must be given before signing" }

// OTP not verified
400: { "error": "OTP verification is required before signing" }

// Sequential order violation
400: { "error": "Previous signer has not yet signed" }

// Too many OTP attempts
400: { "error": "TooManyAttempts" }

Section 13

Security & Compliance

Data Security

ESIGN Act & UETA Compliance

ctSignature includes the following features to support ESIGN Act and UETA compliance:

RequirementHow ctSignature Meets It
Consent to use electronic signatures Configurable consent disclosure (modal or full-page) with versioned history. Each signer's consent is timestamped and IP-logged.
Intent to sign Explicit "I intend to sign" confirmation checkbox. Timestamp recorded as IntentToSignAt.
Signer identity Email-based identification. Optional OTP verification for additional identity assurance. Device fingerprinting (canvas, user agent, IP, geolocation).
Record retention Configurable retention period (default 7 years). Signed PDFs include SHA-256 hash for integrity verification. Certificate of Completion documents the full signing process.
Delivery evidence Postmark integration tracks email delivery, bounces, opens, and spam complaints. All events logged in the audit trail.
Audit trail Every action is logged: document creation, field placement, signer views, consent, OTP, signature submission, email events. All entries include actor, timestamp, IP, and metadata.

Certificate of Completion

Every signed document has a Certificate of Completion automatically appended as the last page. It includes:

Document Verification

Anyone can verify a signed document's authenticity using the public verification endpoint:

// Look up by public ID (printed on the certificate)
GET /api/documents/verify/{publicId}

// Or verify a specific file's hash
POST /api/documents/verify/{publicId}
{ "hash": "sha256-hex-string-of-your-file" }

This does not require any authentication.

Security Best Practices for Integrators

Recommendations
  • Store API keys in environment variables or a secrets manager — never in source code
  • Always verify webhook signatures before processing payloads
  • Use HTTPS for all webhook endpoint URLs
  • Rotate API keys periodically and revoke unused ones
  • Set the shortest reasonable expiration time for signing links
  • Enable OTP verification for high-value documents
  • Publish a custom consent disclosure that matches your legal requirements

Appendix

Quick Reference — All API Endpoints

Authentication

MethodPathAuthDescription
POST/api/auth/registerNoneCreate tenant account; returns ctOneAuth setupUrl
GET/api/auth/oidc/loginNoneStart OIDC sign-in (ctOneAuth)
GET/api/auth/meJWTGet current user

Documents (API v1)

MethodPathAuthDescription
POST/api/v1/documentsAPI KeyCreate single-signer document
POST/api/v1/documents/multi-signerAPI KeyCreate multi-signer document
GET/api/v1/documentsAPI KeyList documents (paginated)
GET/api/v1/documents/{id}API KeyGet document details
GET/api/v1/documents/{id}/statusAPI KeyCheck status
GET/api/v1/documents/{id}/signersAPI KeyPer-signer progress: who signed, who's next, who's waiting
GET/api/v1/documents/{id}/downloadAPI KeyDownload signed PDF (once signed)
GET/api/v1/documents/{id}/separate-filesAPI KeySigned multi-file document as per-file ZIP
POST/api/v1/documents/{id}/resendAPI KeyResend / renew link (unsigned only)
PUT/api/v1/documents/{id}/placementAPI KeyRe-place fields: move one to another signer, nudge, or add (never emails; statuses unchanged)
POST/api/v1/documents/{id}/voidAPI KeyVoid before completion (kills links, keeps audit trail)
DELETE/api/v1/documents/{id}API KeyDelete pending document
POST/api/v1/documents/{id}/archiveAPI KeyHide from default lists (works for signed docs; reversible)
POST/api/v1/documents/{id}/unarchiveAPI KeyRestore an archived document to the lists
POST/api/v1/documents/{id}/embedded-sessionAPI KeyCreate embedded signing URL

Templates (API v1)

MethodPathAuthDescription
POST/api/v1/templatesAPI KeyCreate template from PDF
POST/api/v1/templates/from-document/{id}API KeyCreate from signed doc
GET/api/v1/templatesAPI KeyList templates
GET/api/v1/templates/{id}API KeyGet template details
PUT/api/v1/templates/{id}API KeyUpdate metadata
DELETE/api/v1/templates/{id}API KeyDeactivate template
POST/api/v1/templates/{id}/sendAPI KeySend to one recipient
POST/api/v1/templates/{id}/send-documentAPI KeySend caller-supplied PDF using template roles/anchors
POST/api/v1/templates/{id}/batch-sendAPI KeySend to up to 100 recipients

Recipients (API v1)

MethodPathAuthDescription
POST/api/v1/recipientsAPI KeyCreate recipient
GET/api/v1/recipientsAPI KeyList recipients
GET/api/v1/recipients/{id}API KeyGet recipient
PUT/api/v1/recipients/{id}API KeyUpdate recipient
DELETE/api/v1/recipients/{id}API KeyDeactivate recipient
POST/api/v1/recipients/{id}/reactivateAPI KeyReactivate

Webhooks

MethodPathAuthDescription
POST/api/v1/webhooksAPI KeyCreate webhook endpoint
GET/api/v1/webhooksAPI KeyList webhooks
DELETE/api/v1/webhooks/{id}API KeyDelete webhook
GET/api/dashboard/webhooks/{id}/deliveriesJWTDelivery history (dashboard only)
POST/api/dashboard/webhooks/{id}/testJWTSend test event (dashboard only)

Billing

MethodPathAuthDescription
GET/api/billing/tiersNoneList subscription plans
GET/api/billing/usageJWTCurrent usage stats
POST/api/billing/checkoutJWT (Admin)Create Stripe checkout
POST/api/billing/portalJWT (Admin)Open Stripe portal

Signing Flow (Token-Based)

MethodPathAuthDescription
GET/api/documents/placement/{token}TokenLoad placement page data
POST/api/documents/place/{token}TokenSubmit field positions
POST/api/documents/place-multi/{token}TokenSubmit multi-signer positions
GET/api/documents/sign/{token}TokenLoad signing page data
POST/api/documents/sign/{token}TokenSubmit signature
GET/api/documents/consent/{token}TokenGet consent disclosure
POST/api/documents/consent/{token}TokenRecord consent
GET/api/documents/otp/status/{token}TokenCheck OTP status
POST/api/documents/otp/send/{token}TokenSend OTP code
POST/api/documents/otp/verify/{token}TokenVerify OTP code
GET/api/documents/signed/{id}?token=TokenDownload signed PDF
GET/api/documents/verify/{publicId}NonePublic verification lookup
POST/api/documents/verify/{publicId}NoneVerify file hash

Dashboard

MethodPathAuthDescription
GET/api/dashboard/statsJWTDocument counts
POST/api/dashboard/documentsJWTCreate document
POST/api/dashboard/documents/multi-signerJWTCreate multi-signer
GET/api/dashboard/documentsJWTList documents
GET/api/dashboard/documents/{id}JWTDocument details
GET/api/dashboard/documents/{id}/downloadJWTDownload signed PDF
POST/api/dashboard/documents/{id}/resendJWTResend signing email
GET/POST/api/dashboard/api-keysJWT (Admin)List/create API keys
DELETE/api/dashboard/api-keys/{id}JWT (Admin)Revoke API key
GET/PUT/api/dashboard/accountJWT (Admin)Account settings
GET/api/dashboard/consentJWT (Admin)Read consent settings (updates via /consent/flow, /consent/disclosure, /consent/otp)
GET/PUT/api/dashboard/brandingJWT (Admin)Branding
GET/PUT/api/dashboard/retentionJWT (Admin)Retention policy
GET/PUT/api/dashboard/embedded-signingJWT (Admin)Embedded signing
GET/api/dashboard/usersJWT (Admin)List users
POST/api/dashboard/usersJWT (Admin)Invite user
GET/api/dashboard/analytics/trendsJWTDaily trends
GET/api/dashboard/analytics/summaryJWTAggregate stats

ctSignature Developer Manual — Version 1.3

© 2026 CozziTech LLC. All rights reserved.