API reference

Every endpoint of the PDF Loft API with its parameters, responses and examples in curl, JavaScript and Python.

Conventions

The base address is https://api.pdfloft.com. Send Authorization: Bearer $PDFLOFT_API_KEY on every call. Bodies are JSON unless stated. The machine readable description is at /v1/openapi.json. Every answer carries X-Request-Id and the rate limit headers. Errors are described under Errors.

Tools

Find and run PDF tools.

GET /v1/tools

List tools. Needs the tools:run scope.

Every PDF tool, with the content types it reads, how many files one run takes, whether the API can run it now, and the options it takes as a JSON Schema.

Responses

Responses of List tools
StatusDescription
200The tools.
400The request is not valid.
401The API key is missing, wrong, revoked or expired.
402The account plan does not include the API.
403The key does not hold the scope this call needs.
429Too many calls. Wait for Retry-After seconds.

Response 200

Fields of the 200 answer of List tools
FieldTypeDescription
itemsarray of Tool
JSON
{
  "items": [
    {
      "id": "merge",
      "category": "organize",
      "runsOn": "server",
      "accepts": [
        "accepts"
      ],
      "minFiles": -9007199254740991,
      "maxFiles": -9007199254740991,
      "runnable": true,
      "options": {}
    }
  ]
}

Examples

curl
curl https://api.pdfloft.com/v1/tools -H "Authorization: Bearer $PDFLOFT_API_KEY"
JavaScript
const apiKey = '<your API key>'; // read it from your secret store
const response = await fetch(`https://api.pdfloft.com/v1/tools`, {
  method: 'GET',
  headers: { Authorization: `Bearer ${apiKey}` },
});
console.log(response.status, await response.json());
Python
import os
import requests

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

response = requests.get(
    "https://api.pdfloft.com/v1/tools",
    headers=headers,
)
print(response.status_code, response.text)

GET /v1/tools/{toolId}

Get a tool. Needs the tools:run scope.

One tool by its id, with the content types it reads, how many files a run takes and the options it takes as a JSON Schema.

Parameters

Parameters of Get a tool
NameInTypeRequiredDescription
toolIdpathstringYesThe tool id, such as merge, split or compress.

Responses

Responses of Get a tool
StatusDescription
200The tool.
400The request is not valid.
401The API key is missing, wrong, revoked or expired.
402The account plan does not include the API.
403The key does not hold the scope this call needs.
404No such tool.
429Too many calls. Wait for Retry-After seconds.

Response 200

Fields of the 200 answer of Get a tool
FieldTypeDescription
idstring
categorystring
runsOn"server" | "device"Where the web app processes the file: on the device means the file stays in the browser there. A run through the API always uploads the file and runs on the server, whatever this says; read runnable.
acceptsarray of stringThe content types a run through the API reads. Tools that place pictures on a document, such as sign, take the document first and then the pictures.
minFilesinteger
maxFilesinteger or nullNull means up to your plan limit.
runnablebooleanWhether this tool can be run through the API now.
optionsobject or nullThe options the run takes, as a JSON Schema: the same rules that refuse an invalid run with options.invalid. Null when the tool is not runnable.
JSON
{
  "id": "merge",
  "category": "organize",
  "runsOn": "server",
  "accepts": [
    "accepts"
  ],
  "minFiles": -9007199254740991,
  "maxFiles": -9007199254740991,
  "runnable": true,
  "options": {}
}

Examples

curl
curl https://api.pdfloft.com/v1/tools/merge -H "Authorization: Bearer $PDFLOFT_API_KEY"
JavaScript
const apiKey = '<your API key>'; // read it from your secret store
const toolId = "merge";
const response = await fetch(`https://api.pdfloft.com/v1/tools/${toolId}`, {
  method: 'GET',
  headers: { Authorization: `Bearer ${apiKey}` },
});
console.log(response.status, await response.json());
Python
import os
import requests

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

response = requests.get(
    f"https://api.pdfloft.com/v1/tools/{toolId}",
    headers=headers,
)
print(response.status_code, response.text)

POST /v1/tools/{toolId}/runs

Run a tool. Needs the tools:run scope.

Starts a run on uploaded files and answers at once with the run. The options are those the tool lists at GET /v1/tools/{toolId}; options that do not fit are refused with options.invalid before anything is counted. The API runs the tool on the server, so the files are uploaded, processed and deleted two hours after the run, whatever the web app does for that tool. Tools that place pictures on a document (sign, editor) take the document first and the pictures after it. Poll the run, or subscribe to the job.completed and job.failed webhook events. A run counts against the same task pool as the web; the count is reserved now and committed when the run succeeds.

Parameters

Parameters of Run a tool
NameInTypeRequiredDescription
toolIdpathstringYesThe tool id.
Idempotency-KeyheaderstringNoA unique value of 1 to 255 characters. A repeat with the same key and body returns the first answer; the same key with a different body is refused with idempotency_key_reused. Keys are kept for 24 hours.

Request body (JSON)

Request body of Run a tool
FieldTypeRequiredDescription
filesarray of stringYesThe uploaded files, in the order the tool reads them.
optionsobjectNoThe tool options. Each tool lists the options it takes, as a JSON Schema, at GET /v1/tools/{toolId}, and the Tools page of the guide lists them all. Options that do not fit are refused with options.invalid.
JSON
{
  "files": [
    "file_Qm3nT9xvB2kLw8RzYc1pAe5H"
  ],
  "options": {}
}

Responses

Responses of Run a tool
StatusDescription
202The run was accepted.
400The options do not fit the tool (options.invalid), or the number of files does not.
401The API key is missing, wrong, revoked or expired.
402The tasks or AI credits of this period are used. The details name the limit and when it resets.
403The key does not hold the scope this call needs.
404No such tool or file.
429Too many calls. Wait for Retry-After seconds.

Response 202

Fields of the 202 answer of Run a tool
FieldTypeDescription
idstring
toolstring
status"queued" | "running" | "succeeded" | "failed" | "cancelled"
stagestring
progressinteger
errorstring or nullA stable error code when the run failed.
resultsintegerHow many result files the run produced.
createdAtstring (date-time)
finishedAtstring (date-time) or null
resultExpiresAtstring (date-time) or nullResults are deleted two hours after the run, unless saved as a document.
JSON
{
  "id": "cm3x8k2f10001",
  "tool": "merge",
  "status": "queued",
  "stage": "CONVERTING",
  "progress": 0,
  "error": null,
  "results": -9007199254740991,
  "createdAt": "2026-11-04T10:00:00.000Z",
  "finishedAt": "2026-11-04T10:00:00.000Z",
  "resultExpiresAt": "2026-11-04T10:00:00.000Z"
}

Examples

curl
curl -X POST https://api.pdfloft.com/v1/tools/merge/runs -H "Authorization: Bearer $PDFLOFT_API_KEY" -H "Idempotency-Key: $(uuidgen)" -H "Content-Type: application/json" -d '{"files":["file_Qm3nT9xvB2kLw8RzYc1pAe5H","file_Zr7sD4jKp0uVn6XaMb2cTe9G"]}'
JavaScript
const apiKey = '<your API key>'; // read it from your secret store
const toolId = "merge";
const response = await fetch(`https://api.pdfloft.com/v1/tools/${toolId}/runs`, {
  method: 'POST',
  headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    "files": [
      "file_Qm3nT9xvB2kLw8RzYc1pAe5H"
    ],
    "options": {}
  }),
});
console.log(response.status, await response.json());
Python
import os
import requests

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

response = requests.post(
    f"https://api.pdfloft.com/v1/tools/{toolId}/runs",
    headers=headers,
    json={
      "files": [
        "file_Qm3nT9xvB2kLw8RzYc1pAe5H"
      ],
      "options": {}
    },
)
print(response.status_code, response.text)

Files

Upload the files a run reads.

POST /v1/files

Upload a file. Needs the tools:run scope.

Send the bytes of one file as the request body, with its Content-Type and an exact Content-Length (chunked uploads are not accepted). The largest file is your plan limit: 25 MB on Free up to 1 GB on Enterprise. The returned id goes into a run. An uploaded file that no run uses is deleted after ten minutes.

