Send a document for signature

Upload a PDF, ask people to sign it, follow each signer, receive signature events and download the signed document and its certificate.

What you need

  • An API key with the signatures:send scope to create, send, remind and void requests, and signatures:read to read them and download the signed document. To upload a PDF you also need the tools:run scope, because the upload is the same call the tools use.
  • A Team, Business or Enterprise account. The API is included in those plans.
  • Signature requests left in the plan allowance. A request spends one when it is sent, exactly as in the web app. A draft spends nothing. When the allowance is used, the call is refused with signatures.exhausted and the details give the limit, the amount used and when the period resets.

A key acts as the person who made it. A request it creates is private to that person unless you send "visibility": "ACCOUNT", and the key cannot see a private request of another member. A request that is not in your account, or that you may not see, is answered with 404, the same as one that does not exist.

1. Upload the PDF

Upload the document with POST /v1/files, as in the quick start. Instead of an upload you can name a document of the library with "source": { "documentId": "..." } or a signature template with "source": { "templateId": "..." }; list the templates with GET /v1/signature-templates. A request made from a template brings the template fields, and its signers name the template roles by id or label. The upload is used up by the request: if the call is refused, upload the file again.

curl
curl -X POST https://api.pdfloft.com/v1/files \
  -H "Authorization: Bearer $PDFLOFT_API_KEY" \
  -H "Content-Type: application/pdf" \
  --data-binary @contract.pdf
JavaScript
import { readFile } from 'node:fs/promises';

const apiKey = '<your API key>'; // read it from your secret store, never from source
const headers = { Authorization: `Bearer ${apiKey}` };
const body = await readFile('contract.pdf');

const response = await fetch('https://api.pdfloft.com/v1/files', {
  method: 'POST',
  headers: { ...headers, 'Content-Type': 'application/pdf' },
  body,
});
const file = await response.json(); // { id: 'file_...', sizeBytes, contentType, expiresAt }
Python
import os
import requests

headers = {"Authorization": f"Bearer {os.environ['PDFLOFT_API_KEY']}"}

with open("contract.pdf", "rb") as handle:
    response = requests.post(
        "https://api.pdfloft.com/v1/files",
        headers={**headers, "Content-Type": "application/pdf"},
        data=handle,
    )
file = response.json()  # {"id": "file_...", "sizeBytes": ..., ...}

2. Create and send the request

Name the signers and where each one signs. Positions are in points (72 to the inch) on the page, counted from 0. With "send": true the request is created and sent in one step, and a send the plan allowance refuses creates nothing. Without it you get a draft, and POST /v1/signature-requests/{requestId}/send sends it later. In a sequential request the first signer is asked first and each next signer after the one before has signed; in a parallel request everyone is asked at once.

Send the Idempotency-Key header. A call repeated with the same key and body returns the first answer and creates one request.

curl
curl -X POST https://api.pdfloft.com/v1/signature-requests \
  -H "Authorization: Bearer $PDFLOFT_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Services agreement",
    "source": { "fileId": "file_Qm3nT9xvB2kLw8RzYc1pAe5H" },
    "signers": [
      { "role": "Client", "sequence": 1, "name": "Meera Iyer", "email": "meera@example.com" },
      { "role": "Vendor", "sequence": 2, "name": "Ravi Kumar", "email": "ravi@example.com" }
    ],
    "fields": [
      { "signerRole": "Client", "type": "signature", "label": "Client signature", "page": 0, "x": 60, "y": 100, "w": 160, "h": 40 },
      { "signerRole": "Vendor", "type": "signature", "label": "Vendor signature", "page": 0, "x": 60, "y": 180, "w": 160, "h": 40 }
    ],
    "send": true
  }'
