Split PDF
Send a PDF. Get back one single-page PDF per page, in page order, each with its own link.
SyncPOST /v1/utilities/split-pdfWaits up to 90 s for the finished job.
AsyncPOST /v1/utilities/split-pdf/jobsAnswers 202 with the job id at once.
What it’s for
- Scanned batchesOne scan holding many documents becomes one file per page, ready to route or attach.
- Page-by-page reviewShow, sign or approve a single page without passing the whole document around.
- Attach one pagePull a page out of a statement to attach it to the transaction or ticket it belongs to.
-
Tools that take one pageEach file carries its
pageNumber, so order survives whatever comes next.
How it runs
-
DownloadStreams the PDF from your
http(s)://ors3://URL with a running size check. Over 150 MB stops early. - CheckRefuses encrypted, empty or unreadable PDFs, and anything over 500 pages.
- SplitCopies each page into its own PDF. Pages are copied, not re-rendered, so memory stays low.
-
Store
page-1.pdftopage-N.pdf, in page order, pluspageCount.
Pages are written one after another. For documents with hundreds of pages, use the
async route; the sync route answers 202 with the job id after 90 s
anyway.
Input
Send these fields as input in the request body. Anything not listed is
refused with 400.
| Field | Type | Required or optional | Accepts |
|---|---|---|---|
url |
string |
Required |
http://, https:// or s3:// URL, 1–4,096
characters. The PDF to split: at most 150 MB and 500 pages.
|
The request body
| Field | Required or optional | What it is |
|---|---|---|
input |
Required | The fields above. |
metadata |
Optional | Your own key–value pairs, returned on the job. At most 50 keys, 4 KB serialised. |
callbackUrl |
Optional | Async route only. The finished job is POSTed here, signed. Up to 2,048 characters. |
What comes back
| Field | Value |
|---|---|
files[].url
|
A link to the file. |
result.pageCount
|
Number of pages produced, equal to files.length. |
result.files
|
One entry per page, in page order. |
files[].pageNumber
|
The page number, starting at 1. |
files[].name
|
page-<N>.pdf, without zero padding. Order by
pageNumber.
|
files[].contentType
|
application/pdf |
Example
curl https://api.servicelabs.dev/v1/utilities/split-pdf \
-H "Authorization: Bearer $DU_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"input": {
"url": "https://example.com/statement.pdf"
}
}'
# 1. Start the job. Answers 202 straight away.
curl https://api.servicelabs.dev/v1/utilities/split-pdf/jobs \
-H "Authorization: Bearer $DU_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: split-pdf-0001" \
-d '{
"input": {
"url": "https://example.com/statement.pdf"
},
"callbackUrl": "https://example.com/hooks/servicelabs"
}'
# 2. Collect it by id. Long-polls for up to 60 s.
curl "https://api.servicelabs.dev/v1/jobs/job_01K7Z3Q4N5P6R7S8T9V0W1X2Y3?wait=60" \
-H "Authorization: Bearer $DU_API_KEY"
const API = 'https://api.servicelabs.dev/v1'
const headers = {
Authorization: `Bearer ${process.env.DU_API_KEY}`,
'Content-Type': 'application/json',
}
// Sync: holds the connection for up to 90 s
const res = await fetch(`${API}/utilities/split-pdf`, {
method: 'POST',
headers,
body: JSON.stringify({ input: { url: 'https://example.com/statement.pdf' } }),
})
if (res.status === 400) throw new Error((await res.json()).error.message)
let job = await res.json()
// Still running after 90 s? It carries on. Collect it by id.
while (job.status === 'queued' || job.status === 'running') {
job = await (await fetch(`${API}/jobs/${job.id}?wait=60`, { headers })).json()
}
if (job.status === 'succeeded') console.log(job.result.files.map((f) => f.url))
else console.error(job.error.code, job.error.message)
{
"id": "job_01K7Z3Q4N5P6R7S8T9V0W1X2Y3",
"object": "job",
"utility": "split-pdf",
"mode": "sync",
"status": "succeeded",
"createdAt": "2026-09-10T17:30:06.650Z",
"finishedAt": "2026-09-10T17:30:09.009Z",
"timings": { "queueMs": 0, "upstreamMs": 2140, "storageMs": 196, "totalMs": 2359 },
"result": {
"files": [
{
"url": "https://…/page-1.pdf",
"name": "page-1.pdf",
"pageNumber": 1,
"size": 48211,
"contentType": "application/pdf"
},
{
"url": "https://…/page-2.pdf",
"name": "page-2.pdf",
"pageNumber": 2,
"size": 51377,
"contentType": "application/pdf"
},
{
"url": "https://…/page-3.pdf",
"name": "page-3.pdf",
"pageNumber": 3,
"size": 39904,
"contentType": "application/pdf"
}
],
"pageCount": 3
},
"error": null,
"links": {
"self": "/v1/jobs/job_01K7Z3Q4N5P6R7S8T9V0W1X2Y3"
}
}
An example job. File links point at the stored output; download anything you need to keep.
{
"error": {
"code": "invalid_request",
"message": "input failed validation for split-pdf",
"details": [
{
"path": "input.url",
"message": "must be an http://, https:// or s3:// URL"
}
]
}
}
Input is checked before anything runs. A 400 creates no job and costs nothing.
Limits
| Limit | Value |
|---|---|
| Runs up to | 900 s (15 min) |
| Source size | 150 MB, checked while streaming |
| Pages | 500 |
| Download timeout | 60 s |
| Password-protected PDFs | Refused. Decrypt first. |
| Empty (0-page) PDFs | Refused |
| Sync wait | 90 s, then 202 with the job id |
| Long-poll | Up to 60 s per GET /v1/jobs/{id}?wait= |
| Rate limit | Per key; 600 requests a minute unless set otherwise |
| Output files | Kept for a limited period; download what you need |
When it fails
A 400 means nothing ran. Everything else is a failed job with
status: "failed", an error code and the utility’s own message.
| Situation | Status | Code | Job |
|---|---|---|---|
url missing, or not http(s):// or s3://
|
400 | invalid_request |
No job |
| The download failed, or the source is over 150 MB | 422 | upstream_rejected |
Failed job |
Not a valid PDF: Invalid or unsupported PDF: … |
422 | upstream_rejected |
Failed job |
Password-protected: Password-protected PDFs are not supported
|
422 | upstream_rejected |
Failed job |
| More than 500 pages, or none | 422 | upstream_rejected |
Failed job |
| An unexpected failure mid-split | 502 | upstream_error |
Failed job |
Any call can also get 401 unauthorized, 403 forbidden (the
key isn’t allowed this utility) or 429 rate_limited. The full list is in
Conventions & errors.
Reference
- Split PDF guideEvery option, with worked examples.
-
runSplitPdf
POST /v1/utilities/split-pdf -
createSplitPdfJob
POST /v1/utilities/split-pdf/jobs -
getJob
GET /v1/jobs/{id} - JobsStatuses, timings, retries and cancelling.
- CallbacksVerifying the signature on a finished job.
Also in service
Try Split PDF on your own file.
The console’s playground runs split-pdf against any URL you give it and shows the job it returns.