Request body (file)

The file bytes.

Responses

Responses of Upload a file
StatusDescription
201The file was stored.
400The Content-Length is missing or the body is a different size.
401The API key is missing, wrong, revoked or expired.
402The account plan does not include the API.
403The key does not hold the scope this call needs.
413The file is larger than your plan allows.
415No tool reads this content type.
429Too many calls. Wait for Retry-After seconds.

Response 201

Fields of the 201 answer of Upload a file
FieldTypeDescription
idstringThe id of an uploaded file.
sizeBytesinteger
contentTypestring
expiresAtstring (date-time)An uploaded file that no run uses is deleted after this time.
JSON
{
  "id": "file_Qm3nT9xvB2kLw8RzYc1pAe5H",
  "sizeBytes": 482113,
  "contentType": "application/pdf",
  "expiresAt": "2026-11-04T10:00:00.000Z"
}

Examples

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
const apiKey = '<your API key>'; // read it from your secret store
const response = await fetch(`https://api.pdfloft.com/v1/files`, {
  method: 'POST',
  headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/pdf' },
  body: await readFile('contract.pdf'),
});
console.log(response.status, await response.json());
Python
import os
import requests

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

response = requests.post(
    "https://api.pdfloft.com/v1/files",
    headers={**headers, "Content-Type": "application/pdf"},
    data=open("contract.pdf", "rb"),
)
print(response.status_code, response.text)

Jobs

Follow a run and download its results.

GET /v1/jobs

List runs. Needs the jobs:read scope.

The runs started by the person who owns the key, newest first.

Parameters

Parameters of List runs
NameInTypeRequiredDescription
limitqueryintegerNoHow many items to return, 1 to 100.
cursorquerystringNoThe nextCursor of the previous page.

Responses

Responses of List runs
StatusDescription
200A page of runs.
400The request is not valid.
401The API key is missing, wrong, revoked or expired.
402The account plan does not include the API.
403The key does not hold the scope this call needs.
429Too many calls. Wait for Retry-After seconds.

Response 200

Fields of the 200 answer of List runs
FieldTypeDescription
itemsarray of Job
nextCursorstring or null
JSON
{
  "items": [
    {
      "id": "cm3x8k2f10001",
      "tool": "merge",
      "status": "queued",
      "stage": "CONVERTING",
      "progress": 0,
      "error": null,
      "results": -9007199254740991,
      "createdAt": "2026-11-04T10:00:00.000Z",
      "finishedAt": "2026-11-04T10:00:00.000Z",
      "resultExpiresAt": "2026-11-04T10:00:00.000Z"
    }
  ],
  "nextCursor": "nextCursor"
}

Examples

curl
curl "https://api.pdfloft.com/v1/jobs?limit=20" -H "Authorization: Bearer $PDFLOFT_API_KEY"
JavaScript
const apiKey = '<your API key>'; // read it from your secret store
const response = await fetch(`https://api.pdfloft.com/v1/jobs`, {
  method: 'GET',
  headers: { Authorization: `Bearer ${apiKey}` },
});
console.log(response.status, await response.json());
Python
import os
import requests

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

response = requests.get(
    "https://api.pdfloft.com/v1/jobs",
    headers=headers,
)
print(response.status_code, response.text)

GET /v1/jobs/{jobId}

Get a run. Needs the jobs:read scope.

The state of one run. Poll no faster than once a second.

Parameters

Parameters of Get a run
NameInTypeRequiredDescription
jobIdpathstringYesThe run id.

Responses

Responses of Get a run
StatusDescription
200The run.
400The request is not valid.
401The API key is missing, wrong, revoked or expired.
402The account plan does not include the API.
403The key does not hold the scope this call needs.
404No such run.
429Too many calls. Wait for Retry-After seconds.

Response 200

Fields of the 200 answer of Get a run
FieldTypeDescription
idstring
toolstring
status"queued" | "running" | "succeeded" | "failed" | "cancelled"
stagestring
progressinteger
errorstring or nullA stable error code when the run failed.
resultsintegerHow many result files the run produced.
createdAtstring (date-time)
finishedAtstring (date-time) or null
resultExpiresAtstring (date-time) or nullResults are deleted two hours after the run, unless saved as a document.
JSON
{
  "id": "cm3x8k2f10001",
  "tool": "merge",
  "status": "queued",
  "stage": "CONVERTING",
  "progress": 0,
  "error": null,
  "results": -9007199254740991,
  "createdAt": "2026-11-04T10:00:00.000Z",
  "finishedAt": "2026-11-04T10:00:00.000Z",
  "resultExpiresAt": "2026-11-04T10:00:00.000Z"
}

Examples

curl
curl https://api.pdfloft.com/v1/jobs/cm3x8k2f10001 -H "Authorization: Bearer $PDFLOFT_API_KEY"
JavaScript
const apiKey = '<your API key>'; // read it from your secret store
const jobId = "cm3x8k2f10001";
const response = await fetch(`https://api.pdfloft.com/v1/jobs/${jobId}`, {
  method: 'GET',
  headers: { Authorization: `Bearer ${apiKey}` },
});
console.log(response.status, await response.json());
Python
import os
import requests

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

response = requests.get(
    f"https://api.pdfloft.com/v1/jobs/{jobId}",
    headers=headers,
)
print(response.status_code, response.text)

POST /v1/jobs/{jobId}/cancel

Cancel a run. Needs the tools:run scope.

Ends a queued or running run and gives its tasks back. Cancelling a finished run changes nothing.

Parameters

Parameters of Cancel a run
NameInTypeRequiredDescription
jobIdpathstringYesThe run id.

Responses

Responses of Cancel a run
StatusDescription
200The run after the cancel.
400The request is not valid.
401The API key is missing, wrong, revoked or expired.
402The account plan does not include the API.
403The key does not hold the scope this call needs.
404No such run.
429Too many calls. Wait for Retry-After seconds.

Response 200

Fields of the 200 answer of Cancel a run
FieldTypeDescription
idstring
toolstring
status"queued" | "running" | "succeeded" | "failed" | "cancelled"
stagestring
progressinteger
errorstring or nullA stable error code when the run failed.
resultsintegerHow many result files the run produced.
createdAtstring (date-time)
finishedAtstring (date-time) or null
resultExpiresAtstring (date-time) or nullResults are deleted two hours after the run, unless saved as a document.
JSON
{
  "id": "cm3x8k2f10001",
  "tool": "merge",
  "status": "queued",
  "stage": "CONVERTING",
  "progress": 0,
  "error": null,
  "results": -9007199254740991,
  "createdAt": "2026-11-04T10:00:00.000Z",
  "finishedAt": "2026-11-04T10:00:00.000Z",
  "resultExpiresAt": "2026-11-04T10:00:00.000Z"
}

Examples

curl
curl -X POST https://api.pdfloft.com/v1/jobs/cm3x8k2f10001/cancel -H "Authorization: Bearer $PDFLOFT_API_KEY"
JavaScript
const apiKey = '<your API key>'; // read it from your secret store
const jobId = "cm3x8k2f10001";
const response = await fetch(`https://api.pdfloft.com/v1/jobs/${jobId}/cancel`, {
  method: 'POST',
  headers: { Authorization: `Bearer ${apiKey}` },
});
console.log(response.status, await response.json());
Python
import os
import requests

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

response = requests.post(
    f"https://api.pdfloft.com/v1/jobs/{jobId}/cancel",
    headers=headers,
)
print(response.status_code, response.text)

GET /v1/jobs/{jobId}/results/{index}

Download a result. Needs the jobs:read scope.

A signed link to one result file of a finished run, valid for five minutes. Results are deleted two hours after the run; save one as a document to keep it.

Parameters

Parameters of Download a result
NameInTypeRequiredDescription
jobIdpathstringYesThe run id.
indexpathstringYesThe result number, from 0.

Responses

Responses of Download a result
StatusDescription
200The link.
400The request is not valid.
401The API key is missing, wrong, revoked or expired.
402The account plan does not include the API.
403The key does not hold the scope this call needs.
404No such run or result.
410The result was deleted.
429Too many calls. Wait for Retry-After seconds.

Response 200

