Extract PDF attachments API
Send a PDF to POST /v2/convert/pdf/to/extract-attachments and each file embedded in it comes back as its own entry in Files[], under its attached name. The sample returns a CSV log and a text note.
- Endpoint
- POST /v2/
convert/ pdf/ to/ extract-attachments - Accepts
- Sample timing
- 1 ms median of 5 runsHow we measure
- Request size
- Up to 10 MB on the Free plan or 100 MB on paid plans, all files together
Request and response
Every sample below ran against the API before this page was published. Set PDFCONVERSIONAPI_KEY to your key and run it as is.
curl -sS -X POST \
https://api.pdfconversionapi.com/v2/convert/pdf/to/extract-attachments \
-H "Authorization: Bearer $PDFCONVERSIONAPI_KEY" \
-F File=@greenhouse-inspection.pdf \
-o response.json
# FileData is base64. Save the first file in converted/,
# or print the JSON error.
if jq -e '.Files' response.json > /dev/null; then
mkdir -p converted
name=$(jq -r '.Files[0].FileName' response.json)
jq -r '.Files[0].FileData' response.json \
| base64 --decode > "converted/$name"
else
cat response.json
fiimport base64
import os
import requests
key = os.environ["PDFCONVERSIONAPI_KEY"]
response = requests.post(
"https://api.pdfconversionapi.com/v2/convert/pdf/to/extract-attachments",
headers={"Authorization": f"Bearer {key}"},
files=[("File", open("greenhouse-inspection.pdf", "rb"))],
timeout=180,
)
result = response.json()
if not response.ok:
raise SystemExit(f"{result['Code']}: {result['Message']}")
os.makedirs("converted", exist_ok=True)
for file in result["Files"]:
path = os.path.join("converted", file["FileName"])
with open(path, "wb") as out:
out.write(base64.b64decode(file["FileData"]))
print(path, file["FileSize"], "bytes")
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
const form = new FormData();
const file = "greenhouse-inspection.pdf";
form.append("File", new Blob([await readFile(file)]), file);
const url =
"https://api.pdfconversionapi.com/v2/convert/pdf/to/extract-attachments";
const key = process.env.PDFCONVERSIONAPI_KEY;
const response = await fetch(url, {
method: "POST",
headers: { Authorization: `Bearer ${key}` },
body: form,
});
const result = await response.json();
if (!response.ok) {
throw new Error(`${result.Code}: ${result.Message}`);
}
await mkdir("converted", { recursive: true });
for (const file of result.Files) {
const path = join("converted", file.FileName);
await writeFile(path, Buffer.from(file.FileData, "base64"));
console.log(path, file.FileSize, "bytes");
}
{
"ConversionCost": 1,
"ConversionTime": 0.001,
"Files": [
{
"FileName": "readings-2026-10.csv",
"FileExt": "csv",
"FileSize": 4924,
"FileData": "dGltZXN0YW1wLHNlbnNvcixh…"
},
{
"FileName": "technician-notes.txt",
"FileExt": "txt",
"FileSize": 251,
"FileData": "VmVudCBjb250cm9sbGVyIGNo…"
}
]
}Response from our test run on 2026-11-03. ConversionTime shows the median of 5 runs. FileData is shortened here; the real response carries the whole file as base64.
The sample and the result
The same file the code sends. Download the input and the output and compare them yourself.
readings-2026-10.csv, first 30 of 169 lines
timestamp,sensor,air_temp_c,humidity_pct,vents_open2026-10-21T00:00,A,14.7,79,02026-10-21T01:00,A,14.6,77,02026-10-21T02:00,A,15.2,64,02026-10-21T03:00,A,15.1,77,02026-10-21T04:00,A,14.7,69,02026-10-21T05:00,A,14.6,77,02026-10-21T06:00,A,15.0,79,02026-10-21T07:00,A,17.8,66,02026-10-21T08:00,A,20.2,66,02026-10-21T09:00,A,23.2,78,02026-10-21T10:00,A,24.4,62,12026-10-21T11:00,A,25.8,64,12026-10-21T12:00,A,25.6,80,12026-10-21T13:00,A,25.1,62,12026-10-21T14:00,A,24.9,70,12026-10-21T15:00,A,22.7,74,02026-10-21T16:00,A,20.8,75,02026-10-21T17:00,A,17.7,80,02026-10-21T18:00,A,14.9,66,02026-10-21T19:00,A,15.5,65,02026-10-21T20:00,A,14.4,77,02026-10-21T21:00,A,14.7,75,02026-10-21T22:00,A,15.3,71,02026-10-21T23:00,A,14.9,74,02026-10-22T00:00,A,15.1,79,02026-10-22T01:00,A,15.1,80,02026-10-22T02:00,A,14.7,72,02026-10-22T03:00,A,15.2,62,02026-10-22T04:00,A,15.4,67,0
- Input
- greenhouse-inspection.pdf (115 KB)
- Output
- readings-2026-10.csv (4.8 KB)technician-notes.txt (251 bytes)
- Source
- A three-page greenhouse inspection report for a fictional nursery, with form fields, comments and attached files, made by us for this site. (CC0-1.0). How it was made
- Made by
- 2026-11-04, API build
9eeeb0640aba
Parameters
Send these as form fields next to the file. Names are case-insensitive. If you set one of the documented options below that we don't support yet, the request fails with a 400 that names it instead of returning a file made without it.
| Parameter | What it does |
|---|---|
IncludeAnnotationAttachmentsboolean, default true | Also return files pinned to pages as paperclip comments. With false, only the files in the Attachments panel come back: the sample then returns the CSV and not the technician's note on page 3. Any other value returns 400.Values: true, false |
Passwordstring | Opens a PDF that needs a password. Either the open password or the owner password works. Files that open freely but restrict printing or copying, and signed files, need no password at all. |
StoreFileboolean, default false | Return one download link (Url and FileId) per extracted file instead of the data. Links last 3 hours. |
What carries over, and what doesn't
Kept
- Each file's bytes: the extracted CSV and text note have the same SHA-256 hashes as the files we attached, and an attached OpenDocument text, Word file and PDF came back identical too
- Both places attachments live: the Attachments panel (the CSV) and paperclip comments on a page (the note on page 3)
- The attached file names,
readings-2026-10.csvandtechnician-notes.txt, used as the output names - Order: files from the Attachments panel first, then pinned files page by page. One attachment entry used in two places (the panel and a comment, or two comments) comes back once; two separate copies of a file both come back, as
notes.txtandnotes-2.txt
Lost or not yet supported
- Descriptions and dates stored with each attachment. The sample's "Hourly readings from sensor A, 21 to 27 October" is not returned;
/v2/convert/pdf/to/metalists it. - Which page a pinned file was on, and its comment's author.
- Folders in a stored name and characters unsafe in file names:
C:\Users\jm\Site Plan v2.PDFcame back asSite Plan v2.pdf, andrate*card?.csvasrate_card_.csv. Two files both callednotes.txtcame back asnotes.txtandnotes-2.txt. - Files listed only in the document's associated-files array (/AF) without an entry in the attachments list. Such a test file returned 422.
- References to files outside the PDF, which hold no data and are skipped.
Limits
- A PDF with nothing attached returns 422 "the input file could not be read: the PDF has no attached files".
- Up to 10,000 attached files and 1 GiB of attached data per PDF; more returns 413 "the attached files are too large or too many".
- A PDF that needs a password returns 422 "the input file is password-protected" until
Passwordholds the open or the owner password. - One PDF per request, up to 10 MB on the Free plan or 100 MB on paid plans.
Where teams use it
Get the machine-readable part of a hybrid invoice
Hybrid e-invoices carry an XML invoice inside the PDF that people read. When the XML sits in the attachments list, as the hybrid formats require, it comes back as its own file for your accounting import; our test file returned factur-x.xml unchanged.
Recover data files shipped inside reports
Inspection and lab reports often attach the raw measurements. The sample's CSV of 168 hourly readings and the note pinned to page 3 come back as two plain files you can load or archive without a PDF reader.
Check uploads for hidden files
An uploaded PDF can carry other files that a quick look at its pages never shows. Extract them in your intake step so they can be scanned, logged or rejected before the document reaches anyone else.
Questions
Does it return files attached as comments on a page?
Yes, by default. The sample's text note is pinned to page 3 with a paperclip icon and comes back next to the CSV from the Attachments panel. Send IncludeAnnotationAttachments=false to get only the panel's files.
How can I tell whether a PDF has attachments before extracting?
Send it to /v2/convert/pdf/to/meta. Its JSON has an attachments list with each file's name, size and description; for the sample it lists both files. Extracting from a file with none returns 422.
Do I need the password for a protected PDF?
Only if the PDF asks for one to open. A copy we protected with permission restrictions and no open password gave up both files without Password, and so did a signed copy. For a file with an open password, either that password or the owner password works.
Are the extracted files changed or converted?
No. Each file is returned exactly as it was embedded, with compressed streams unpacked. Our CSV and text note matched the originals byte for byte, and so did an OpenDocument text saved by an office suite (35,850 bytes, its generator entry untouched), a Word file and a small PDF that we attached to a test file.
What are the files called if two attachments share a name?
The second gets a number: notes.txt and notes-2.txt. Paths are cut to the last part, extensions are lowercased, and characters such as * and ? become _.
Convert your first PDF file in a minute.