SayPDF E-Sign API
REST API to programmatically send documents for e-signature, build signing flows, manage templates, and receive real-time webhook events — all with cryptographically-embedded PKCS#7 signatures.
Overview
All API endpoints are served at:
https://pdf.classicmovie.net/api
Base URL
Prepend /api to all endpoint paths below. For example, GET /esign/documents → https://pdf.classicmovie.net/api/esign/documents.
Response format
All responses are JSON. Successful responses always include "success": true and the relevant data key. Errors follow the NestJS exception format with statusCode, message.
Zapier, Make, n8n
Use your SayPDF API key (same as PDF conversion API) to create signing requests without JWT.
POST https://api.saypdf.com/api/v1/esign/documents X-API-Key: your_api_key_here Content-Type: multipart/form-data pdf — binary PDF file data — JSON string, same shape asCreateDocumentDtoforPOST /esign/documents(title, parties[], optional brandName, brandLogoUrl, …)
Response: {"success":true,"documentId":"…","status":"pending","parties":[…]}. Pair with webhooks for “document completed” triggers in Zapier.
Authentication
Authorization header.Format:
Authorization: Bearer <token>Signer endpoints (
/esign/sign/:token/*) are public — authenticated via the unique signing token in the URL.
Obtaining a token
curl -X POST https://pdf.classicmovie.net/api/auth/login \ -H "Content-Type: application/json" \ -d '{"email":"you@example.com","password":"••••••••"}' # Response { "access_token": "eyJhbGciOiJIUzI1NiIs..." }
Errors
| Code | Meaning |
|---|---|
| 400 | Bad Request — missing required field or invalid JSON |
| 401 | Unauthorized — missing or invalid Bearer token |
| 403 | Forbidden — quota exceeded or not your resource |
| 404 | Not Found — document/template/token doesn't exist |
| 429 | Too Many Requests — rate limit hit |
| 500 | Server Error — check our status page |
Rate Limits
Global limit: 60 requests / minute per IP. The signature submission endpoint is additionally capped at 5 requests / minute to prevent brute-force.
When rate limited you will receive HTTP 429 with a Retry-After header.
Documents
Description
Upload a PDF and define signers with field positions. Invitation emails are sent immediately. Uses multipart/form-data.
Request body (multipart/form-data)
| Field | Type | Required | Description |
|---|---|---|---|
| file | required | PDF file to send for signing | |
| data | string (JSON) | required | JSON-serialized CreateDocumentDto |
Example
curl -X POST https://pdf.classicmovie.net/api/esign/documents \ -H "Authorization: Bearer $TOKEN" \ -F "pdf=@contract.pdf" \ -F 'data={ "title": "Service Agreement", "message": "Please review and sign.", "ownerEmail": "alice@company.com", "ownerName": "Alice", "expiresInDays": 7, "parties": [ { "name": "Bob Smith", "email": "bob@example.com", "signingOrder": 1, "fields": [ { "type": "signature", "page": 0, "x": 10, "y": 80, "width": 30, "height": 8 } ] } ] }'
{
"success": true,
"document": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"title": "Service Agreement",
"status": "pending",
"expiresAt": "2026-04-15T09:00:00.000Z"
}
}
Response
{
"success": true,
"documents": [/* SigningDocument[] */]
}Path Parameters
| Name | Type | Description | |
|---|---|---|---|
| id | uuid | required | Document UUID |
Response
{
"success": true,
"document": {
"id": "...",
"status": "completed",
"parties": [...],
"auditLogs": [...]
}
}Returns the final PDF with embedded PKCS#7 digital signatures as application/pdf. Only available when document status is completed.
Returns a PDF Certificate of Completion listing all signers, timestamps, IP addresses, and document hash — suitable for legal record-keeping.
Cancels a document in pending status. Completed documents cannot be cancelled.
Templates
Templates let you upload a PDF once with predefined role-based field positions, then send it to different signers without re-uploading the PDF each time.
Request body (multipart/form-data)
| Field | Type | Required | Description |
|---|---|---|---|
| file | required | PDF file to use as template | |
| data | string (JSON) | required | JSON CreateTemplateDto |
Example
curl -X POST https://pdf.classicmovie.net/api/esign/templates \ -H "Authorization: Bearer $TOKEN" \ -F "pdf=@nda-template.pdf" \ -F 'data={ "name": "NDA Template", "description": "Standard non-disclosure agreement", "defaultMessage": "Please review and sign the NDA.", "roles": [ { "role": "Employee", "color": "#2563eb", "fields": [ { "type": "signature", "page": 0, "x": 10, "y": 80, "width": 30, "height": 8 } ] }, { "role": "Manager", "color": "#16a34a", "fields": [ { "type": "signature", "page": 0, "x": 60, "y": 80, "width": 30, "height": 8 } ] } ] }'
{
"success": true,
"templates": [
{
"id": "...",
"name": "NDA Template",
"usageCount": 14,
"roles": [...]
}
]
}Request body (application/json)
| Field | Type | Required | Description |
|---|---|---|---|
| templateId | uuid | required | ID of the template |
| title | string | required | Document title |
| ownerEmail | string | required | Your email (for notifications) |
| ownerName | string | optional | Your name shown to signers |
| message | string | optional | Override template default message |
| expiresInDays | number | optional | Default: 30 |
| signers | object | required | Map of role name → signer details |
Example
curl -X POST https://pdf.classicmovie.net/api/esign/templates/send \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "templateId": "550e8400-...", "title": "NDA with Bob Smith", "ownerEmail": "alice@company.com", "ownerName": "Alice", "signers": { "Employee": { "name": "Bob Smith", "email": "bob@example.com", "signingOrder": 1 }, "Manager": { "name": "Alice Manager", "email": "alice@company.com", "signingOrder": 2 } } }'
Soft-deletes the template (marks isActive = false). Existing documents sent from this template are not affected.
Signer Endpoints
These endpoints are public — no JWT required. They are authenticated via the unique :token in the signing invitation URL.
Response
{
"success": true,
"document": { "id": "...", "title": "...", "expiresAt": "..." },
"party": {
"id": "...", "name": "Bob Smith", "status": "pending",
"fields": [{ "id": "...", "type": "signature", "page": 0,
"x": 10, "y": 80, "width": 30, "height": 8 }]
}
}Returns application/pdf with Content-Disposition: inline. Used by the signing page to render via PDF.js.
Request body
{
"fields": [
{
"fieldId": "uuid-of-the-field",
"value": "data:image/png;base64,iVBORw0KGg..."
}
]
}value is a base64-encoded PNG for signature/initials fields, or plain text for date/text fields. When all parties have signed, the document is automatically finalized with PKCS#7 signature embedding.
Request body
{ "reason": "I need to review with my lawyer first." }Webhooks
Returns all webhook delivery attempts with status, HTTP response code, and retry history. Optionally scope to a specific document: GET /esign/webhooks/deliveries/:documentId.
Re-queues the delivery for immediate retry regardless of the backoff schedule.
Schemas
CreateDocumentDto
| Field | Type | Description |
|---|---|---|
| title | string | Document title shown to signers |
| message | string? | Optional message in invitation email |
| ownerEmail | string | Owner email for completion notifications |
| ownerName | string? | Owner display name |
| expiresInDays | number? | Default 30. Max depends on plan. |
| parties | CreatePartyDto[] | Array of signers (min 1) |
CreatePartyDto
| Field | Type | Description |
|---|---|---|
| name | string | Signer full name |
| string | Signer email (invitation sent here) | |
| signingOrder | number? | Sequential order (omit = parallel) |
| fields | CreateFieldDto[] | Fields this signer must fill |
CreateFieldDto
| Field | Type | Description |
|---|---|---|
| type | signature | initials | date | text | Field type |
| page | number | 0-indexed page number |
| x | number (0–100) | Left edge, % of page width |
| y | number (0–100) | Top edge, % of page height |
| width | number (0–100) | Width as % of page width |
| height | number (0–100) | Height as % of page height |
CreateTemplateDto
| Field | Type | Description |
|---|---|---|
| name | string | Template display name |
| description | string? | Optional description |
| defaultMessage | string? | Default invitation message |
| roles | TemplateRoleDto[] | Role definitions with fields |
Document Status
| Value | Description |
|---|---|
| draft | Created but not yet sent (reserved) |
| pending | At least one party has not signed yet |
| completed | All parties signed; signed PDF ready |
| expired | Expired before all parties signed |
| cancelled | Manually cancelled by owner |
Webhooks Guide
When document events occur, SayPDF sends a signed POST to your configured webhook URL. Configure your webhook URL in the dashboard settings.
Event types
| Event | When it fires |
|---|---|
| document.completed | All parties have signed |
| document.declined | A signer declined |
| document.expired | Expiry date passed |
| document.cancelled | Owner cancelled |
| party.viewed | A signer opened the document |
| party.signed | A signer submitted their signature |
Payload format
{
"event": "document.completed",
"documentId": "550e8400-...",
"timestamp": "2026-04-08T12:34:56Z",
"data": { /* SigningDocument object */ }
}
Verifying signatures
Every webhook request includes an X-SayPDF-Signature header containing an HMAC-SHA256 signature of the raw body using your webhook secret.
const crypto = require('crypto'); function verifyWebhook(rawBody, signature, secret) { const expected = crypto .createHmac('sha256', secret) .update(rawBody) .digest('hex'); return crypto.timingSafeEqual( Buffer.from(expected), Buffer.from(signature) ); }
Retry policy
Failed deliveries are retried up to 5 times with exponential backoff: 60s → 5min → 30min → 2h → 6h. You can also trigger an immediate replay from the dashboard or via the API.
Changelog
-
2026-04-08
NEW
Document Templates — reuse PDFs with role-based field placements. New endpoints:
POST /esign/templates,POST /esign/templates/send. -
2026-04-07
NEW
Webhook delivery logs with full retry history. Replay failed deliveries via
POST /esign/webhooks/deliveries/:id/replay. - 2026-04-05 NEW PKCS#7 digital signatures embedded in PDF — verifiable in Adobe Reader and other PDF validators.
- 2026-04-05 NEW Certificate of Completion generated after all parties sign, listing audit trail with IPs and timestamps.
- 2026-04-04 NEW Automatic reminders at 3 days and 1 day before expiry. Daily GDPR cleanup cron for retention-expired documents.