Fields of the 200 answer of Download a result
FieldTypeDescription
urlstring (uri)A signed link. Fetch it with a plain GET; it needs no API key.
expiresInSecondsinteger
JSON
{
  "url": "https://example.com/file",
  "expiresInSeconds": 300
}

Examples

curl
curl https://api.pdfloft.com/v1/jobs/cm3x8k2f10001/results/0 -H "Authorization: Bearer $PDFLOFT_API_KEY"
JavaScript
const apiKey = '<your API key>'; // read it from your secret store
const jobId = "cm3x8k2f10001";
const index = "0";
const response = await fetch(`https://api.pdfloft.com/v1/jobs/${jobId}/results/${index}`, {
  method: 'GET',
  headers: { Authorization: `Bearer ${apiKey}` },
});
console.log(response.status, await response.json());
Python
import os
import requests

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

response = requests.get(
    f"https://api.pdfloft.com/v1/jobs/{jobId}/results/{index}",
    headers=headers,
)
print(response.status_code, response.text)

Documents

The document library.

GET /v1/documents

List documents. Needs the documents:read scope.

The library documents the key can see: every account-wide document and the private documents of the key owner.

Parameters

Parameters of List documents
NameInTypeRequiredDescription
limitqueryintegerNoHow many items to return, 1 to 100.
cursorquerystringNoThe nextCursor of the previous page.

Responses

Responses of List documents
StatusDescription
200A page of documents.
400The request is not valid.
401The API key is missing, wrong, revoked or expired.
402The account plan does not include the API.
403The key does not hold the scope this call needs.
429Too many calls. Wait for Retry-After seconds.

Response 200

Fields of the 200 answer of List documents
FieldTypeDescription
itemsarray of Document
nextCursorstring or null
JSON
{
  "items": [
    {
      "id": "id",
      "name": "Lease.pdf",
      "visibility": "PRIVATE",
      "currentVersion": -9007199254740991,
      "sizeBytes": -9007199254740991,
      "pageCount": -9007199254740991,
      "createdAt": "2026-11-04T10:00:00.000Z",
      "updatedAt": "2026-11-04T10:00:00.000Z"
    }
  ],
  "nextCursor": "nextCursor"
}

Examples

curl
curl https://api.pdfloft.com/v1/documents -H "Authorization: Bearer $PDFLOFT_API_KEY"
JavaScript
const apiKey = '<your API key>'; // read it from your secret store
const response = await fetch(`https://api.pdfloft.com/v1/documents`, {
  method: 'GET',
  headers: { Authorization: `Bearer ${apiKey}` },
});
console.log(response.status, await response.json());
Python
import os
import requests

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

response = requests.get(
    "https://api.pdfloft.com/v1/documents",
    headers=headers,
)
print(response.status_code, response.text)

POST /v1/documents

Save a document. Needs the documents:write scope.

Saves an uploaded PDF or a run result to the library. The file is scanned for viruses and counted against the library storage of your plan.

Parameters

Parameters of Save a document
NameInTypeRequiredDescription
Idempotency-KeyheaderstringNoA unique value of 1 to 255 characters. A repeat with the same key and body returns the first answer; the same key with a different body is refused with idempotency_key_reused. Keys are kept for 24 hours.

Request body (JSON)

Request body of Save a document
FieldTypeRequiredDescription
sourceobject or objectYesAn uploaded PDF, or one result file of a run.
namestringYes
visibility"PRIVATE" | "ACCOUNT"No
JSON
{
  "source": {
    "fileId": "file_Qm3nT9xvB2kLw8RzYc1pAe5H"
  },
  "name": "Lease.pdf",
  "visibility": "ACCOUNT"
}

Responses

Responses of Save a document
StatusDescription
201The document.
400The request is not valid.
401The API key is missing, wrong, revoked or expired.
402The account plan does not include the API.
403The key does not hold the scope this call needs.
409The library is full.
422The file contains a virus.
429Too many calls. Wait for Retry-After seconds.

Response 201

Fields of the 201 answer of Save a document
FieldTypeDescription
idstring
namestring
visibility"PRIVATE" | "ACCOUNT"PRIVATE documents are visible to the person who owns the key only.
currentVersioninteger
sizeBytesinteger
pageCountinteger or null
createdAtstring (date-time)
updatedAtstring (date-time)
JSON
{
  "id": "id",
  "name": "Lease.pdf",
  "visibility": "PRIVATE",
  "currentVersion": -9007199254740991,
  "sizeBytes": -9007199254740991,
  "pageCount": -9007199254740991,
  "createdAt": "2026-11-04T10:00:00.000Z",
  "updatedAt": "2026-11-04T10:00:00.000Z"
}

Examples

curl
curl -X POST https://api.pdfloft.com/v1/documents -H "Authorization: Bearer $PDFLOFT_API_KEY" -H "Content-Type: application/json" -d '{"source":{"jobId":"cm3x8k2f10001","index":0},"name":"Merged.pdf"}'
JavaScript
const apiKey = '<your API key>'; // read it from your secret store
const response = await fetch(`https://api.pdfloft.com/v1/documents`, {
  method: 'POST',
  headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    "source": {
      "fileId": "file_Qm3nT9xvB2kLw8RzYc1pAe5H"
    },
    "name": "Lease.pdf",
    "visibility": "ACCOUNT"
  }),
});
console.log(response.status, await response.json());
Python
import os
import requests

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

response = requests.post(
    "https://api.pdfloft.com/v1/documents",
    headers=headers,
    json={
      "source": {
        "fileId": "file_Qm3nT9xvB2kLw8RzYc1pAe5H"
      },
      "name": "Lease.pdf",
      "visibility": "ACCOUNT"
    },
)
print(response.status_code, response.text)

GET /v1/documents/{documentId}

Get a document. Needs the documents:read scope.

One document of the library, with the size and page count of its current version.

Parameters

Parameters of Get a document
NameInTypeRequiredDescription
documentIdpathstringYesThe document id.

Responses

Responses of Get a document
StatusDescription
200The document.
400The request is not valid.
401The API key is missing, wrong, revoked or expired.
402The account plan does not include the API.
403The key does not hold the scope this call needs.
404No such document.
429Too many calls. Wait for Retry-After seconds.

Response 200

Fields of the 200 answer of Get a document
FieldTypeDescription
idstring
namestring
visibility"PRIVATE" | "ACCOUNT"PRIVATE documents are visible to the person who owns the key only.
currentVersioninteger
sizeBytesinteger
pageCountinteger or null
createdAtstring (date-time)
updatedAtstring (date-time)
JSON
{
  "id": "id",
  "name": "Lease.pdf",
  "visibility": "PRIVATE",
  "currentVersion": -9007199254740991,
  "sizeBytes": -9007199254740991,
  "pageCount": -9007199254740991,
  "createdAt": "2026-11-04T10:00:00.000Z",
  "updatedAt": "2026-11-04T10:00:00.000Z"
}

Examples

curl
curl https://api.pdfloft.com/v1/documents/cm3x8k2f10002 -H "Authorization: Bearer $PDFLOFT_API_KEY"
JavaScript
const apiKey = '<your API key>'; // read it from your secret store
const documentId = "cm3x8k2f10002";
const response = await fetch(`https://api.pdfloft.com/v1/documents/${documentId}`, {
  method: 'GET',
  headers: { Authorization: `Bearer ${apiKey}` },
});
console.log(response.status, await response.json());
Python
import os
import requests

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

response = requests.get(
    f"https://api.pdfloft.com/v1/documents/{documentId}",
    headers=headers,
)
print(response.status_code, response.text)

DELETE /v1/documents/{documentId}

Delete a document. Needs the documents:write scope.

Deletes a document and every version of it.

Parameters

Parameters of Delete a document
NameInTypeRequiredDescription
documentIdpathstringYesThe document id.

Responses

Responses of Delete a document
StatusDescription
204Deleted.
400The request is not valid.
401The API key is missing, wrong, revoked or expired.
402The account plan does not include the API.
403The key does not hold the scope this call needs.
404No such document.
429Too many calls. Wait for Retry-After seconds.

Examples

