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.
- GET
/v1/tools - GET
/v1/tools/{toolId} - POST
/v1/files - POST
/v1/tools/{toolId}/runs - GET
/v1/jobs - GET
/v1/jobs/{jobId} - POST
/v1/jobs/{jobId}/cancel - GET
/v1/jobs/{jobId}/results/{index} - GET
/v1/documents - POST
/v1/documents - GET
/v1/documents/{documentId} - DELETE
/v1/documents/{documentId} - GET
/v1/documents/{documentId}/download - GET
/v1/webhooks - POST
/v1/webhooks - DELETE
/v1/webhooks/{endpointId} - GET
/v1/signature-templates - GET
/v1/signature-templates/{templateId} - POST
/v1/signature-requests - GET
/v1/signature-requests - POST
/v1/signature-requests/bulk - GET
/v1/signature-requests/{requestId} - GET
/v1/signature-requests/{requestId}/timeline - POST
/v1/signature-requests/{requestId}/send - POST
/v1/signature-requests/{requestId}/remind - POST
/v1/signature-requests/{requestId}/void - GET
/v1/signature-requests/{requestId}/signed-document - GET
/v1/signature-requests/{requestId}/certificate
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
| Status | Description |
|---|---|
200 | The tools. |
400 | The request is not valid. |
401 | The API key is missing, wrong, revoked or expired. |
402 | The account plan does not include the API. |
403 | The key does not hold the scope this call needs. |
429 | Too many calls. Wait for Retry-After seconds. |
Response 200
| Field | Type | Description |
|---|---|---|
items | array of Tool |
{
"items": [
{
"id": "merge",
"category": "organize",
"runsOn": "server",
"accepts": [
"accepts"
],
"minFiles": -9007199254740991,
"maxFiles": -9007199254740991,
"runnable": true,
"options": {}
}
]
}Examples
curl https://api.pdfloft.com/v1/tools -H "Authorization: Bearer $PDFLOFT_API_KEY"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());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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
toolId | path | string | Yes | The tool id, such as merge, split or compress. |
Responses
| Status | Description |
|---|---|
200 | The tool. |
400 | The request is not valid. |
401 | The API key is missing, wrong, revoked or expired. |
402 | The account plan does not include the API. |
403 | The key does not hold the scope this call needs. |
404 | No such tool. |
429 | Too many calls. Wait for Retry-After seconds. |
Response 200
| Field | Type | Description |
|---|---|---|
id | string | |
category | string | |
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. |
accepts | array of string | The 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. |
minFiles | integer | |
maxFiles | integer or null | Null means up to your plan limit. |
runnable | boolean | Whether this tool can be run through the API now. |
options | object or null | The 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. |
{
"id": "merge",
"category": "organize",
"runsOn": "server",
"accepts": [
"accepts"
],
"minFiles": -9007199254740991,
"maxFiles": -9007199254740991,
"runnable": true,
"options": {}
}Examples
curl https://api.pdfloft.com/v1/tools/merge -H "Authorization: Bearer $PDFLOFT_API_KEY"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());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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
toolId | path | string | Yes | The tool id. |
Idempotency-Key | header | string | No | A 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)
| Field | Type | Required | Description |
|---|---|---|---|
files | array of string | Yes | The uploaded files, in the order the tool reads them. |
options | object | No | The 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. |
{
"files": [
"file_Qm3nT9xvB2kLw8RzYc1pAe5H"
],
"options": {}
}Responses
| Status | Description |
|---|---|
202 | The run was accepted. |
400 | The options do not fit the tool (options.invalid), or the number of files does not. |
401 | The API key is missing, wrong, revoked or expired. |
402 | The tasks or AI credits of this period are used. The details name the limit and when it resets. |
403 | The key does not hold the scope this call needs. |
404 | No such tool or file. |
429 | Too many calls. Wait for Retry-After seconds. |
Response 202
| Field | Type | Description |
|---|---|---|
id | string | |
tool | string | |
status | "queued" | "running" | "succeeded" | "failed" | "cancelled" | |
stage | string | |
progress | integer | |
error | string or null | A stable error code when the run failed. |
results | integer | How many result files the run produced. |
createdAt | string (date-time) | |
finishedAt | string (date-time) or null | |
resultExpiresAt | string (date-time) or null | Results are deleted two hours after the run, unless saved as a document. |
{
"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 -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"]}'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());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
| Status | Description |
|---|---|
201 | The file was stored. |
400 | The Content-Length is missing or the body is a different size. |
401 | The API key is missing, wrong, revoked or expired. |
402 | The account plan does not include the API. |
403 | The key does not hold the scope this call needs. |
413 | The file is larger than your plan allows. |
415 | No tool reads this content type. |
429 | Too many calls. Wait for Retry-After seconds. |
Response 201
| Field | Type | Description |
|---|---|---|
id | string | The id of an uploaded file. |
sizeBytes | integer | |
contentType | string | |
expiresAt | string (date-time) | An uploaded file that no run uses is deleted after this time. |
{
"id": "file_Qm3nT9xvB2kLw8RzYc1pAe5H",
"sizeBytes": 482113,
"contentType": "application/pdf",
"expiresAt": "2026-11-04T10:00:00.000Z"
}Examples
curl -X POST https://api.pdfloft.com/v1/files -H "Authorization: Bearer $PDFLOFT_API_KEY" -H "Content-Type: application/pdf" --data-binary @contract.pdfconst 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());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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
limit | query | integer | No | How many items to return, 1 to 100. |
cursor | query | string | No | The nextCursor of the previous page. |
Responses
| Status | Description |
|---|---|
200 | A page of runs. |
400 | The request is not valid. |
401 | The API key is missing, wrong, revoked or expired. |
402 | The account plan does not include the API. |
403 | The key does not hold the scope this call needs. |
429 | Too many calls. Wait for Retry-After seconds. |
Response 200
| Field | Type | Description |
|---|---|---|
items | array of Job | |
nextCursor | string or null |
{
"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 "https://api.pdfloft.com/v1/jobs?limit=20" -H "Authorization: Bearer $PDFLOFT_API_KEY"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());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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
jobId | path | string | Yes | The run id. |
Responses
| Status | Description |
|---|---|
200 | The run. |
400 | The request is not valid. |
401 | The API key is missing, wrong, revoked or expired. |
402 | The account plan does not include the API. |
403 | The key does not hold the scope this call needs. |
404 | No such run. |
429 | Too many calls. Wait for Retry-After seconds. |
Response 200
| Field | Type | Description |
|---|---|---|
id | string | |
tool | string | |
status | "queued" | "running" | "succeeded" | "failed" | "cancelled" | |
stage | string | |
progress | integer | |
error | string or null | A stable error code when the run failed. |
results | integer | How many result files the run produced. |
createdAt | string (date-time) | |
finishedAt | string (date-time) or null | |
resultExpiresAt | string (date-time) or null | Results are deleted two hours after the run, unless saved as a document. |
{
"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 https://api.pdfloft.com/v1/jobs/cm3x8k2f10001 -H "Authorization: Bearer $PDFLOFT_API_KEY"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());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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
jobId | path | string | Yes | The run id. |
Responses
| Status | Description |
|---|---|
200 | The run after the cancel. |
400 | The request is not valid. |
401 | The API key is missing, wrong, revoked or expired. |
402 | The account plan does not include the API. |
403 | The key does not hold the scope this call needs. |
404 | No such run. |
429 | Too many calls. Wait for Retry-After seconds. |
Response 200
| Field | Type | Description |
|---|---|---|
id | string | |
tool | string | |
status | "queued" | "running" | "succeeded" | "failed" | "cancelled" | |
stage | string | |
progress | integer | |
error | string or null | A stable error code when the run failed. |
results | integer | How many result files the run produced. |
createdAt | string (date-time) | |
finishedAt | string (date-time) or null | |
resultExpiresAt | string (date-time) or null | Results are deleted two hours after the run, unless saved as a document. |
{
"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 -X POST https://api.pdfloft.com/v1/jobs/cm3x8k2f10001/cancel -H "Authorization: Bearer $PDFLOFT_API_KEY"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());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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
jobId | path | string | Yes | The run id. |
index | path | string | Yes | The result number, from 0. |
Responses
| Status | Description |
|---|---|
200 | The link. |
400 | The request is not valid. |
401 | The API key is missing, wrong, revoked or expired. |
402 | The account plan does not include the API. |
403 | The key does not hold the scope this call needs. |
404 | No such run or result. |
410 | The result was deleted. |
429 | Too many calls. Wait for Retry-After seconds. |
Response 200
| Field | Type | Description |
|---|---|---|
url | string (uri) | A signed link. Fetch it with a plain GET; it needs no API key. |
expiresInSeconds | integer |
{
"url": "https://example.com/file",
"expiresInSeconds": 300
}Examples
curl https://api.pdfloft.com/v1/jobs/cm3x8k2f10001/results/0 -H "Authorization: Bearer $PDFLOFT_API_KEY"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());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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
limit | query | integer | No | How many items to return, 1 to 100. |
cursor | query | string | No | The nextCursor of the previous page. |
Responses
| Status | Description |
|---|---|
200 | A page of documents. |
400 | The request is not valid. |
401 | The API key is missing, wrong, revoked or expired. |
402 | The account plan does not include the API. |
403 | The key does not hold the scope this call needs. |
429 | Too many calls. Wait for Retry-After seconds. |
Response 200
| Field | Type | Description |
|---|---|---|
items | array of Document | |
nextCursor | string or null |
{
"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 https://api.pdfloft.com/v1/documents -H "Authorization: Bearer $PDFLOFT_API_KEY"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());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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
Idempotency-Key | header | string | No | A 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)
| Field | Type | Required | Description |
|---|---|---|---|
source | object or object | Yes | An uploaded PDF, or one result file of a run. |
name | string | Yes | |
visibility | "PRIVATE" | "ACCOUNT" | No |
{
"source": {
"fileId": "file_Qm3nT9xvB2kLw8RzYc1pAe5H"
},
"name": "Lease.pdf",
"visibility": "ACCOUNT"
}Responses
| Status | Description |
|---|---|
201 | The document. |
400 | The request is not valid. |
401 | The API key is missing, wrong, revoked or expired. |
402 | The account plan does not include the API. |
403 | The key does not hold the scope this call needs. |
409 | The library is full. |
422 | The file contains a virus. |
429 | Too many calls. Wait for Retry-After seconds. |
Response 201
| Field | Type | Description |
|---|---|---|
id | string | |
name | string | |
visibility | "PRIVATE" | "ACCOUNT" | PRIVATE documents are visible to the person who owns the key only. |
currentVersion | integer | |
sizeBytes | integer | |
pageCount | integer or null | |
createdAt | string (date-time) | |
updatedAt | string (date-time) |
{
"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 -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"}'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());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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
documentId | path | string | Yes | The document id. |
Responses
| Status | Description |
|---|---|
200 | The document. |
400 | The request is not valid. |
401 | The API key is missing, wrong, revoked or expired. |
402 | The account plan does not include the API. |
403 | The key does not hold the scope this call needs. |
404 | No such document. |
429 | Too many calls. Wait for Retry-After seconds. |
Response 200
| Field | Type | Description |
|---|---|---|
id | string | |
name | string | |
visibility | "PRIVATE" | "ACCOUNT" | PRIVATE documents are visible to the person who owns the key only. |
currentVersion | integer | |
sizeBytes | integer | |
pageCount | integer or null | |
createdAt | string (date-time) | |
updatedAt | string (date-time) |
{
"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 https://api.pdfloft.com/v1/documents/cm3x8k2f10002 -H "Authorization: Bearer $PDFLOFT_API_KEY"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());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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
documentId | path | string | Yes | The document id. |
Responses
| Status | Description |
|---|---|
204 | Deleted. |
400 | The request is not valid. |
401 | The API key is missing, wrong, revoked or expired. |
402 | The account plan does not include the API. |
403 | The key does not hold the scope this call needs. |
404 | No such document. |
429 | Too many calls. Wait for Retry-After seconds. |
Examples
curl -X DELETE https://api.pdfloft.com/v1/documents/cm3x8k2f10002 -H "Authorization: Bearer $PDFLOFT_API_KEY"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);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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
documentId | path | string | Yes | The document id. |
version | query | integer | No | A version number. The current version when omitted. |
Responses
| Status | Description |
|---|---|
200 | The link. |
400 | The request is not valid. |
401 | The API key is missing, wrong, revoked or expired. |
402 | The account plan does not include the API. |
403 | The key does not hold the scope this call needs. |
404 | No such document or version. |
429 | Too many calls. Wait for Retry-After seconds. |
Response 200
| Field | Type | Description |
|---|---|---|
url | string (uri) | A signed link. Fetch it with a plain GET; it needs no API key. |
expiresInSeconds | integer |
{
"url": "https://example.com/file",
"expiresInSeconds": 300
}Examples
curl https://api.pdfloft.com/v1/documents/cm3x8k2f10002/download -H "Authorization: Bearer $PDFLOFT_API_KEY"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());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
| Status | Description |
|---|---|
200 | The endpoints. |
400 | The request is not valid. |
401 | The API key is missing, wrong, revoked or expired. |
402 | The account plan does not include the API. |
403 | The key does not hold the scope this call needs. |
429 | Too many calls. Wait for Retry-After seconds. |
Response 200
| Field | Type | Description |
|---|---|---|
items | array of WebhookEndpoint |
{
"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 https://api.pdfloft.com/v1/webhooks -H "Authorization: Bearer $PDFLOFT_API_KEY"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());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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
Idempotency-Key | header | string | No | A 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)
| Field | Type | Required | Description |
|---|---|---|---|
url | string (uri) | Yes | An https address that answers POST. Private, loopback and link-local addresses are refused. |
events | array 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 | |
description | string | No |
{
"url": "https://example.com/file",
"events": [
"job.completed"
],
"description": "description"
}Responses
| Status | Description |
|---|---|
201 | The endpoint, with its secret. |
400 | The address is refused. |
401 | The API key is missing, wrong, revoked or expired. |
402 | The account plan does not include the API. |
403 | The key does not hold the scope this call needs. |
409 | The account has the most endpoints it may. |
429 | Too many calls. Wait for Retry-After seconds. |
Response 201
| Field | Type | Description |
|---|---|---|
id | string | |
url | string (uri) | |
description | string or null | |
events | array of string | |
status | "active" | "disabled" | |
secretHint | string | The last characters of the signing secret. |
createdAt | string (date-time) | |
secret | string | The signing secret. It is shown once and cannot be read again. |
{
"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 -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"]}'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());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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
endpointId | path | string | Yes | The endpoint id. |
Responses
| Status | Description |
|---|---|
204 | Deleted. |
400 | The request is not valid. |
401 | The API key is missing, wrong, revoked or expired. |
402 | The account plan does not include the API. |
403 | The key does not hold the scope this call needs. |
404 | No such endpoint. |
429 | Too many calls. Wait for Retry-After seconds. |
Examples
curl -X DELETE https://api.pdfloft.com/v1/webhooks/cm3x8k2f10003 -H "Authorization: Bearer $PDFLOFT_API_KEY"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);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
| Status | Description |
|---|---|
200 | The templates. |
400 | The request is not valid. |
401 | The API key is missing, wrong, revoked or expired. |
402 | The account plan does not include the API. |
403 | The key does not hold the scope this call needs. |
429 | Too many calls. Wait for Retry-After seconds. |
Response 200
| Field | Type | Description |
|---|---|---|
items | array of SignatureTemplate | |
limit | integer | How many templates the plan keeps. |
{
"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 https://api.pdfloft.com/v1/signature-templates -H "Authorization: Bearer $PDFLOFT_API_KEY"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());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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
templateId | path | string | Yes | The template id. |
Responses
| Status | Description |
|---|---|
200 | The template. |
400 | The request is not valid. |
401 | The API key is missing, wrong, revoked or expired. |
402 | The account plan does not include the API. |
403 | The key does not hold the scope this call needs. |
404 | No such template in this account. |
429 | Too many calls. Wait for Retry-After seconds. |
Response 200
| Field | Type | Description |
|---|---|---|
id | string | |
name | string | |
description | string or null | |
signerRoles | array of object | The roles a request from this template names its signers by (the id or the label). |
fields | array of object | |
pageCount | integer | |
sizeBytes | integer | |
createdAt | string (date-time) | |
updatedAt | string (date-time) |
{
"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 https://api.pdfloft.com/v1/signature-templates/cm3x8k2f10020 -H "Authorization: Bearer $PDFLOFT_API_KEY"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());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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
Idempotency-Key | header | string | No | A 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)
| Field | Type | Required | Description |
|---|---|---|---|
title | string | Yes | |
message | string | No | A note shown to the signers. |
visibility | "PRIVATE" | "ACCOUNT" | No | PRIVATE 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" | No | The language of the emails the signers receive. |
expiresAt | string (date-time) or null | No | When the request stops accepting signatures. It must be in the future. |
reminderCadenceHours | integer or null | No | Hours between reminders to a signer who has not signed. 72 when omitted; null turns reminders off. |
source | object or object or object | Yes | The 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. |
signers | array of SignatureSignerInput | Yes | |
fields | array of SignatureFieldInput | No | Where each signer signs. Leave out when a template supplies them. |
send | boolean | No | Send the request at once. A send the plan allowance refuses creates nothing. |
{
"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
| Status | Description |
|---|---|
201 | The request. |
400 | The signers, the fields or the document do not fit (validation_failed, with the problem in the details). |
401 | The API key is missing, wrong, revoked or expired. |
402 | The account has used its signature requests for this period. The details give the limit, the amount used and when it resets. |
403 | The key does not hold the scope this call needs. |
404 | No such file, document or template in this account. |
429 | Too many calls. Wait for Retry-After seconds. |
Response 201
| Field | Type | Description |
|---|---|---|
id | string | |
title | string | |
message | string 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" | |
locale | string | |
templateId | string or null | |
pageCount | integer | |
sizeBytes | integer | |
createdAt | string (date-time) | |
sentAt | string (date-time) or null | |
expiresAt | string (date-time) or null | |
completedAt | string (date-time) or null | |
voidedAt | string (date-time) or null | |
voidReason | string or null | |
reminderCadenceHours | integer or null | |
signedDocumentAvailable | boolean | True once every signer signed and the signed document and certificate can be downloaded. |
signers | array of SignatureSigner |
{
"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 -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}'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());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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
status | query | string | No | 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. |
mine | query | string | No | true: only the requests the owner of the key sent. |
limit | query | integer | No | How many items to return, 1 to 100. |
cursor | query | string | No | The nextCursor of the previous page. |
Responses
| Status | Description |
|---|---|
200 | A page of requests. |
400 | The request is not valid. |
401 | The API key is missing, wrong, revoked or expired. |
402 | The account plan does not include the API. |
403 | The key does not hold the scope this call needs. |
429 | Too many calls. Wait for Retry-After seconds. |
Response 200
| Field | Type | Description |
|---|---|---|
items | array of SignatureRequestSummary | |
nextCursor | string or null |
{
"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 "https://api.pdfloft.com/v1/signature-requests?status=sent&limit=20" -H "Authorization: Bearer $PDFLOFT_API_KEY"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());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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
Idempotency-Key | header | string | No | A 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)
| Field | Type | Required | Description |
|---|---|---|---|
templateId | string | Yes | |
visibility | "PRIVATE" | "ACCOUNT" | No | PRIVATE 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" | No | The language of the emails the signers receive. |
expiresAt | string (date-time) or null | No | When the request stops accepting signatures. It must be in the future. |
reminderCadenceHours | integer or null | No | Hours between reminders to a signer who has not signed. 72 when omitted; null turns reminders off. |
rows | array of object | Yes | One 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. |
{
"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
| Status | Description |
|---|---|
202 | The rows that were created and the rows that failed. |
400 | The request is not valid. |
401 | The API key is missing, wrong, revoked or expired. |
402 | The account has used its signature requests for this period. The details give the limit, the amount used and when it resets. |
403 | The key does not hold the scope this call needs. |
404 | No such template in this account. |
429 | Too many calls. Wait for Retry-After seconds. |
Response 202
| Field | Type | Description |
|---|---|---|
created | array of object | |
failed | array of object |
{
"created": [
{
"row": -9007199254740991,
"id": "id"
}
],
"failed": [
{
"row": -9007199254740991,
"code": "code"
}
]
}Examples
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"}]}]}'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());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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
requestId | path | string | Yes | The signature request id. |
Responses
| Status | Description |
|---|---|
200 | The request. |
400 | The request is not valid. |
401 | The API key is missing, wrong, revoked or expired. |
402 | The account plan does not include the API. |
403 | The key does not hold the scope this call needs. |
404 | No such signature request in this account, or it is private to another person. |
429 | Too many calls. Wait for Retry-After seconds. |
Response 200
| Field | Type | Description |
|---|---|---|
id | string | |
title | string | |
message | string 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" | |
locale | string | |
templateId | string or null | |
pageCount | integer | |
sizeBytes | integer | |
createdAt | string (date-time) | |
sentAt | string (date-time) or null | |
expiresAt | string (date-time) or null | |
completedAt | string (date-time) or null | |
voidedAt | string (date-time) or null | |
voidReason | string or null | |
reminderCadenceHours | integer or null | |
signedDocumentAvailable | boolean | True once every signer signed and the signed document and certificate can be downloaded. |
signers | array of SignatureSigner |
{
"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 https://api.pdfloft.com/v1/signature-requests/cm3x8k2f10010 -H "Authorization: Bearer $PDFLOFT_API_KEY"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());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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
requestId | path | string | Yes | The signature request id. |
Responses
| Status | Description |
|---|---|
200 | The timeline. |
400 | The request is not valid. |
401 | The API key is missing, wrong, revoked or expired. |
402 | The account plan does not include the API. |
403 | The key does not hold the scope this call needs. |
404 | No such signature request in this account, or it is private to another person. |
429 | Too many calls. Wait for Retry-After seconds. |
Response 200
| Field | Type | Description |
|---|---|---|
chain | object | Whether the hash chain of the trail still verifies. |
events | array of object |
{
"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 https://api.pdfloft.com/v1/signature-requests/cm3x8k2f10010/timeline -H "Authorization: Bearer $PDFLOFT_API_KEY"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());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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
requestId | path | string | Yes | The signature request id. |
Idempotency-Key | header | string | No | A 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
| Status | Description |
|---|---|
200 | The request was sent. |
400 | The request is not valid. |
401 | The API key is missing, wrong, revoked or expired. |
402 | The account has used its signature requests for this period. The details give the limit, the amount used and when it resets. |
403 | The key owner may not change this request (forbidden). |
404 | No such signature request in this account, or it is private to another person. |
409 | The request was already sent (signature.not_draft). |
429 | Too many calls. Wait for Retry-After seconds. |
Response 200
| Field | Type | Description |
|---|---|---|
status | string | |
emailsQueued | integer |
{
"status": "sent",
"emailsQueued": -9007199254740991
}Examples
curl -X POST https://api.pdfloft.com/v1/signature-requests/cm3x8k2f10010/send -H "Authorization: Bearer $PDFLOFT_API_KEY"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());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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
requestId | path | string | Yes | The signature request id. |
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
signerId | string | No | Remind one signer. Every signer whose turn it is when omitted. |
{
"signerId": "signerId"
}Responses
| Status | Description |
|---|---|
200 | How many signers were reminded. |
400 | The request is not valid. |
401 | The API key is missing, wrong, revoked or expired. |
402 | The account plan does not include the API. |
403 | The key does not hold the scope this call needs. |
404 | No such signature request in this account, or it is private to another person. |
409 | The request is not waiting for signatures (signature.not_open). |
429 | Too many calls. Wait for Retry-After seconds. |
Response 200
| Field | Type | Description |
|---|---|---|
remindersSent | integer | |
emailsQueued | integer |
{
"remindersSent": -9007199254740991,
"emailsQueued": -9007199254740991
}Examples
curl -X POST https://api.pdfloft.com/v1/signature-requests/cm3x8k2f10010/remind -H "Authorization: Bearer $PDFLOFT_API_KEY" -H "Content-Type: application/json" -d '{}'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());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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
requestId | path | string | Yes | The signature request id. |
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
reason | string | Yes | Why the request is cancelled. The signers who were asked are told. |
{
"reason": "reason"
}Responses
| Status | Description |
|---|---|
200 | The request was voided. |
400 | The request is not valid. |
401 | The API key is missing, wrong, revoked or expired. |
402 | The account plan does not include the API. |
403 | The key does not hold the scope this call needs. |
404 | No such signature request in this account, or it is private to another person. |
409 | The request is complete or already voided (signature.not_open). |
429 | Too many calls. Wait for Retry-After seconds. |
Response 200
| Field | Type | Description |
|---|---|---|
status | "voided" |
{
"status": "voided"
}Examples
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."}'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());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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
requestId | path | string | Yes | The signature request id. |
Responses
| Status | Description |
|---|---|
200 | The link. |
400 | The request is not valid. |
401 | The API key is missing, wrong, revoked or expired. |
402 | The account plan does not include the API. |
403 | The key does not hold the scope this call needs. |
404 | No such signature request in this account, or it is private to another person. |
409 | The request is not complete (signature.not_completed). |
429 | Too many calls. Wait for Retry-After seconds. |
Response 200
| Field | Type | Description |
|---|---|---|
url | string (uri) | A signed link. Fetch it with a plain GET; it needs no API key. |
expiresInSeconds | integer |
{
"url": "https://example.com/file",
"expiresInSeconds": 300
}Examples
curl https://api.pdfloft.com/v1/signature-requests/cm3x8k2f10010/signed-document -H "Authorization: Bearer $PDFLOFT_API_KEY"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());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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
requestId | path | string | Yes | The signature request id. |
Responses
| Status | Description |
|---|---|
200 | The link. |
400 | The request is not valid. |
401 | The API key is missing, wrong, revoked or expired. |
402 | The account plan does not include the API. |
403 | The key does not hold the scope this call needs. |
404 | No such signature request in this account, or it is private to another person. |
409 | The request is not complete (signature.not_completed). |
429 | Too many calls. Wait for Retry-After seconds. |
Response 200
| Field | Type | Description |
|---|---|---|
url | string (uri) | A signed link. Fetch it with a plain GET; it needs no API key. |
expiresInSeconds | integer |
{
"url": "https://example.com/file",
"expiresInSeconds": 300
}Examples
curl https://api.pdfloft.com/v1/signature-requests/cm3x8k2f10010/certificate -H "Authorization: Bearer $PDFLOFT_API_KEY"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());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)