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:
- Single and multi-signer documents — sequential or parallel signing order
- Reusable templates — pre-place fields once, send to many recipients
- Embedded signing — host the signing experience inside your own app via iframe
- Webhooks — get notified in real time when documents are viewed, signed, or completed
- ESIGN/UETA compliance — consent capture, OTP verification, device fingerprinting, audit log, and certificate of completion
- White-label branding — custom logo, colors, and brand name on signing pages
- Address book — save frequent signers for quick reuse
Base URLs & Environments
| Environment | Base URL | Notes |
|---|---|---|
| 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)
Follow these steps to send your first document for signing:
POST /api/auth/register
Content-Type: application/json
{
"companyName": "Acme Corp",
"name": "Jane Developer",
"email": "jane@acme.com"
}
The response contains a setupUrl. Open it in a browser to set your password on ctOneAuth (our identity provider). Once set, sign in via GET /api/auth/oidc/login — you’ll land on the dashboard with a JWT delivered via URL fragment.
POST /api/dashboard/api-keys Authorization: Bearer <jwt-token>
Save the full API key returned. It is only shown once. The key looks like: ctds_aBcDeFgHiJkLm...
POST /api/v1/documents Authorization: Bearer ctds_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).
Open the placementUrl in a browser. Drag and drop the signature, printed name, date, and initials fields onto the document. Click Save & Send.
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.
GET /api/dashboard/documents/{documentId}/download
Authorization: Bearer <jwt-token>
Or, using the signer’s secure token: GET /api/documents/signed/{documentId}?token=<secureToken>. The signed PDF has a Certificate of Completion appended.
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
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"
}
Revoking a Key
DELETE /api/dashboard/api-keys/{keyId}
Authorization: Bearer <jwt-token>
Endpoints That Accept API Keys
| Path Prefix | Description |
|---|---|
/api/v1/documents | Create, list, get, delete documents |
/api/v1/templates | Create, send, batch-send templates |
/api/v1/recipients | Manage address book |
/api/v1/webhooks | Manage 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
| Property | Value |
|---|---|
| Algorithm | HMAC-SHA256 |
| Lifetime | 24 hours (configurable) |
| Issuer | Jwt:Issuer setting |
| Audience | Jwt:Audience setting |
JWT Claims
| Claim | Description |
|---|---|
sub | Tenant ID (number as string) |
user_id | Tenant user ID (if multi-user tenant) |
role | User role: Admin, User, or Reviewer |
email | User's email address |
iat | Issued-at timestamp |
exp | Expiration 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 Type | Used For | URL Pattern | Expires |
|---|---|---|---|
| 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)
Upload a PDF and specify who should sign it.
Request
Content-Type: multipart/form-data
| Field | Type | Required | Description |
|---|---|---|---|
File | file | Yes | PDF file (max 10 MB) |
RecipientName | string | Yes | Signer's full name |
RecipientEmail | string | Yes | Signer's email address |
ExpirationHours | integer | No | Hours until signing link expires (default: 72) |
NotificationEmail | string | No | Email address that receives the "document signed" notification. If omitted, falls back to the tenant-level email. |
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"
}
placementUrl to position signature fields on the PDF. Once placed, the signing link becomes active and the signer receives an email.
Create a Multi-Signer Document
Create a document that requires two or more signers.
Request
Content-Type: multipart/form-data
| Field | Type | Required | Description |
|---|---|---|---|
File | file | Yes | PDF file (max 10 MB) |
Signers | JSON array | Yes | Array of signer objects (see below) |
WorkflowType | string | Yes | sequential or parallel |
ExpirationHours | integer | No | Hours until expiration (default: 72) |
NotificationEmail | string | No | Email address that receives the "document signed" notification. If omitted, falls back to the tenant-level email. |
Signer Object
| Field | Type | Required | Description |
|---|---|---|---|
SignerName | string | Yes | Signer's full name |
SignerEmail | string | Yes | Signer's email |
SignerRole | string | No | Role label (e.g., "Manager", "Legal") |
SignOrder | integer | No | Signing 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..."
}
]
}
Parallel: all signers can sign at the same time. Everyone gets their link immediately.
List Documents
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
status | string | (all) | Filter: pending, sent, signed, expired |
page | integer | 1 | Page number |
pageSize | integer | 20 | Results 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
}
expired filterstatus 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
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...",
"placementUrl": null,
"signingUrl": null,
"verifiedBadgeApplied": true,
"initialsFields": [
{ "id": 1, "x": 450, "y": 700, "width": 60, "height": 30, "page": 2, "isRequired": true }
],
"signatures": [
{
"id": 10,
"signatureFont": "Dancing Script",
"ipAddress": "203.0.113.42",
"platform": "Windows",
"timezone": "America/New_York",
"signatureHash": "a1b2c3d4e5f6...",
"createdDate": "2026-05-17T14:30:00Z"
}
]
}
hasPlacement flag tells you whether placement has been completed.
Check Document Status
A lightweight endpoint to check the current status of a document and its signers.
Document Status Values
| Status | Meaning |
|---|---|
pending | Created but fields not yet placed |
sent | Fields placed, signing link is active |
partially_signed | Some (but not all) signers have signed (multi-signer only) |
signed | All 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.
Delete a Document
Delete a pending document. Signed documents cannot be deleted through the API.
Download Signed PDF
Downloads the fully signed PDF with the Certificate of Completion appended. The download is logged in the audit trail.
From the dashboard, use:
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
// 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
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
// 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 === 'signing-complete') {
console.log('Document signed!', event.data.documentId);
// Redirect user, show confirmation, etc.
}
});
</script>
Section 4
Templates API
Templates let you upload a PDF once, position the signature fields, and then reuse it for many recipients without re-uploading or re-placing fields each time.
Create a Template
| Field | Type | Required | Description |
|---|---|---|---|
File | file | Yes | PDF file |
Name | string | Yes | Template name (e.g., "Employee NDA") |
Description | string | No | Optional description |
You can also create a template from an existing signed document:
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
| Method | Path | Description |
|---|---|---|
GET | /api/v1/templates | List all active templates |
GET | /api/v1/templates/{id} | Get template details with field coordinates |
PUT | /api/v1/templates/{id} | Update name or description |
DELETE | /api/v1/templates/{id} | Deactivate template (soft delete) |
Send from a Template
Creates a new document from the template with pre-placed fields and sends the signing link directly. No placement step needed.
POST /api/v1/templates/5/send
Authorization: Bearer ctds_yourApiKey
Content-Type: application/json
{
"recipientName": "Sarah Johnson",
"recipientEmail": "sarah@example.com",
"expirationHours": 48
}
// Response
{
"documentId": 44,
"signingUrl": "https://ctsign.io/sign/document/xyz...",
"templateId": 5,
"skippedPlacement": true
}
Batch Send
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": "..." }
]
}
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.
| Method | Path | Description |
|---|---|---|
GET | /api/v1/recipients | List recipients (supports ?search= and ?includeInactive=true) |
GET | /api/v1/recipients/{id} | Get recipient details |
POST | /api/v1/recipients | Create recipient |
PUT | /api/v1/recipients/{id} | Update recipient |
DELETE | /api/v1/recipients/{id} | Deactivate recipient |
POST | /api/v1/recipients/{id}/reactivate | Reactivate 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
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"
}
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.
| Event | Fires When |
|---|---|
document.created | A new document is uploaded via the API |
document.sent | Signature fields are placed and the signing link becomes active |
signer.signed | A signer submits their signature |
document.completed | All signers have signed the document |
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
| Header | Description |
|---|---|
Content-Type | application/json |
User-Agent | ctSignature-Webhook/1.0 |
X-Webhook-Id | Numeric ID of the webhook endpoint receiving the event |
X-Timestamp | Unix epoch seconds at the moment the signature was computed (used in the signing input — see below) |
X-Signature | Hex-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
- Read the raw request body as a UTF-8 string (do not re-serialize the JSON — whitespace matters).
- Read the
X-Timestampheader. - Build the signing string:
{timestamp}.{rawBody}(timestamp, a literal dot, then the body). - 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.
- Hex-encode the result (lowercase) and compare to
X-Signatureusing a constant-time comparison. - 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:
| Attempt | Delay after previous failure |
|---|---|
| 1 | — (initial delivery, within ~15s of the event) |
| 2 | 30 seconds |
| 3 | 2 minutes |
| 4 | 15 minutes |
| 5 | 1 hour |
After 5 failed attempts the delivery is marked as exhausted. You can view delivery history from the dashboard:
limit is clamped to 1–100 (default 25).
To test your webhook endpoint without creating a real document:
Section 7
Signing Workflow
Single-Signer Flow
This is the complete lifecycle of a single-signer document:
pending. You receive a placementUrl and signingUrl.
sent. An email is sent to the signer automatically.
signed.
document.completed webhook. The signed PDF is available for download.
Multi-Signer Flow
Sequential Workflow
- Create multi-signer document with
workflowType: "sequential" - Place fields for all signers on the placement page
- Signer 1 receives an email. They sign.
- Only after Signer 1 completes does Signer 2 receive their email
- This continues through all signers in order
- When the last signer signs, the document status becomes
signed
Parallel Workflow
- Create multi-signer document with
workflowType: "parallel" - Place fields for all signers
- All signers receive emails at the same time
- Signers can sign in any order
- When the last remaining signer completes, the document is fully signed
Consent & OTP Verification
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.
- Modal mode: The disclosure appears as a popup on the signing page
- Page mode: The signer is taken to a full-page disclosure before seeing the document
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.
- OTP codes expire after 10 minutes
- Maximum 5 verification attempts per code
- Each new code invalidates any previous unverified codes
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
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
// 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
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:
Returns a Stripe Customer Portal URL.
Section 9
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:
- Create documents — upload a PDF or select a template, enter recipient info
- Create multi-signer documents — add multiple signers with roles and ordering
- Track status — see pending, sent, signed, and expired documents
- Resend emails — re-send signing request emails to recipients
- Download signed PDFs — download the completed document with certificate
- View audit trail — see who signed, when, from where, and with what device
Dashboard Document Creation Endpoint
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
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:
- Upload new templates
- Create templates from previously signed documents
- Send individual or batch documents from templates
- Edit template names and descriptions
- Deactivate templates you no longer need
User Management
Tenant admins can invite team members and control their access.
User Roles
| Role | Permissions |
|---|---|
Admin | Full access: documents, templates, settings, billing, users, webhooks, API keys |
User | Create and manage documents and templates. Cannot change settings, billing, or users. |
Reviewer | View documents only. Cannot create or modify. |
User Management Endpoints
| Method | Path | Description |
|---|---|---|
GET | /api/dashboard/users | List all users in the tenant |
POST | /api/dashboard/users | Invite a new user (by email) |
PUT | /api/dashboard/users/{id}/role | Change a user's role |
POST | /api/dashboard/users/{id}/deactivate | Disable a user's access |
POST | /api/dashboard/users/{id}/activate | Re-enable a disabled user |
POST | /api/dashboard/users/{id}/reset-password | Force a password reset |
Settings & Branding
Account Settings
Update company name, contact email, or password.
Consent Settings
Read the current consent configuration:
Updates use separate sub-paths:
| Method | Path | Description |
|---|---|---|
PUT | /api/dashboard/consent/flow | Set flow type (modal or page) |
POST | /api/dashboard/consent/disclosure | Publish a new disclosure version |
PUT | /api/dashboard/consent/otp | Enable or disable OTP verification |
Branding
| Method | Path | Description |
|---|---|---|
GET | /api/dashboard/branding | Get current branding settings |
PUT | /api/dashboard/branding | Update colors and brand name |
POST | /api/dashboard/branding/logo | Upload logo (PNG, JPG, SVG; max 2 MB) |
DELETE | /api/dashboard/branding/logo | Remove logo |
Document Retention
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
Enable or disable iframe-based embedded signing and set allowed domains.
Analytics
Returns daily document creation and signing counts for charting. Supports 7, 30, 60, and 90 day windows.
Returns aggregate statistics: total documents, completion rate, average time to sign, and template usage count.
Section 10
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
| Setting | Default | Description |
|---|---|---|
Jwt:Secret | — | Signing key for JWT tokens. Must be at least 32 characters. Change this in production! |
Jwt:Issuer | ctDocSign | Issuer claim in JWT tokens |
Jwt:Audience | ctDocSign-dashboard | Audience claim in JWT tokens |
Jwt:ExpirationHours | 24 | How long JWT tokens are valid |
Stripe Settings
| Setting | Description |
|---|---|
Stripe:SecretKey | Stripe API secret key (sk_live_... or sk_test_...) |
Stripe:PublishableKey | Stripe publishable key (for frontend) |
Stripe:WebhookSecret | Stripe webhook endpoint signing secret (whsec_...) |
Platform Admin
| Setting | Description |
|---|---|
PlatformAdmin:Email | Email for platform admin login |
PlatformAdmin:Password | Password for platform admin login |
PlatformAdmin:Secret | Secret for admin endpoint validation |
Document Storage & Security
| Setting | Default | Description |
|---|---|---|
DocumentSigning:Storage:BasePath | ./Documents/Signing | Root directory for stored PDFs |
DocumentSigning:Storage:MaxFileSize | 10485760 | Max upload size in bytes (10 MB) |
DocumentSigning:Security:TokenExpirationHours | 72 | Signing link lifetime |
DocumentSigning:Security:AllowedOrigins | [] | CORS allowed origins |
DocumentSigning:ProductionBaseUrl | — | Base URL for signing links in emails |
Testing & Development
| Setting | Default | Description |
|---|---|---|
DocumentSigning:Testing:EnableTestMode | true | Enables the /api/test/ endpoints |
DocumentSigning:Testing:SkipEmailSending | true | Skips actual email delivery in dev |
DocumentSigning:Testing:LocalhostBaseUrl | http://localhost:8080 | Base 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 Key | Provider |
|---|---|
PostmarkApiKey | Postmark (recommended for delivery tracking, bounces, opens) |
ResendApiKey | Resend (alternative provider) |
Self-hosted deployments configure these through /admin-panel/ after the first platform-admin login.
Rate Limits
| Scope | Limit |
|---|---|
| General API | 60 requests/minute |
| Signature submission | 10 requests/minute |
| Test endpoint | 5 requests/minute |
| Tenant API (per key) | 100 requests/minute |
Section 11
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
| Code | Meaning | Common Causes |
|---|---|---|
200 | OK | Request succeeded |
201 | Created | Resource created successfully |
400 | Bad Request | Missing required field, invalid file format, validation error |
401 | Unauthorized | Missing or invalid API key / JWT token |
402 | Payment Required | Billing quota exceeded (trial or subscription limit reached) |
403 | Forbidden | Valid auth but not allowed (wrong tenant, non-admin user) |
404 | Not Found | Document, template, or recipient doesn't exist |
409 | Conflict | Email already registered, duplicate recipient |
429 | Too Many Requests | Rate limit exceeded — slow down and retry |
500 | Internal Server Error | Server-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)" } // Not a PDF 400: { "error": "Only PDF files are accepted" } // 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 12
Security & Compliance
Data Security
- Passwords — hashed with BCrypt (tenant passwords, user passwords, API keys)
- Tokens — compared using timing-safe operations to prevent timing attacks
- File storage — path traversal protection on all file access
- Tenant isolation — every query is scoped to the authenticated tenant; no cross-tenant data access is possible
- Rate limiting — prevents brute-force and abuse
ESIGN Act & UETA Compliance
ctSignature includes the following features to support ESIGN Act and UETA compliance:
| Requirement | How 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 public ID (for verification lookup)
- Original filename
- List of all signers with their signed dates
- SHA-256 hash of the signed document
- Audit trail summary
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
- 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
| Method | Path | Auth | Description |
|---|---|---|---|
POST | /api/auth/register | None | Create tenant account; returns ctOneAuth setupUrl |
GET | /api/auth/oidc/login | None | Start OIDC sign-in (ctOneAuth) |
GET | /api/auth/me | JWT | Get current user |
Documents (API v1)
| Method | Path | Auth | Description |
|---|---|---|---|
POST | /api/v1/documents | API Key | Create single-signer document |
POST | /api/v1/documents/multi-signer | API Key | Create multi-signer document |
GET | /api/v1/documents | API Key | List documents (paginated) |
GET | /api/v1/documents/{id} | API Key | Get document details |
GET | /api/v1/documents/{id}/status | API Key | Check status |
DELETE | /api/v1/documents/{id} | API Key | Delete pending document |
POST | /api/v1/documents/{id}/embedded-session | API Key | Create embedded signing URL |
Templates (API v1)
| Method | Path | Auth | Description |
|---|---|---|---|
POST | /api/v1/templates | API Key | Create template from PDF |
POST | /api/v1/templates/from-document/{id} | API Key | Create from signed doc |
GET | /api/v1/templates | API Key | List templates |
GET | /api/v1/templates/{id} | API Key | Get template details |
PUT | /api/v1/templates/{id} | API Key | Update metadata |
DELETE | /api/v1/templates/{id} | API Key | Deactivate template |
POST | /api/v1/templates/{id}/send | API Key | Send to one recipient |
POST | /api/v1/templates/{id}/batch-send | API Key | Send to up to 100 recipients |
Recipients (API v1)
| Method | Path | Auth | Description |
|---|---|---|---|
POST | /api/v1/recipients | API Key | Create recipient |
GET | /api/v1/recipients | API Key | List recipients |
GET | /api/v1/recipients/{id} | API Key | Get recipient |
PUT | /api/v1/recipients/{id} | API Key | Update recipient |
DELETE | /api/v1/recipients/{id} | API Key | Deactivate recipient |
POST | /api/v1/recipients/{id}/reactivate | API Key | Reactivate |
Webhooks
| Method | Path | Auth | Description |
|---|---|---|---|
POST | /api/v1/webhooks | API Key | Create webhook endpoint |
GET | /api/v1/webhooks | API Key | List webhooks |
DELETE | /api/v1/webhooks/{id} | API Key | Delete webhook |
GET | /api/dashboard/webhooks/{id}/deliveries | JWT | Delivery history (dashboard only) |
POST | /api/dashboard/webhooks/{id}/test | JWT | Send test event (dashboard only) |
Billing
| Method | Path | Auth | Description |
|---|---|---|---|
GET | /api/billing/tiers | None | List subscription plans |
GET | /api/billing/usage | JWT | Current usage stats |
POST | /api/billing/checkout | JWT (Admin) | Create Stripe checkout |
POST | /api/billing/portal | JWT (Admin) | Open Stripe portal |
Signing Flow (Token-Based)
| Method | Path | Auth | Description |
|---|---|---|---|
GET | /api/documents/placement/{token} | Token | Load placement page data |
POST | /api/documents/place/{token} | Token | Submit field positions |
POST | /api/documents/place-multi/{token} | Token | Submit multi-signer positions |
GET | /api/documents/sign/{token} | Token | Load signing page data |
POST | /api/documents/sign/{token} | Token | Submit signature |
GET | /api/documents/consent/{token} | Token | Get consent disclosure |
POST | /api/documents/consent/{token} | Token | Record consent |
GET | /api/documents/otp/status/{token} | Token | Check OTP status |
POST | /api/documents/otp/send/{token} | Token | Send OTP code |
POST | /api/documents/otp/verify/{token} | Token | Verify OTP code |
GET | /api/documents/signed/{id}?token= | Token | Download signed PDF |
GET | /api/documents/verify/{publicId} | None | Public verification lookup |
POST | /api/documents/verify/{publicId} | None | Verify file hash |
Dashboard
| Method | Path | Auth | Description |
|---|---|---|---|
GET | /api/dashboard/stats | JWT | Document counts |
POST | /api/dashboard/documents | JWT | Create document |
POST | /api/dashboard/documents/multi-signer | JWT | Create multi-signer |
GET | /api/dashboard/documents | JWT | List documents |
GET | /api/dashboard/documents/{id} | JWT | Document details |
GET | /api/dashboard/documents/{id}/download | JWT | Download signed PDF |
POST | /api/dashboard/documents/{id}/resend | JWT | Resend signing email |
GET/POST | /api/dashboard/api-keys | JWT (Admin) | List/create API keys |
DELETE | /api/dashboard/api-keys/{id} | JWT (Admin) | Revoke API key |
GET/PUT | /api/dashboard/account | JWT (Admin) | Account settings |
GET | /api/dashboard/consent | JWT (Admin) | Read consent settings (updates via /consent/flow, /consent/disclosure, /consent/otp) |
GET/PUT | /api/dashboard/branding | JWT (Admin) | Branding |
GET/PUT | /api/dashboard/retention | JWT (Admin) | Retention policy |
GET/PUT | /api/dashboard/embedded-signing | JWT (Admin) | Embedded signing |
GET | /api/dashboard/users | JWT (Admin) | List users |
POST | /api/dashboard/users | JWT (Admin) | Invite user |
GET | /api/dashboard/analytics/trends | JWT | Daily trends |
GET | /api/dashboard/analytics/summary | JWT | Aggregate stats |
ctSignature Developer Manual — Version 1.1
© 2026 CozziTech LLC. All rights reserved.