curl
curl -X DELETE https://api.pdfloft.com/v1/documents/cm3x8k2f10002 -H "Authorization: Bearer $PDFLOFT_API_KEY"
JavaScript
const apiKey = '<your API key>'; // read it from your secret store
const documentId = "cm3x8k2f10002";
const response = await fetch(`https://api.pdfloft.com/v1/documents/${documentId}`, {
  method: 'DELETE',
  headers: { Authorization: `Bearer ${apiKey}` },
});
console.log(response.status);
Python
import os
import requests

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

response = requests.delete(
    f"https://api.pdfloft.com/v1/documents/{documentId}",
    headers=headers,
)
print(response.status_code, response.text)

GET /v1/documents/{documentId}/download

Download a document. Needs the documents:read scope.

A signed link to the current version of a document, or to the version named in the query. Valid for five minutes.

Parameters

Parameters of Download a document
NameInTypeRequiredDescription
documentIdpathstringYesThe document id.
versionqueryintegerNoA version number. The current version when omitted.

Responses

Responses of Download a document
StatusDescription
200The link.
400The request is not valid.
401The API key is missing, wrong, revoked or expired.
402The account plan does not include the API.
403The key does not hold the scope this call needs.
404No such document or version.
429Too many calls. Wait for Retry-After seconds.

Response 200

Fields of the 200 answer of Download a document
FieldTypeDescription
urlstring (uri)A signed link. Fetch it with a plain GET; it needs no API key.
expiresInSecondsinteger
JSON
{
  "url": "https://example.com/file",
  "expiresInSeconds": 300
}

Examples

curl
curl https://api.pdfloft.com/v1/documents/cm3x8k2f10002/download -H "Authorization: Bearer $PDFLOFT_API_KEY"
JavaScript
const apiKey = '<your API key>'; // read it from your secret store
const documentId = "cm3x8k2f10002";
const response = await fetch(`https://api.pdfloft.com/v1/documents/${documentId}/download`, {
  method: 'GET',
  headers: { Authorization: `Bearer ${apiKey}` },
});
console.log(response.status, await response.json());
Python
import os
import requests

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

response = requests.get(
    f"https://api.pdfloft.com/v1/documents/{documentId}/download",
    headers=headers,
)
print(response.status_code, response.text)

Webhooks

Endpoints that receive events.

GET /v1/webhooks

List webhook endpoints. Needs the webhooks:manage scope.

Every webhook endpoint of the account, newest first.

Responses

Responses of List webhook endpoints
StatusDescription
200The endpoints.
400The request is not valid.
401The API key is missing, wrong, revoked or expired.
402The account plan does not include the API.
403The key does not hold the scope this call needs.
429Too many calls. Wait for Retry-After seconds.

Response 200

Fields of the 200 answer of List webhook endpoints
FieldTypeDescription
itemsarray of WebhookEndpoint
JSON
{
  "items": [
    {
      "id": "id",
      "url": "https://example.com/pdfloft/webhooks",
      "description": "description",
      "events": [
        "events"
      ],
      "status": "active",
      "secretHint": "secretHint",
      "createdAt": "2026-11-04T10:00:00.000Z"
    }
  ]
}

Examples

curl
curl https://api.pdfloft.com/v1/webhooks -H "Authorization: Bearer $PDFLOFT_API_KEY"
JavaScript
const apiKey = '<your API key>'; // read it from your secret store
const response = await fetch(`https://api.pdfloft.com/v1/webhooks`, {
  method: 'GET',
  headers: { Authorization: `Bearer ${apiKey}` },
});
console.log(response.status, await response.json());
Python
import os
import requests

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

response = requests.get(
    "https://api.pdfloft.com/v1/webhooks",
    headers=headers,
)
print(response.status_code, response.text)

POST /v1/webhooks

Create a webhook endpoint. Needs the webhooks:manage scope.

Registers an https address for events. The signing secret is in the answer once. Addresses that are private, loopback, link-local or a metadata address are refused.

Parameters

Parameters of Create a webhook endpoint
NameInTypeRequiredDescription
Idempotency-KeyheaderstringNoA unique value of 1 to 255 characters. A repeat with the same key and body returns the first answer; the same key with a different body is refused with idempotency_key_reused. Keys are kept for 24 hours.

Request body (JSON)

Request body of Create a webhook endpoint
FieldTypeRequiredDescription
urlstring (uri)YesAn https address that answers POST. Private, loopback and link-local addresses are refused.
eventsarray of "job.completed" | "job.failed" | "document.created" | "document.deleted" | "signature.request_sent" | "signature.viewed" | "signature.signed" | "signature.declined" | "signature.completed" | "signature.voided" | "signature.expired"Yes
descriptionstringNo
JSON
{
  "url": "https://example.com/file",
  "events": [
    "job.completed"
  ],
  "description": "description"
}

Responses

Responses of Create a webhook endpoint
StatusDescription
201The endpoint, with its secret.
400The address is refused.
401The API key is missing, wrong, revoked or expired.
402The account plan does not include the API.
403The key does not hold the scope this call needs.
409The account has the most endpoints it may.
429Too many calls. Wait for Retry-After seconds.

Response 201

Fields of the 201 answer of Create a webhook endpoint
FieldTypeDescription
idstring
urlstring (uri)
descriptionstring or null
eventsarray of string
status"active" | "disabled"
secretHintstringThe last characters of the signing secret.
createdAtstring (date-time)
secretstringThe signing secret. It is shown once and cannot be read again.
JSON
{
  "id": "id",
  "url": "https://example.com/pdfloft/webhooks",
  "description": "description",
  "events": [
    "events"
  ],
  "status": "active",
  "secretHint": "secretHint",
  "createdAt": "2026-11-04T10:00:00.000Z",
  "secret": "whsec_..."
}

Examples

curl
curl -X POST https://api.pdfloft.com/v1/webhooks -H "Authorization: Bearer $PDFLOFT_API_KEY" -H "Content-Type: application/json" -d '{"url":"https://example.com/pdfloft/webhooks","events":["job.completed","job.failed"]}'
JavaScript
const apiKey = '<your API key>'; // read it from your secret store
const response = await fetch(`https://api.pdfloft.com/v1/webhooks`, {
  method: 'POST',
  headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    "url": "https://example.com/file",
    "events": [
      "job.completed"
    ],
    "description": "description"
  }),
});
console.log(response.status, await response.json());
Python
import os
import requests

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

response = requests.post(
    "https://api.pdfloft.com/v1/webhooks",
    headers=headers,
    json={
      "url": "https://example.com/file",
      "events": [
        "job.completed"
      ],
      "description": "description"
    },
)
print(response.status_code, response.text)

DELETE /v1/webhooks/{endpointId}

Delete a webhook endpoint. Needs the webhooks:manage scope.

Removes an endpoint and its delivery log.

Parameters

Parameters of Delete a webhook endpoint
NameInTypeRequiredDescription
endpointIdpathstringYesThe endpoint id.

Responses

Responses of Delete a webhook endpoint
StatusDescription
204Deleted.
400The request is not valid.
401The API key is missing, wrong, revoked or expired.
402The account plan does not include the API.
403The key does not hold the scope this call needs.
404No such endpoint.
429Too many calls. Wait for Retry-After seconds.

Examples

curl
curl -X DELETE https://api.pdfloft.com/v1/webhooks/cm3x8k2f10003 -H "Authorization: Bearer $PDFLOFT_API_KEY"
JavaScript
const apiKey = '<your API key>'; // read it from your secret store
const endpointId = "cm3x8k2f10003";
const response = await fetch(`https://api.pdfloft.com/v1/webhooks/${endpointId}`, {
  method: 'DELETE',
  headers: { Authorization: `Bearer ${apiKey}` },
});
console.log(response.status);
Python
import os
import requests

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

response = requests.delete(
    f"https://api.pdfloft.com/v1/webhooks/{endpointId}",
    headers=headers,
)
print(response.status_code, response.text)

Signatures

Send documents for electronic signature, follow each signer and download the signed document and its certificate.

GET /v1/signature-templates

List signature templates. Needs the signatures:read scope.

The reusable layouts of the account: the document, the signer roles and the fields placed on it. Templates are shared with every member of the account. Make and edit them in the web app.