JavaScript
const request = await fetch('https://api.pdfloft.com/v1/signature-requests', {
  method: 'POST',
  headers: { ...headers, 'Content-Type': 'application/json', 'Idempotency-Key': crypto.randomUUID() },
  body: JSON.stringify({
    title: 'Services agreement',
    source: { fileId: file.id },
    signers: [
      { role: 'Client', sequence: 1, name: 'Meera Iyer', email: 'meera@example.com' },
      { role: 'Vendor', sequence: 2, name: 'Ravi Kumar', email: 'ravi@example.com' },
    ],
    fields: [
      { signerRole: 'Client', type: 'signature', label: 'Client signature', page: 0, x: 60, y: 100, w: 160, h: 40 },
      { signerRole: 'Vendor', type: 'signature', label: 'Vendor signature', page: 0, x: 60, y: 180, w: 160, h: 40 },
    ],
    send: true,
  }),
}).then((r) => r.json()); // { id: 'cm3x8k2f10010', status: 'sent', signers: [...], ... }
Python
request = requests.post(
    "https://api.pdfloft.com/v1/signature-requests",
    headers={**headers, "Idempotency-Key": str(uuid.uuid4())},
    json={
        "title": "Services agreement",
        "source": {"fileId": file["id"]},
        "signers": [
            {"role": "Client", "sequence": 1, "name": "Meera Iyer", "email": "meera@example.com"},
            {"role": "Vendor", "sequence": 2, "name": "Ravi Kumar", "email": "ravi@example.com"},
        ],
        "fields": [
            {"signerRole": "Client", "type": "signature", "label": "Client signature", "page": 0, "x": 60, "y": 100, "w": 160, "h": 40},
            {"signerRole": "Vendor", "type": "signature", "label": "Vendor signature", "page": 0, "x": 60, "y": 180, "w": 160, "h": 40},
        ],
        "send": True,
    },
).json()  # {"id": "cm3x8k2f10010", "status": "sent", "signers": [...], ...}

To send one template to many people, use POST /v1/signature-requests/bulk with a row for each request. The batch is refused whole when it would pass the allowance.

3. Follow the request

A request is draft, sent, in_progress (at least one signer has signed), then completed, declined, expired or voided. Each signer is pending (not asked yet), sent, viewed, signed or declined. Instead of polling, subscribe to the events below.

curl
curl https://api.pdfloft.com/v1/signature-requests/cm3x8k2f10010 \
  -H "Authorization: Bearer $PDFLOFT_API_KEY"

# What happened, in order:
curl https://api.pdfloft.com/v1/signature-requests/cm3x8k2f10010/timeline \
  -H "Authorization: Bearer $PDFLOFT_API_KEY"
JavaScript
const current = await fetch(`https://api.pdfloft.com/v1/signature-requests/${request.id}`, { headers }).then((r) => r.json());
console.log(current.status, current.signers.map((signer) => `${signer.name}: ${signer.status}`));

const timeline = await fetch(`https://api.pdfloft.com/v1/signature-requests/${request.id}/timeline`, { headers }).then((r) => r.json());
console.log(timeline.chain.valid, timeline.events.map((event) => event.kind));
Python
current = requests.get(f"https://api.pdfloft.com/v1/signature-requests/{request['id']}", headers=headers).json()
print(current["status"], [f"{s['name']}: {s['status']}" for s in current["signers"]])

timeline = requests.get(f"https://api.pdfloft.com/v1/signature-requests/{request['id']}/timeline", headers=headers).json()
print(timeline["chain"]["valid"], [event["kind"] for event in timeline["events"]])

4. Download the signed document and the certificate

When every signer has signed, the request is completed and signedDocumentAvailable is true. The signed PDF carries the signatures and, when sealing is on, a seal that any PDF reader can verify. The completion certificate lists who signed, when, from where, and the root of the trail. Asking before the request is complete is answered with signature.not_completed.

curl
curl https://api.pdfloft.com/v1/signature-requests/cm3x8k2f10010/signed-document \
  -H "Authorization: Bearer $PDFLOFT_API_KEY"
