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/documentshttps://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 as CreateDocumentDto for POST /esign/documents (title, parties[], optional brandName, brandLogoUrl, …)

Response: {"success":true,"documentId":"…","status":"pending","parties":[…]}. Pair with webhooks for “document completed” triggers in Zapier.

Authentication

All owner-side endpoints require a JWT access token in the 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

CodeMeaning
400Bad Request — missing required field or invalid JSON
401Unauthorized — missing or invalid Bearer token
403Forbidden — quota exceeded or not your resource
404Not Found — document/template/token doesn't exist
429Too Many Requests — rate limit hit
500Server 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

POST /esign/documents Create signing request · Auth required

Description

Upload a PDF and define signers with field positions. Invitation emails are sent immediately. Uses multipart/form-data.

Request body (multipart/form-data)

FieldTypeRequiredDescription
pdffilerequiredPDF file to send for signing
datastring (JSON)requiredJSON-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 }
        ]
      }
    ]
  }'
201 Created
{
  "success": true,
  "document": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "title": "Service Agreement",
    "status": "pending",
    "expiresAt": "2026-04-15T09:00:00.000Z"
  }
}
GET /esign/documents List my documents · Auth required

Response

{
  "success": true,
  "documents": [/* SigningDocument[] */]
}
GET /esign/documents/:id Get document + audit trail · Auth required

Path Parameters

NameTypeDescription
iduuidrequiredDocument UUID

Response

{
  "success": true,
  "document": {
    "id": "...",
    "status": "completed",
    "parties": [...],
    "auditLogs": [...]
  }
}
GET /esign/documents/:id/download Download signed PDF · Auth required

Returns the final PDF with embedded PKCS#7 digital signatures as application/pdf. Only available when document status is completed.

GET /esign/documents/:id/certificate Download Certificate of Completion · Auth required

Returns a PDF Certificate of Completion listing all signers, timestamps, IP addresses, and document hash — suitable for legal record-keeping.

DELETE /esign/documents/:id Cancel a pending document · Auth required

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.

POST /esign/templates Create template · Auth required

Request body (multipart/form-data)

FieldTypeRequiredDescription
pdffilerequiredPDF file to use as template
datastring (JSON)requiredJSON 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 }
        ]
      }
    ]
  }'
GET /esign/templates List my templates · Auth required
{
  "success": true,
  "templates": [
    {
      "id": "...",
      "name": "NDA Template",
      "usageCount": 14,
      "roles": [...]
    }
  ]
}
POST /esign/templates/send Send document from template · Auth required

Request body (application/json)

FieldTypeRequiredDescription
templateIduuidrequiredID of the template
titlestringrequiredDocument title
ownerEmailstringrequiredYour email (for notifications)
ownerNamestringoptionalYour name shown to signers
messagestringoptionalOverride template default message
expiresInDaysnumberoptionalDefault: 30
signersobjectrequiredMap 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
      }
    }
  }'
DELETE /esign/templates/:id Delete template · Auth required

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.

GET /esign/sign/:token Get document + fields for signing

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 }]
  }
}
GET /esign/sign/:token/pdf Download PDF for signing (inline)

Returns application/pdf with Content-Disposition: inline. Used by the signing page to render via PDF.js.

POST /esign/sign/:token/submit Submit signature fields · Rate-limited 5/min

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.

POST /esign/sign/:token/decline Decline to sign

Request body

{ "reason": "I need to review with my lawyer first." }

Webhooks

GET /esign/webhooks/deliveries List all delivery logs · Auth required

Returns all webhook delivery attempts with status, HTTP response code, and retry history. Optionally scope to a specific document: GET /esign/webhooks/deliveries/:documentId.

POST /esign/webhooks/deliveries/:id/replay Replay a failed delivery · Auth required

Re-queues the delivery for immediate retry regardless of the backoff schedule.

Schemas

CreateDocumentDto

FieldTypeDescription
titlestringDocument title shown to signers
messagestring?Optional message in invitation email
ownerEmailstringOwner email for completion notifications
ownerNamestring?Owner display name
expiresInDaysnumber?Default 30. Max depends on plan.
partiesCreatePartyDto[]Array of signers (min 1)

CreatePartyDto

FieldTypeDescription
namestringSigner full name
emailstringSigner email (invitation sent here)
signingOrdernumber?Sequential order (omit = parallel)
fieldsCreateFieldDto[]Fields this signer must fill

CreateFieldDto

FieldTypeDescription
typesignature | initials | date | textField type
pagenumber0-indexed page number
xnumber (0–100)Left edge, % of page width
ynumber (0–100)Top edge, % of page height
widthnumber (0–100)Width as % of page width
heightnumber (0–100)Height as % of page height

CreateTemplateDto

FieldTypeDescription
namestringTemplate display name
descriptionstring?Optional description
defaultMessagestring?Default invitation message
rolesTemplateRoleDto[]Role definitions with fields

Document Status

ValueDescription
draftCreated but not yet sent (reserved)
pendingAt least one party has not signed yet
completedAll parties signed; signed PDF ready
expiredExpired before all parties signed
cancelledManually 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

EventWhen it fires
document.completedAll parties have signed
document.declinedA signer declined
document.expiredExpiry date passed
document.cancelledOwner cancelled
party.viewedA signer opened the document
party.signedA 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.