Responses

Responses of List signature templates
StatusDescription
200The templates.
400The request is not valid.
401The API key is missing, wrong, revoked or expired.
402The account plan does not include the API.
403The key does not hold the scope this call needs.
429Too many calls. Wait for Retry-After seconds.

Response 200

Fields of the 200 answer of List signature templates
FieldTypeDescription
itemsarray of SignatureTemplate
limitintegerHow many templates the plan keeps.
JSON
{
  "items": [
    {
      "id": "id",
      "name": "name",
      "description": "description",
      "signerRoles": [
        {
          "id": "id",
          "label": "label",
          "color": "color"
        }
      ],
      "fields": [
        {
          "id": "id",
          "type": "type",
          "label": "label",
          "page": -9007199254740991,
          "x": 1,
          "y": 1,
          "w": 1,
          "h": 1,
          "required": true,
          "signerRoleId": "signerRoleId"
        }
      ],
      "pageCount": -9007199254740991,
      "sizeBytes": -9007199254740991,
      "createdAt": "2026-11-04T10:00:00.000Z",
      "updatedAt": "2026-11-04T10:00:00.000Z"
    }
  ],
  "limit": -9007199254740991
}

Examples

curl
curl https://api.pdfloft.com/v1/signature-templates -H "Authorization: Bearer $PDFLOFT_API_KEY"
JavaScript
const apiKey = '<your API key>'; // read it from your secret store
const response = await fetch(`https://api.pdfloft.com/v1/signature-templates`, {
  method: 'GET',
  headers: { Authorization: `Bearer ${apiKey}` },
});
console.log(response.status, await response.json());
Python
import os
import requests

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

response = requests.get(
    "https://api.pdfloft.com/v1/signature-templates",
    headers=headers,
)
print(response.status_code, response.text)

GET /v1/signature-templates/{templateId}

Get a signature template. Needs the signatures:read scope.

One template with its signer roles and fields. Name a role by its id or its label when you send a request from the template.

Parameters

Parameters of Get a signature template
NameInTypeRequiredDescription
templateIdpathstringYesThe template id.

Responses

Responses of Get a signature template
StatusDescription
200The template.
400The request is not valid.
401The API key is missing, wrong, revoked or expired.
402The account plan does not include the API.
403The key does not hold the scope this call needs.
404No such template in this account.
429Too many calls. Wait for Retry-After seconds.

Response 200

Fields of the 200 answer of Get a signature template
FieldTypeDescription
idstring
namestring
descriptionstring or null
signerRolesarray of objectThe roles a request from this template names its signers by (the id or the label).
fieldsarray of object
pageCountinteger
sizeBytesinteger
createdAtstring (date-time)
updatedAtstring (date-time)
JSON
{
  "id": "id",
  "name": "name",
  "description": "description",
  "signerRoles": [
    {
      "id": "id",
      "label": "label",
      "color": "color"
    }
  ],
  "fields": [
    {
      "id": "id",
      "type": "type",
      "label": "label",
      "page": -9007199254740991,
      "x": 1,
      "y": 1,
      "w": 1,
      "h": 1,
      "required": true,
      "signerRoleId": "signerRoleId"
    }
  ],
  "pageCount": -9007199254740991,
  "sizeBytes": -9007199254740991,
  "createdAt": "2026-11-04T10:00:00.000Z",
  "updatedAt": "2026-11-04T10:00:00.000Z"
}

Examples

curl
curl https://api.pdfloft.com/v1/signature-templates/cm3x8k2f10020 -H "Authorization: Bearer $PDFLOFT_API_KEY"
JavaScript
const apiKey = '<your API key>'; // read it from your secret store
const templateId = "cm3x8k2f10020";
const response = await fetch(`https://api.pdfloft.com/v1/signature-templates/${templateId}`, {
  method: 'GET',
  headers: { Authorization: `Bearer ${apiKey}` },
});
console.log(response.status, await response.json());
Python
import os
import requests

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

response = requests.get(
    f"https://api.pdfloft.com/v1/signature-templates/{templateId}",
    headers=headers,
)
print(response.status_code, response.text)

POST /v1/signature-requests

Create a signature request. Needs the signatures:send scope.

Makes a signature request from an uploaded PDF (POST /v1/files), a library document or a template, with its signers and the fields they fill. A draft is created unless send is true, and a sent request spends one signature request of the plan allowance, exactly as in the web app: a send the allowance refuses (signatures.exhausted) creates nothing. The request belongs to the person who owns the key and is private to them unless visibility is ACCOUNT. Signers are emailed their links when the request is sent.

Parameters

Parameters of Create a signature request
NameInTypeRequiredDescription
Idempotency-KeyheaderstringNoA unique value of 1 to 255 characters. A repeat with the same key and body returns the first answer; the same key with a different body is refused with idempotency_key_reused. Keys are kept for 24 hours.

Request body (JSON)

Request body of Create a signature request
FieldTypeRequiredDescription
titlestringYes
messagestringNoA note shown to the signers.
visibility"PRIVATE" | "ACCOUNT"NoPRIVATE requests are visible to the person who owns the key only. ACCOUNT requests are visible to every member of the account.
signingOrder"sequential" | "parallel"No
locale"en" | "es" | "hi" | "fr" | "de" | "ja" | "zh" | "ar" | "pt-PT" | "he"NoThe language of the emails the signers receive.
expiresAtstring (date-time) or nullNoWhen the request stops accepting signatures. It must be in the future.
reminderCadenceHoursinteger or nullNoHours between reminders to a signer who has not signed. 72 when omitted; null turns reminders off.
sourceobject or object or objectYesThe document to send: a PDF uploaded to POST /v1/files, a document of the library, or a signature template. A template brings its own fields.
signersarray of SignatureSignerInputYes
fieldsarray of SignatureFieldInputNoWhere each signer signs. Leave out when a template supplies them.
sendbooleanNoSend the request at once. A send the plan allowance refuses creates nothing.
JSON
{
  "title": "Services agreement",
  "message": "message",
  "visibility": "PRIVATE",
  "signingOrder": "sequential",
  "locale": "en",
  "expiresAt": "2026-11-04T10:00:00.000Z",
  "reminderCadenceHours": 1,
  "source": {
    "fileId": "file_Qm3nT9xvB2kLw8RzYc1pAe5H"
  },
  "signers": [
    {
      "role": "Client",
      "sequence": 1,
      "name": "Meera Iyer",
      "email": "meera@example.com"
    }
  ],
  "fields": [
    {
      "signerRole": "Client",
      "type": "signature",
      "label": "Client signature",
      "page": 0,
      "x": 0,
      "y": 0,
      "w": 0,
      "h": 0,
      "required": true
    }
  ],
  "send": false
}

Responses

Responses of Create a signature request
StatusDescription
201The request.
400The signers, the fields or the document do not fit (validation_failed, with the problem in the details).
401The API key is missing, wrong, revoked or expired.
402The account has used its signature requests for this period. The details give the limit, the amount used and when it resets.
403The key does not hold the scope this call needs.
404No such file, document or template in this account.
429Too many calls. Wait for Retry-After seconds.

Response 201