curl https://api.pdfloft.com/v1/signature-requests/cm3x8k2f10010/certificate \
  -H "Authorization: Bearer $PDFLOFT_API_KEY"
# Each answer holds a signed link, valid for five minutes. Fetch it without the key:
curl -o signed.pdf "<url from the answer>"
JavaScript
const signed = await fetch(`https://api.pdfloft.com/v1/signature-requests/${request.id}/signed-document`, { headers }).then((r) => r.json());
const certificate = await fetch(`https://api.pdfloft.com/v1/signature-requests/${request.id}/certificate`, { headers }).then((r) => r.json());
const signedPdf = await fetch(signed.url).then((r) => r.arrayBuffer()); // the signed link needs no key
Python
signed = requests.get(f"https://api.pdfloft.com/v1/signature-requests/{request['id']}/signed-document", headers=headers).json()
certificate = requests.get(f"https://api.pdfloft.com/v1/signature-requests/{request['id']}/certificate", headers=headers).json()
open("signed.pdf", "wb").write(requests.get(signed["url"]).content)  # the signed link needs no key

Remind and void

A reminder gives each signer whose turn it is a new link and ends the old one. Voiding cancels a request that is not complete; the signers who were asked are told and their links stop working. A completed request cannot be voided. Only the sender, or an owner or admin for an account-wide request, can send, remind or void; otherwise the answer is forbidden.

curl
# Remind the signers whose turn it is
curl -X POST https://api.pdfloft.com/v1/signature-requests/cm3x8k2f10010/remind \
  -H "Authorization: Bearer $PDFLOFT_API_KEY" -H "Content-Type: application/json" -d '{}'

# Cancel the request
curl -X POST https://api.pdfloft.com/v1/signature-requests/cm3x8k2f10010/void \
  -H "Authorization: Bearer $PDFLOFT_API_KEY" -H "Content-Type: application/json" \
  -d '{"reason":"The terms changed."}'
JavaScript
await fetch(`https://api.pdfloft.com/v1/signature-requests/${request.id}/remind`, {
  method: 'POST',
  headers: { ...headers, 'Content-Type': 'application/json' },
  body: '{}',
});
await fetch(`https://api.pdfloft.com/v1/signature-requests/${request.id}/void`, {
  method: 'POST',
  headers: { ...headers, 'Content-Type': 'application/json' },
  body: JSON.stringify({ reason: 'The terms changed.' }),
});
Python
requests.post(f"https://api.pdfloft.com/v1/signature-requests/{request['id']}/remind", headers=headers, json={})
requests.post(
    f"https://api.pdfloft.com/v1/signature-requests/{request['id']}/void",
    headers=headers,
    json={"reason": "The terms changed."},
)

Signature events

Subscribe an endpoint to any of these events in Settings, then Developers, or with POST /v1/webhooks. Each is sent once when the change happens, and never for an action that was refused. The data is the request as GET /v1/signature-requests/{requestId} returns it. Events about one signer add signer; a decline or a void adds reason. Delivery, signing and retries are the same as for every webhook: see Webhooks.

Signature events
EventSent when
signature.request_sentA request was sent and the first signers were asked.
signature.viewedA signer opened the request, the first time.
signature.signedA signer signed.
signature.declinedA signer declined. The request is closed and the reason is in the data.
signature.completedEvery signer signed and the signed document and the certificate are ready.
signature.voidedThe request was voided. The reason is in the data.
signature.expiredThe request passed its expiry before every signer signed.
JSON
{
  "id": "evt_Xk2mQ9rT4vLw8aBc1dEf",
  "type": "signature.signed",
  "createdAt": "2026-11-04T10:05:00.000Z",
  "data": {
    "id": "cm3x8k2f10010",
    "title": "Services agreement",
    "status": "in_progress",
    "signerId": "cm3x8k2f10011",
    "signer": { "name": "Meera Iyer", "email": "meera@example.com", "role": "Client", "status": "signed" },
    "signers": [ ... ]
  }
}