Fields of the 201 answer of Create a signature request
FieldTypeDescription
idstring
titlestring
messagestring or null
status"draft" | "sent" | "in_progress" | "completed" | "declined" | "expired" | "voided"draft: not sent yet. sent: waiting for the first signer. in_progress: at least one signer signed. completed: every signer signed. declined: a signer declined. expired: the request passed its expiry. voided: the sender cancelled it.
visibility"PRIVATE" | "ACCOUNT"
signingOrder"sequential" | "parallel"
localestring
templateIdstring or null
pageCountinteger
sizeBytesinteger
createdAtstring (date-time)
sentAtstring (date-time) or null
expiresAtstring (date-time) or null
completedAtstring (date-time) or null
voidedAtstring (date-time) or null
voidReasonstring or null
reminderCadenceHoursinteger or null
signedDocumentAvailablebooleanTrue once every signer signed and the signed document and certificate can be downloaded.
signersarray of SignatureSigner
JSON
{
  "id": "cm3x8k2f10010",
  "title": "title",
  "message": "message",
  "status": "draft",
  "visibility": "PRIVATE",
  "signingOrder": "sequential",
  "locale": "locale",
  "templateId": "templateId",
  "pageCount": -9007199254740991,
  "sizeBytes": -9007199254740991,
  "createdAt": "2026-11-04T10:00:00.000Z",
  "sentAt": "2026-11-04T10:00:00.000Z",
  "expiresAt": "2026-11-04T10:00:00.000Z",
  "completedAt": "2026-11-04T10:00:00.000Z",
  "voidedAt": "2026-11-04T10:00:00.000Z",
  "voidReason": "voidReason",
  "reminderCadenceHours": -9007199254740991,
  "signedDocumentAvailable": true,
  "signers": [
    {
      "id": "id",
      "name": "name",
      "email": "email",
      "role": "role",
      "sequence": -9007199254740991,
      "status": "pending",
      "sentAt": "2026-11-04T10:00:00.000Z",
      "viewedAt": "2026-11-04T10:00:00.000Z",
      "signedAt": "2026-11-04T10:00:00.000Z",
      "declinedAt": "2026-11-04T10:00:00.000Z",
      "declinedReason": "declinedReason"
    }
  ]
}

Examples

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"}],"fields":[{"signerRole":"Client","type":"signature","label":"Client signature","page":0,"x":60,"y":100,"w":160,"h":40}],"send":true}'
JavaScript
const apiKey = '<your API key>'; // read it from your secret store
const response = await fetch(`https://api.pdfloft.com/v1/signature-requests`, {
  method: 'POST',
  headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    "title": "Services agreement",
    "message": "message",
    "visibility": "PRIVATE",
    "signingOrder": "sequential",
    "locale": "en",
    "expiresAt": "2026-11-04T10:00:00.000Z",
    "reminderCadenceHours": 1,
    "source": {
      "fileId": "file_Qm3nT9xvB2kLw8RzYc1pAe5H"
    },
    "signers": [
      {
        "role": "Client",
        "sequence": 1,
        "name": "Meera Iyer",
        "email": "meera@example.com"
      }
    ],
    "fields": [
      {
        "signerRole": "Client",
        "type": "signature",
        "label": "Client signature",
        "page": 0,
        "x": 0,
        "y": 0,
        "w": 0,
        "h": 0,
        "required": true
      }
    ],
    "send": false
  }),
});
console.log(response.status, await response.json());
Python
import os
import requests

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

response = requests.post(
    "https://api.pdfloft.com/v1/signature-requests",
    headers=headers,
    json={
      "title": "Services agreement",
      "message": "message",
      "visibility": "PRIVATE",
      "signingOrder": "sequential",
      "locale": "en",
      "expiresAt": "2026-11-04T10:00:00.000Z",
      "reminderCadenceHours": 1,
      "source": {
        "fileId": "file_Qm3nT9xvB2kLw8RzYc1pAe5H"
      },
      "signers": [
        {
          "role": "Client",
          "sequence": 1,
          "name": "Meera Iyer",
          "email": "meera@example.com"
        }
      ],
      "fields": [
        {
          "signerRole": "Client",
          "type": "signature",
          "label": "Client signature",
          "page": 0,
          "x": 0,
          "y": 0,
          "w": 0,
          "h": 0,
          "required": True
        }
      ],
      "send": False
    },
)
print(response.status_code, response.text)

GET /v1/signature-requests

List signature requests. Needs the signatures:read scope.

The requests the key can see, newest first: every account-wide request and the private requests of the person who owns the key. Filter by status, or by mine to list only the requests that person sent.

Parameters

Parameters of List signature requests
NameInTypeRequiredDescription
statusquerystringNodraft: not sent yet. sent: waiting for the first signer. in_progress: at least one signer signed. completed: every signer signed. declined: a signer declined. expired: the request passed its expiry. voided: the sender cancelled it.
minequerystringNotrue: only the requests the owner of the key sent.
limitqueryintegerNoHow many items to return, 1 to 100.
cursorquerystringNoThe nextCursor of the previous page.

Responses

Responses of List signature requests
StatusDescription
200A page of requests.
400The request is not valid.
401The API key is missing, wrong, revoked or expired.
402The account plan does not include the API.
403The key does not hold the scope this call needs.
429Too many calls. Wait for Retry-After seconds.

Response 200

Fields of the 200 answer of List signature requests
FieldTypeDescription
itemsarray of SignatureRequestSummary
nextCursorstring or null
JSON
{
  "items": [
    {
      "id": "id",
      "title": "title",
      "status": "draft",
      "visibility": "PRIVATE",
      "signingOrder": "sequential",
      "templateId": "templateId",
      "pageCount": -9007199254740991,
      "sizeBytes": -9007199254740991,
      "createdAt": "2026-11-04T10:00:00.000Z",
      "sentAt": "2026-11-04T10:00:00.000Z",
      "expiresAt": "2026-11-04T10:00:00.000Z",
      "completedAt": "2026-11-04T10:00:00.000Z",
      "signers": [
        {
          "id": "id",
          "name": "name",
          "status": "status",
          "sequence": -9007199254740991
        }
      ]
    }
  ],
  "nextCursor": "nextCursor"
}

Examples

curl
curl "https://api.pdfloft.com/v1/signature-requests?status=sent&limit=20" -H "Authorization: Bearer $PDFLOFT_API_KEY"
JavaScript
const apiKey = '<your API key>'; // read it from your secret store
const response = await fetch(`https://api.pdfloft.com/v1/signature-requests`, {
  method: 'GET',
  headers: { Authorization: `Bearer ${apiKey}` },
});
console.log(response.status, await response.json());
Python
import os
import requests

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

response = requests.get(
    "https://api.pdfloft.com/v1/signature-requests",
    headers=headers,
)
print(response.status_code, response.text)

POST /v1/signature-requests/bulk

Send a template to many signers. Needs the signatures:send scope.

Sends one request from a template for each row, each row naming the signers who fill the template roles. The batch is refused whole when it would pass the signature requests the plan allows; otherwise the answer lists the rows that were created and the rows that failed with a code.

Parameters

Parameters of Send a template to many signers
NameInTypeRequiredDescription
Idempotency-KeyheaderstringNoA unique value of 1 to 255 characters. A repeat with the same key and body returns the first answer; the same key with a different body is refused with idempotency_key_reused. Keys are kept for 24 hours.

Request body (JSON)

Request body of Send a template to many signers
FieldTypeRequiredDescription
templateIdstringYes
visibility"PRIVATE" | "ACCOUNT"NoPRIVATE requests are visible to the person who owns the key only. ACCOUNT requests are visible to every member of the account.
signingOrder"sequential" | "parallel"No
locale"en" | "es" | "hi" | "fr" | "de" | "ja" | "zh" | "ar" | "pt-PT" | "he"NoThe language of the emails the signers receive.
expiresAtstring (date-time) or nullNoWhen the request stops accepting signatures. It must be in the future.
reminderCadenceHoursinteger or nullNoHours between reminders to a signer who has not signed. 72 when omitted; null turns reminders off.
rowsarray of objectYesOne signature request for each row, up to 100. Each is sent at once. The whole batch is refused if it would pass the plan allowance.
JSON
{
  "templateId": "templateId",
  "visibility": "PRIVATE",
  "signingOrder": "sequential",
  "locale": "en",
  "expiresAt": "2026-11-04T10:00:00.000Z",
  "reminderCadenceHours": 1,
  "rows": [
    {
      "title": "title",
      "signers": [
        {
          "role": "role",
          "name": "name",
          "email": "email"
        }
      ]
    }
  ]
}

Responses

Responses of Send a template to many signers
StatusDescription
202The rows that were created and the rows that failed.
400The request is not valid.
401The API key is missing, wrong, revoked or expired.
402The account has used its signature requests for this period. The details give the limit, the amount used and when it resets.
403The key does not hold the scope this call needs.
404No such template in this account.
429Too many calls. Wait for Retry-After seconds.

Response 202

Fields of the 202 answer of Send a template to many signers
FieldTypeDescription
createdarray of object
failedarray of object
JSON
{
  "created": [
    {
      "row": -9007199254740991,
      "id": "id"
    }
  ],
  "failed": [
    {
      "row": -9007199254740991,
      "code": "code"
    }
  ]
}

Examples

curl
curl -X POST https://api.pdfloft.com/v1/signature-requests/bulk -H "Authorization: Bearer $PDFLOFT_API_KEY" -H "Content-Type: application/json" -d '{"templateId":"cm3x8k2f10020","rows":[{"title":"Services agreement for Northwind","signers":[{"role":"client","name":"Meera Iyer","email":"meera@example.com"}]}]}'
JavaScript
const apiKey = '<your API key>'; // read it from your secret store
const response = await fetch(`https://api.pdfloft.com/v1/signature-requests/bulk`, {
  method: 'POST',
  headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    "templateId": "templateId",
    "visibility": "PRIVATE",
    "signingOrder": "sequential",
    "locale": "en",
    "expiresAt": "2026-11-04T10:00:00.000Z",
    "reminderCadenceHours": 1,
    "rows": [
      {
        "title": "title",
        "signers": [
          {
            "role": "role",
            "name": "name",
            "email": "email"
          }
        ]
      }
    ]
  }),
});
console.log(response.status, await response.json());
Python
import os
import requests

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

response = requests.post(
    "https://api.pdfloft.com/v1/signature-requests/bulk",
    headers=headers,
    json={
      "templateId": "templateId",
      "visibility": "PRIVATE",
      "signingOrder": "sequential",
      "locale": "en",
      "expiresAt": "2026-11-04T10:00:00.000Z",
      "reminderCadenceHours": 1,
      "rows": [
        {
          "title": "title",
          "signers": [
            {
              "role": "role",
              "name": "name",
              "email": "email"
            }
          ]
        }
      ]
    },
)
print(response.status_code, response.text)

GET /v1/signature-requests/{requestId}

Get a signature request. Needs the signatures:read scope.

One request with its status and each signer: whether they were asked, opened it, signed or declined, and when. A request past its expiry reads as expired.

Parameters

Parameters of Get a signature request
NameInTypeRequiredDescription
requestIdpathstringYesThe signature request id.

Responses

Responses of Get a signature request
StatusDescription
200The request.
400The request is not valid.
401The API key is missing, wrong, revoked or expired.
402The account plan does not include the API.
403The key does not hold the scope this call needs.
404No such signature request in this account, or it is private to another person.
429Too many calls. Wait for Retry-After seconds.

Response 200

Fields of the 200 answer of Get a signature request
FieldTypeDescription
idstring
titlestring
messagestring or null
status"draft" | "sent" | "in_progress" | "completed" | "declined" | "expired" | "voided"draft: not sent yet. sent: waiting for the first signer. in_progress: at least one signer signed. completed: every signer signed. declined: a signer declined. expired: the request passed its expiry. voided: the sender cancelled it.
visibility"PRIVATE" | "ACCOUNT"
signingOrder"sequential" | "parallel"
localestring
templateIdstring or null
pageCountinteger
sizeBytesinteger
createdAtstring (date-time)
sentAtstring (date-time) or null
expiresAtstring (date-time) or null
completedAtstring (date-time) or null
voidedAtstring (date-time) or null
voidReasonstring or null
reminderCadenceHoursinteger or null
signedDocumentAvailablebooleanTrue once every signer signed and the signed document and certificate can be downloaded.
signersarray of SignatureSigner
JSON
{
  "id": "cm3x8k2f10010",
  "title": "title",
  "message": "message",
  "status": "draft",
  "visibility": "PRIVATE",
  "signingOrder": "sequential",
  "locale": "locale",
  "templateId": "templateId",
  "pageCount": -9007199254740991,
  "sizeBytes": -9007199254740991,
  "createdAt": "2026-11-04T10:00:00.000Z",
  "sentAt": "2026-11-04T10:00:00.000Z",
  "expiresAt": "2026-11-04T10:00:00.000Z",
  "completedAt": "2026-11-04T10:00:00.000Z",
  "voidedAt": "2026-11-04T10:00:00.000Z",
  "voidReason": "voidReason",
  "reminderCadenceHours": -9007199254740991,
  "signedDocumentAvailable": true,
  "signers": [
    {
      "id": "id",
      "name": "name",
      "email": "email",
      "role": "role",
      "sequence": -9007199254740991,
      "status": "pending",
      "sentAt": "2026-11-04T10:00:00.000Z",
      "viewedAt": "2026-11-04T10:00:00.000Z",
      "signedAt": "2026-11-04T10:00:00.000Z",
      "declinedAt": "2026-11-04T10:00:00.000Z",
      "declinedReason": "declinedReason"
    }
  ]
}

Examples

curl
curl https://api.pdfloft.com/v1/signature-requests/cm3x8k2f10010 -H "Authorization: Bearer $PDFLOFT_API_KEY"
JavaScript
const apiKey = '<your API key>'; // read it from your secret store
const requestId = "cm3x8k2f10010";
const response = await fetch(`https://api.pdfloft.com/v1/signature-requests/${requestId}`, {
  method: 'GET',
  headers: { Authorization: `Bearer ${apiKey}` },
});
console.log(response.status, await response.json());
Python
import os
import requests

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

response = requests.get(
    f"https://api.pdfloft.com/v1/signature-requests/{requestId}",
    headers=headers,
)
print(response.status_code, response.text)

GET /v1/signature-requests/{requestId}/timeline

Get the timeline of a request. Needs the signatures:read scope.

What happened to the request, in order: created, sent, opened, signed, reminded, declined, voided, expired or completed, with the time and the person. The chain field says whether the hash chain of the trail still verifies.

Parameters

Parameters of Get the timeline of a request
NameInTypeRequiredDescription
requestIdpathstringYesThe signature request id.

Responses

Responses of Get the timeline of a request
StatusDescription
200The timeline.
400The request is not valid.
401The API key is missing, wrong, revoked or expired.
402The account plan does not include the API.
403The key does not hold the scope this call needs.
404No such signature request in this account, or it is private to another person.
429Too many calls. Wait for Retry-After seconds.

Response 200

Fields of the 200 answer of Get the timeline of a request
FieldTypeDescription
chainobjectWhether the hash chain of the trail still verifies.
eventsarray of object
JSON
{
  "chain": {
    "valid": true,
    "brokenAt": "brokenAt"
  },
  "events": [
    {
      "id": "id",
      "kind": "signed",
      "at": "2026-11-04T10:00:00.000Z",
      "actorKind": "sender",
      "actorName": "actorName",
      "signerName": "signerName",
      "reason": "reason",
      "hash": "hash"
    }
  ]
}

Examples

curl
curl https://api.pdfloft.com/v1/signature-requests/cm3x8k2f10010/timeline -H "Authorization: Bearer $PDFLOFT_API_KEY"
JavaScript
const apiKey = '<your API key>'; // read it from your secret store
const requestId = "cm3x8k2f10010";
const response = await fetch(`https://api.pdfloft.com/v1/signature-requests/${requestId}/timeline`, {
  method: 'GET',
  headers: { Authorization: `Bearer ${apiKey}` },
});
console.log(response.status, await response.json());
Python
import os
import requests

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

response = requests.get(
    f"https://api.pdfloft.com/v1/signature-requests/{requestId}/timeline",
    headers=headers,
)
print(response.status_code, response.text)

POST /v1/signature-requests/{requestId}/send

Send a draft. Needs the signatures:send scope.

Sends a draft request: one signature request of the plan allowance is spent, the signers whose turn it is are emailed, and the signature.request_sent event is raised. Only the sender, or an owner or admin for an account-wide request, can send it.

Parameters

Parameters of Send a draft
NameInTypeRequiredDescription
requestIdpathstringYesThe signature request id.
Idempotency-KeyheaderstringNoA unique value of 1 to 255 characters. A repeat with the same key and body returns the first answer; the same key with a different body is refused with idempotency_key_reused. Keys are kept for 24 hours.

Responses

Responses of Send a draft
StatusDescription
200The request was sent.
400The request is not valid.
401The API key is missing, wrong, revoked or expired.
402The account has used its signature requests for this period. The details give the limit, the amount used and when it resets.
403The key owner may not change this request (forbidden).
404No such signature request in this account, or it is private to another person.
409The request was already sent (signature.not_draft).
429Too many calls. Wait for Retry-After seconds.

Response 200

Fields of the 200 answer of Send a draft
FieldTypeDescription
statusstring
emailsQueuedinteger
JSON
{
  "status": "sent",
  "emailsQueued": -9007199254740991
}

Examples

curl
curl -X POST https://api.pdfloft.com/v1/signature-requests/cm3x8k2f10010/send -H "Authorization: Bearer $PDFLOFT_API_KEY"
JavaScript
const apiKey = '<your API key>'; // read it from your secret store
const requestId = "cm3x8k2f10010";
const response = await fetch(`https://api.pdfloft.com/v1/signature-requests/${requestId}/send`, {
  method: 'POST',
  headers: { Authorization: `Bearer ${apiKey}` },
});
console.log(response.status, await response.json());
Python
import os
import requests

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

response = requests.post(
    f"https://api.pdfloft.com/v1/signature-requests/{requestId}/send",
    headers=headers,
)
print(response.status_code, response.text)

POST /v1/signature-requests/{requestId}/remind

Remind the signers. Needs the signatures:send scope.

Asks the signers whose turn it is to sign again. Each gets a new link and the old one stops working. A signer waiting for an earlier signer is not reminded.

Parameters

Parameters of Remind the signers
NameInTypeRequiredDescription
requestIdpathstringYesThe signature request id.

Request body (JSON)

Request body of Remind the signers
FieldTypeRequiredDescription
signerIdstringNoRemind one signer. Every signer whose turn it is when omitted.
JSON
{
  "signerId": "signerId"
}

Responses

Responses of Remind the signers
StatusDescription
200How many signers were reminded.
400The request is not valid.
401The API key is missing, wrong, revoked or expired.
402The account plan does not include the API.
403The key does not hold the scope this call needs.
404No such signature request in this account, or it is private to another person.
409The request is not waiting for signatures (signature.not_open).
429Too many calls. Wait for Retry-After seconds.

Response 200

Fields of the 200 answer of Remind the signers
FieldTypeDescription
remindersSentinteger
emailsQueuedinteger
JSON
{
  "remindersSent": -9007199254740991,
  "emailsQueued": -9007199254740991
}

Examples

curl
curl -X POST https://api.pdfloft.com/v1/signature-requests/cm3x8k2f10010/remind -H "Authorization: Bearer $PDFLOFT_API_KEY" -H "Content-Type: application/json" -d '{}'
JavaScript
const apiKey = '<your API key>'; // read it from your secret store
const requestId = "cm3x8k2f10010";
const response = await fetch(`https://api.pdfloft.com/v1/signature-requests/${requestId}/remind`, {
  method: 'POST',
  headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    "signerId": "signerId"
  }),
});
console.log(response.status, await response.json());
Python
import os
import requests

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

response = requests.post(
    f"https://api.pdfloft.com/v1/signature-requests/{requestId}/remind",
    headers=headers,
    json={
      "signerId": "signerId"
    },
)
print(response.status_code, response.text)

POST /v1/signature-requests/{requestId}/void

Void a request. Needs the signatures:send scope.

Cancels a request that is not complete. The signers who were asked are told, their links stop working, and the signature.voided event is raised. A completed request cannot be voided.

Parameters

Parameters of Void a request
NameInTypeRequiredDescription
requestIdpathstringYesThe signature request id.

Request body (JSON)

Request body of Void a request
FieldTypeRequiredDescription
reasonstringYesWhy the request is cancelled. The signers who were asked are told.
JSON
{
  "reason": "reason"
}

Responses

Responses of Void a request
StatusDescription
200The request was voided.
400The request is not valid.
401The API key is missing, wrong, revoked or expired.
402The account plan does not include the API.
403The key does not hold the scope this call needs.
404No such signature request in this account, or it is private to another person.
409The request is complete or already voided (signature.not_open).
429Too many calls. Wait for Retry-After seconds.

Response 200

Fields of the 200 answer of Void a request
FieldTypeDescription
status"voided"
JSON
{
  "status": "voided"
}

Examples

curl
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
const apiKey = '<your API key>'; // read it from your secret store
const requestId = "cm3x8k2f10010";
const response = await fetch(`https://api.pdfloft.com/v1/signature-requests/${requestId}/void`, {
  method: 'POST',
  headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    "reason": "reason"
  }),
});
console.log(response.status, await response.json());
Python
import os
import requests

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

response = requests.post(
    f"https://api.pdfloft.com/v1/signature-requests/{requestId}/void",
    headers=headers,
    json={
      "reason": "reason"
    },
)
print(response.status_code, response.text)

GET /v1/signature-requests/{requestId}/signed-document

Download the signed document. Needs the signatures:read scope.

A signed link to the signed PDF of a completed request, valid for five minutes. Until every signer has signed the answer is signature.not_completed.

Parameters

Parameters of Download the signed document
NameInTypeRequiredDescription
requestIdpathstringYesThe signature request id.

Responses

Responses of Download the signed document
StatusDescription
200The link.
400The request is not valid.
401The API key is missing, wrong, revoked or expired.
402The account plan does not include the API.
403The key does not hold the scope this call needs.
404No such signature request in this account, or it is private to another person.
409The request is not complete (signature.not_completed).
429Too many calls. Wait for Retry-After seconds.

Response 200

Fields of the 200 answer of Download the signed document
FieldTypeDescription
urlstring (uri)A signed link. Fetch it with a plain GET; it needs no API key.
expiresInSecondsinteger
JSON
{
  "url": "https://example.com/file",
  "expiresInSeconds": 300
}

Examples

curl
curl https://api.pdfloft.com/v1/signature-requests/cm3x8k2f10010/signed-document -H "Authorization: Bearer $PDFLOFT_API_KEY"
JavaScript
const apiKey = '<your API key>'; // read it from your secret store
const requestId = "cm3x8k2f10010";
const response = await fetch(`https://api.pdfloft.com/v1/signature-requests/${requestId}/signed-document`, {
  method: 'GET',
  headers: { Authorization: `Bearer ${apiKey}` },
});
console.log(response.status, await response.json());
Python
import os
import requests

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

response = requests.get(
    f"https://api.pdfloft.com/v1/signature-requests/{requestId}/signed-document",
    headers=headers,
)
print(response.status_code, response.text)

GET /v1/signature-requests/{requestId}/certificate

Download the completion certificate. Needs the signatures:read scope.

A signed link to the completion certificate of a completed request: who signed, when, from where, and the root of the trail. Valid for five minutes.

Parameters

Parameters of Download the completion certificate
NameInTypeRequiredDescription
requestIdpathstringYesThe signature request id.

Responses

Responses of Download the completion certificate
StatusDescription
200The link.
400The request is not valid.
401The API key is missing, wrong, revoked or expired.
402The account plan does not include the API.
403The key does not hold the scope this call needs.
404No such signature request in this account, or it is private to another person.
409The request is not complete (signature.not_completed).
429Too many calls. Wait for Retry-After seconds.

Response 200

Fields of the 200 answer of Download the completion certificate
FieldTypeDescription
urlstring (uri)A signed link. Fetch it with a plain GET; it needs no API key.
expiresInSecondsinteger
JSON
{
  "url": "https://example.com/file",
  "expiresInSeconds": 300
}

Examples

curl
curl https://api.pdfloft.com/v1/signature-requests/cm3x8k2f10010/certificate -H "Authorization: Bearer $PDFLOFT_API_KEY"
JavaScript
const apiKey = '<your API key>'; // read it from your secret store
const requestId = "cm3x8k2f10010";
const response = await fetch(`https://api.pdfloft.com/v1/signature-requests/${requestId}/certificate`, {
  method: 'GET',
  headers: { Authorization: `Bearer ${apiKey}` },
});
console.log(response.status, await response.json());
Python
import os
import requests

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

response = requests.get(
    f"https://api.pdfloft.com/v1/signature-requests/{requestId}/certificate",
    headers=headers,
)
print(response.status_code, response.text)