On this page▾
Getting started
Optimize
Extract from PDF
Reference
API reference
Papyrio PDF API
PDF processing over plain HTTP. Post a multipart/form-data upload, get the processed file back in the response body. One request, one file. You never poll a job queue or register a callback.
We hold your file in memory, process it, and drop it when the response goes out. We keep no copy and share nothing. Uploads stop at 50 MB, and most requests finish in 2–7 seconds depending on the file and the operation.
- Base URL
- https://api.papyrio.net
- Endpoints
- 12 operations
- Free tier
- 50 requests / month
Quickstart
- 1Subscribe to a plan on RapidAPI. The Basic plan is free and needs no card.
- 2Copy your key from
Apps → Authorizationin the RapidAPI dashboard and export it asRAPIDAPI_KEY. - 3Send a file. The response body is the result.
curl -X POST "https://papyrio-pdf-tools.p.rapidapi.com/compress" \ -H "X-RapidAPI-Key: $RAPIDAPI_KEY" \ -H "X-RapidAPI-Host: papyrio-pdf-tools.p.rapidapi.com" \ -F "file=@document.pdf" \ -F "level=medium" \ --output output.pdfimport osimport requests url = "https://papyrio-pdf-tools.p.rapidapi.com/compress"headers = { "X-RapidAPI-Key": os.environ["RAPIDAPI_KEY"], "X-RapidAPI-Host": "papyrio-pdf-tools.p.rapidapi.com",} files = {"file": open("document.pdf", "rb")} data = {"level": "medium"} response = requests.post(url, headers=headers, files=files, data=data, timeout=120)response.raise_for_status() with open("output.pdf", "wb") as out: out.write(response.content)import { createReadStream } from "node:fs";import { writeFile } from "node:fs/promises"; const form = new FormData();form.append("file", await openAsFile("document.pdf"));form.append("level", "medium"); const response = await fetch("https://papyrio-pdf-tools.p.rapidapi.com/compress", { method: "POST", headers: { "X-RapidAPI-Key": process.env.RAPIDAPI_KEY, "X-RapidAPI-Host": "papyrio-pdf-tools.p.rapidapi.com", }, body: form,}); if (!response.ok) { const { error } = await response.json(); throw new Error(`${error.code}: ${error.message}`);} await writeFile("output.pdf", Buffer.from(await response.arrayBuffer())); // Node 20+: wrap a path as a File the FormData can stream.async function openAsFile(path) { const { openAsBlob } = await import("node:fs"); return openAsBlob(path);}package main import ( "bytes" "io" "mime/multipart" "net/http" "os") func main() { var body bytes.Buffer writer := multipart.NewWriter(&body) if err := addFile(writer, "file", "document.pdf"); err != nil { panic(err) } writer.WriteField("level", "medium") writer.Close() req, _ := http.NewRequest(http.MethodPost, "https://papyrio-pdf-tools.p.rapidapi.com/compress", &body) req.Header.Set("Content-Type", writer.FormDataContentType()) req.Header.Set("X-RapidAPI-Key", os.Getenv("RAPIDAPI_KEY")) req.Header.Set("X-RapidAPI-Host", "papyrio-pdf-tools.p.rapidapi.com") resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() out, _ := os.Create("output.pdf") defer out.Close() io.Copy(out, resp.Body)} func addFile(w *multipart.Writer, field, path string) error { f, err := os.Open(path) if err != nil { return err } defer f.Close() part, err := w.CreateFormFile(field, path) if err != nil { return err } _, err = io.Copy(part, f) return err}Authentication
The RapidAPI gateway authenticates every request. Send two headers:
X-RapidAPI-Key- Your subscription key, from Apps → Authorization in the RapidAPI dashboard. Keep it in an environment variable. Anyone who reads your client-side bundle can spend your quota.
X-RapidAPI-Host- Always
papyrio-pdf-tools.p.rapidapi.com.
Through the gateway we meter by subscriber rather than by IP, so a shared office address or a NAT costs you nothing. Calling https://api.papyrio.net without these headers still works, but it counts as ordinary free web traffic and gets the guest limit of three operations a day per IP address, not your subscription. Sending a X-RapidAPI-User header without the matching proxy secret returns 403 forbidden.
Requests and responses
- Every processing endpoint is
POSTwith amultipart/form-databody. Let your HTTP client write theContent-Typeheader. Setting it by hand breaks the multipart boundary, which is the most common cause of a400. - On success you get the file itself in the body. Read
Content-Typeto see what arrived, then write the bytes to disk as binary. - Errors come back as JSON, shaped
{"error":{"code","message"}}. Branch oncode. We rewordmessagewhenever it reads badly, so do not parse it. - Requests run synchronously. Give your client a timeout of 120 seconds or more, since OCR and large conversions are the slowest operations.
Rate limits and plans
Each subscription carries its own quota. Go past the monthly quota or the hourly rate and you get 429 with a Retry-After header in seconds.
| Plan | Price | Quota | Rate |
|---|---|---|---|
| Basic | Free | 50 requests / month | 10 / hour |
| ProPopular | $9.99 | 1,000 requests / month | 120 / hour |
| Ultra | $29.99 | 5,000 requests / month | 120 / hour |
| Mega | $99.99 | 25,000 requests / month | 120 / hour |
The rate column shows what you can sustain. RapidAPI’s plan page says 1,000 requests per hour on the paid tiers because the platform gives a provider no way to lower that number; our own limit is 120 per hour per subscription. A larger plan raises your monthly quota and leaves the hourly rate where it is, so write to us instead if you need more throughput.
Apart from your plan, we cap how many conversions run at once across all users. Hit that cap and the request returns 503 capacity_exceeded with Retry-After. Retry with backoff and it clears in seconds.
Errors
| Code | HTTP | Meaning | What to do |
|---|---|---|---|
bad_request | 400 | Wrong file type, corrupt PDF, or a missing required field. | Check the field names against the endpoint reference and confirm the file opens locally. |
forbidden | 403 | The request carried RapidAPI headers without the matching proxy secret. | Call the RapidAPI host and let the gateway set those headers for you. |
file_too_large | 413 | The upload exceeds the 50 MB limit. | Compress or split the file before sending it. |
unsupported_media_type | 415 | The body arrived as something other than multipart/form-data. | Let your HTTP client set the multipart boundary; do not set Content-Type by hand. |
method_not_allowed | 405 | Wrong HTTP method for this path. | Every processing endpoint is POST. Only /health is GET. |
rate_limit_exceeded | 429 | You have used up the hourly limit on your subscription. | Read the Retry-After header and wait that many seconds, or move to a higher plan. |
capacity_exceeded | 503 | Too many conversions are running at once across all users. | Wait the number of seconds in Retry-After and send it again. It clears in seconds. |
processing_failed | 500 | The file reached the converter but could not be processed. | The file is probably malformed. Run it through /repair on the web app, or send it to us. |
Optimize
Compress a PDF
/compressDownsamples embedded images and rebuilds the file with Ghostscript. Fonts stay fonts, so the text stays selectable. Image-heavy files shrink 30–80%. On a file that is already optimized the rebuild can come out bigger, and we send back your original untouched.
Body — multipart/form-data
filefilerequiredPDF file, 50 MB maximum.
levelstringoptionalCompression strength. low keeps images at 300 DPI, medium uses 150 DPI, high uses 72 DPI.
One of
low,medium,high— defaults tomedium
Returns
Compressed PDF
application/pdfcurl -X POST "https://papyrio-pdf-tools.p.rapidapi.com/compress" \ -H "X-RapidAPI-Key: $RAPIDAPI_KEY" \ -H "X-RapidAPI-Host: papyrio-pdf-tools.p.rapidapi.com" \ -F "file=@document.pdf" \ -F "level=medium" \ --output output.pdfimport osimport requests url = "https://papyrio-pdf-tools.p.rapidapi.com/compress"headers = { "X-RapidAPI-Key": os.environ["RAPIDAPI_KEY"], "X-RapidAPI-Host": "papyrio-pdf-tools.p.rapidapi.com",} files = {"file": open("document.pdf", "rb")} data = {"level": "medium"} response = requests.post(url, headers=headers, files=files, data=data, timeout=120)response.raise_for_status() with open("output.pdf", "wb") as out: out.write(response.content)import { createReadStream } from "node:fs";import { writeFile } from "node:fs/promises"; const form = new FormData();form.append("file", await openAsFile("document.pdf"));form.append("level", "medium"); const response = await fetch("https://papyrio-pdf-tools.p.rapidapi.com/compress", { method: "POST", headers: { "X-RapidAPI-Key": process.env.RAPIDAPI_KEY, "X-RapidAPI-Host": "papyrio-pdf-tools.p.rapidapi.com", }, body: form,}); if (!response.ok) { const { error } = await response.json(); throw new Error(`${error.code}: ${error.message}`);} await writeFile("output.pdf", Buffer.from(await response.arrayBuffer())); // Node 20+: wrap a path as a File the FormData can stream.async function openAsFile(path) { const { openAsBlob } = await import("node:fs"); return openAsBlob(path);}package main import ( "bytes" "io" "mime/multipart" "net/http" "os") func main() { var body bytes.Buffer writer := multipart.NewWriter(&body) if err := addFile(writer, "file", "document.pdf"); err != nil { panic(err) } writer.WriteField("level", "medium") writer.Close() req, _ := http.NewRequest(http.MethodPost, "https://papyrio-pdf-tools.p.rapidapi.com/compress", &body) req.Header.Set("Content-Type", writer.FormDataContentType()) req.Header.Set("X-RapidAPI-Key", os.Getenv("RAPIDAPI_KEY")) req.Header.Set("X-RapidAPI-Host", "papyrio-pdf-tools.p.rapidapi.com") resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() out, _ := os.Create("output.pdf") defer out.Close() io.Copy(out, resp.Body)} func addFile(w *multipart.Writer, field, path string) error { f, err := os.Open(path) if err != nil { return err } defer f.Close() part, err := w.CreateFormFile(field, path) if err != nil { return err } _, err = io.Copy(part, f) return err}Organize
Merge PDFs into one
/mergeCombines 2–10 PDFs into one document in upload order. Total size across all files has to stay under 200 MB. Unlock password-protected files before you send them.
Body — multipart/form-data
filesfile[]requiredTwo to ten PDF files, merged in the order sent.
Returns
Merged PDF
application/pdfcurl -X POST "https://papyrio-pdf-tools.p.rapidapi.com/merge" \ -H "X-RapidAPI-Key: $RAPIDAPI_KEY" \ -H "X-RapidAPI-Host: papyrio-pdf-tools.p.rapidapi.com" \ -F "files=@first.pdf" \ -F "files=@second.pdf" \ --output output.pdfimport osimport requests url = "https://papyrio-pdf-tools.p.rapidapi.com/merge"headers = { "X-RapidAPI-Key": os.environ["RAPIDAPI_KEY"], "X-RapidAPI-Host": "papyrio-pdf-tools.p.rapidapi.com",} files = [ ("files", open("first.pdf", "rb")), ("files", open("second.pdf", "rb")),] response = requests.post(url, headers=headers, files=files, timeout=120)response.raise_for_status() with open("output.pdf", "wb") as out: out.write(response.content)import { createReadStream } from "node:fs";import { writeFile } from "node:fs/promises"; const form = new FormData();form.append("files", await openAsFile("first.pdf"));form.append("files", await openAsFile("second.pdf")); const response = await fetch("https://papyrio-pdf-tools.p.rapidapi.com/merge", { method: "POST", headers: { "X-RapidAPI-Key": process.env.RAPIDAPI_KEY, "X-RapidAPI-Host": "papyrio-pdf-tools.p.rapidapi.com", }, body: form,}); if (!response.ok) { const { error } = await response.json(); throw new Error(`${error.code}: ${error.message}`);} await writeFile("output.pdf", Buffer.from(await response.arrayBuffer())); // Node 20+: wrap a path as a File the FormData can stream.async function openAsFile(path) { const { openAsBlob } = await import("node:fs"); return openAsBlob(path);}package main import ( "bytes" "io" "mime/multipart" "net/http" "os") func main() { var body bytes.Buffer writer := multipart.NewWriter(&body) if err := addFile(writer, "files", "first.pdf"); err != nil { panic(err) } if err := addFile(writer, "files", "second.pdf"); err != nil { panic(err) } writer.Close() req, _ := http.NewRequest(http.MethodPost, "https://papyrio-pdf-tools.p.rapidapi.com/merge", &body) req.Header.Set("Content-Type", writer.FormDataContentType()) req.Header.Set("X-RapidAPI-Key", os.Getenv("RAPIDAPI_KEY")) req.Header.Set("X-RapidAPI-Host", "papyrio-pdf-tools.p.rapidapi.com") resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() out, _ := os.Create("output.pdf") defer out.Close() io.Copy(out, resp.Body)} func addFile(w *multipart.Writer, field, path string) error { f, err := os.Open(path) if err != nil { return err } defer f.Close() part, err := w.CreateFormFile(field, path) if err != nil { return err } _, err = io.Copy(part, f) return err}Split a PDF into single pages
/split-pdfSeparates every page into its own PDF and returns them as a ZIP archive.
Body — multipart/form-data
filefilerequiredPDF file, 50 MB maximum.
Returns
ZIP archive, one PDF per page
application/zipcurl -X POST "https://papyrio-pdf-tools.p.rapidapi.com/split-pdf" \ -H "X-RapidAPI-Key: $RAPIDAPI_KEY" \ -H "X-RapidAPI-Host: papyrio-pdf-tools.p.rapidapi.com" \ -F "file=@document.pdf" \ --output pages.zipimport osimport requests url = "https://papyrio-pdf-tools.p.rapidapi.com/split-pdf"headers = { "X-RapidAPI-Key": os.environ["RAPIDAPI_KEY"], "X-RapidAPI-Host": "papyrio-pdf-tools.p.rapidapi.com",} files = {"file": open("document.pdf", "rb")} response = requests.post(url, headers=headers, files=files, timeout=120)response.raise_for_status() with open("pages.zip", "wb") as out: out.write(response.content)import { createReadStream } from "node:fs";import { writeFile } from "node:fs/promises"; const form = new FormData();form.append("file", await openAsFile("document.pdf")); const response = await fetch("https://papyrio-pdf-tools.p.rapidapi.com/split-pdf", { method: "POST", headers: { "X-RapidAPI-Key": process.env.RAPIDAPI_KEY, "X-RapidAPI-Host": "papyrio-pdf-tools.p.rapidapi.com", }, body: form,}); if (!response.ok) { const { error } = await response.json(); throw new Error(`${error.code}: ${error.message}`);} await writeFile("pages.zip", Buffer.from(await response.arrayBuffer())); // Node 20+: wrap a path as a File the FormData can stream.async function openAsFile(path) { const { openAsBlob } = await import("node:fs"); return openAsBlob(path);}package main import ( "bytes" "io" "mime/multipart" "net/http" "os") func main() { var body bytes.Buffer writer := multipart.NewWriter(&body) if err := addFile(writer, "file", "document.pdf"); err != nil { panic(err) } writer.Close() req, _ := http.NewRequest(http.MethodPost, "https://papyrio-pdf-tools.p.rapidapi.com/split-pdf", &body) req.Header.Set("Content-Type", writer.FormDataContentType()) req.Header.Set("X-RapidAPI-Key", os.Getenv("RAPIDAPI_KEY")) req.Header.Set("X-RapidAPI-Host", "papyrio-pdf-tools.p.rapidapi.com") resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() out, _ := os.Create("pages.zip") defer out.Close() io.Copy(out, resp.Body)} func addFile(w *multipart.Writer, field, path string) error { f, err := os.Open(path) if err != nil { return err } defer f.Close() part, err := w.CreateFormFile(field, path) if err != nil { return err } _, err = io.Copy(part, f) return err}Convert to PDF
Convert an Office document to PDF
/convertConverts Word (.doc, .docx, .odt), PowerPoint (.ppt, .pptx) and Excel (.xls, .xlsx) to PDF with LibreOffice, keeping fonts, tables, images and page layout.
Body — multipart/form-data
filefilerequiredOffice document, 50 MB maximum.
Returns
Converted PDF
application/pdfcurl -X POST "https://papyrio-pdf-tools.p.rapidapi.com/convert" \ -H "X-RapidAPI-Key: $RAPIDAPI_KEY" \ -H "X-RapidAPI-Host: papyrio-pdf-tools.p.rapidapi.com" \ -F "file=@report.docx" \ --output output.pdfimport osimport requests url = "https://papyrio-pdf-tools.p.rapidapi.com/convert"headers = { "X-RapidAPI-Key": os.environ["RAPIDAPI_KEY"], "X-RapidAPI-Host": "papyrio-pdf-tools.p.rapidapi.com",} files = {"file": open("report.docx", "rb")} response = requests.post(url, headers=headers, files=files, timeout=120)response.raise_for_status() with open("output.pdf", "wb") as out: out.write(response.content)import { createReadStream } from "node:fs";import { writeFile } from "node:fs/promises"; const form = new FormData();form.append("file", await openAsFile("report.docx")); const response = await fetch("https://papyrio-pdf-tools.p.rapidapi.com/convert", { method: "POST", headers: { "X-RapidAPI-Key": process.env.RAPIDAPI_KEY, "X-RapidAPI-Host": "papyrio-pdf-tools.p.rapidapi.com", }, body: form,}); if (!response.ok) { const { error } = await response.json(); throw new Error(`${error.code}: ${error.message}`);} await writeFile("output.pdf", Buffer.from(await response.arrayBuffer())); // Node 20+: wrap a path as a File the FormData can stream.async function openAsFile(path) { const { openAsBlob } = await import("node:fs"); return openAsBlob(path);}package main import ( "bytes" "io" "mime/multipart" "net/http" "os") func main() { var body bytes.Buffer writer := multipart.NewWriter(&body) if err := addFile(writer, "file", "report.docx"); err != nil { panic(err) } writer.Close() req, _ := http.NewRequest(http.MethodPost, "https://papyrio-pdf-tools.p.rapidapi.com/convert", &body) req.Header.Set("Content-Type", writer.FormDataContentType()) req.Header.Set("X-RapidAPI-Key", os.Getenv("RAPIDAPI_KEY")) req.Header.Set("X-RapidAPI-Host", "papyrio-pdf-tools.p.rapidapi.com") resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() out, _ := os.Create("output.pdf") defer out.Close() io.Copy(out, resp.Body)} func addFile(w *multipart.Writer, field, path string) error { f, err := os.Open(path) if err != nil { return err } defer f.Close() part, err := w.CreateFormFile(field, path) if err != nil { return err } _, err = io.Copy(part, f) return err}Convert an image to PDF
/image-to-pdfWraps a JPG, PNG or WebP image in a PDF page.
Body — multipart/form-data
filefilerequiredJPG, PNG or WebP image.
Returns
PDF containing the image
application/pdfcurl -X POST "https://papyrio-pdf-tools.p.rapidapi.com/image-to-pdf" \ -H "X-RapidAPI-Key: $RAPIDAPI_KEY" \ -H "X-RapidAPI-Host: papyrio-pdf-tools.p.rapidapi.com" \ -F "file=@scan.jpg" \ --output output.pdfimport osimport requests url = "https://papyrio-pdf-tools.p.rapidapi.com/image-to-pdf"headers = { "X-RapidAPI-Key": os.environ["RAPIDAPI_KEY"], "X-RapidAPI-Host": "papyrio-pdf-tools.p.rapidapi.com",} files = {"file": open("scan.jpg", "rb")} response = requests.post(url, headers=headers, files=files, timeout=120)response.raise_for_status() with open("output.pdf", "wb") as out: out.write(response.content)import { createReadStream } from "node:fs";import { writeFile } from "node:fs/promises"; const form = new FormData();form.append("file", await openAsFile("scan.jpg")); const response = await fetch("https://papyrio-pdf-tools.p.rapidapi.com/image-to-pdf", { method: "POST", headers: { "X-RapidAPI-Key": process.env.RAPIDAPI_KEY, "X-RapidAPI-Host": "papyrio-pdf-tools.p.rapidapi.com", }, body: form,}); if (!response.ok) { const { error } = await response.json(); throw new Error(`${error.code}: ${error.message}`);} await writeFile("output.pdf", Buffer.from(await response.arrayBuffer())); // Node 20+: wrap a path as a File the FormData can stream.async function openAsFile(path) { const { openAsBlob } = await import("node:fs"); return openAsBlob(path);}package main import ( "bytes" "io" "mime/multipart" "net/http" "os") func main() { var body bytes.Buffer writer := multipart.NewWriter(&body) if err := addFile(writer, "file", "scan.jpg"); err != nil { panic(err) } writer.Close() req, _ := http.NewRequest(http.MethodPost, "https://papyrio-pdf-tools.p.rapidapi.com/image-to-pdf", &body) req.Header.Set("Content-Type", writer.FormDataContentType()) req.Header.Set("X-RapidAPI-Key", os.Getenv("RAPIDAPI_KEY")) req.Header.Set("X-RapidAPI-Host", "papyrio-pdf-tools.p.rapidapi.com") resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() out, _ := os.Create("output.pdf") defer out.Close() io.Copy(out, resp.Body)} func addFile(w *multipart.Writer, field, path string) error { f, err := os.Open(path) if err != nil { return err } defer f.Close() part, err := w.CreateFormFile(field, path) if err != nil { return err } _, err = io.Copy(part, f) return err}Render a web page as PDF
/html-to-pdfFetches a URL and renders it to PDF in a headless browser. This one takes a URL rather than a file upload.
Body — multipart/form-data
urlstringrequiredPublicly reachable page URL.
Returns
Rendered PDF
application/pdfcurl -X POST "https://papyrio-pdf-tools.p.rapidapi.com/html-to-pdf" \ -H "X-RapidAPI-Key: $RAPIDAPI_KEY" \ -H "X-RapidAPI-Host: papyrio-pdf-tools.p.rapidapi.com" \ -F "url=https://example.com" \ --output output.pdfimport osimport requests url = "https://papyrio-pdf-tools.p.rapidapi.com/html-to-pdf"headers = { "X-RapidAPI-Key": os.environ["RAPIDAPI_KEY"], "X-RapidAPI-Host": "papyrio-pdf-tools.p.rapidapi.com",} data = {"url": "https://example.com"} response = requests.post(url, headers=headers, data=data, timeout=120)response.raise_for_status() with open("output.pdf", "wb") as out: out.write(response.content)import { createReadStream } from "node:fs";import { writeFile } from "node:fs/promises"; const form = new FormData();form.append("url", "https://example.com"); const response = await fetch("https://papyrio-pdf-tools.p.rapidapi.com/html-to-pdf", { method: "POST", headers: { "X-RapidAPI-Key": process.env.RAPIDAPI_KEY, "X-RapidAPI-Host": "papyrio-pdf-tools.p.rapidapi.com", }, body: form,}); if (!response.ok) { const { error } = await response.json(); throw new Error(`${error.code}: ${error.message}`);} await writeFile("output.pdf", Buffer.from(await response.arrayBuffer()));package main import ( "bytes" "io" "mime/multipart" "net/http" "os") func main() { var body bytes.Buffer writer := multipart.NewWriter(&body) writer.WriteField("url", "https://example.com") writer.Close() req, _ := http.NewRequest(http.MethodPost, "https://papyrio-pdf-tools.p.rapidapi.com/html-to-pdf", &body) req.Header.Set("Content-Type", writer.FormDataContentType()) req.Header.Set("X-RapidAPI-Key", os.Getenv("RAPIDAPI_KEY")) req.Header.Set("X-RapidAPI-Host", "papyrio-pdf-tools.p.rapidapi.com") resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() out, _ := os.Create("output.pdf") defer out.Close() io.Copy(out, resp.Body)} func addFile(w *multipart.Writer, field, path string) error { f, err := os.Open(path) if err != nil { return err } defer f.Close() part, err := w.CreateFormFile(field, path) if err != nil { return err } _, err = io.Copy(part, f) return err}Extract from PDF
Convert PDF to Word
/pdf-to-wordRebuilds the document structure as .docx. PDFs exported from Word, Google Docs or InDesign come through well. A scanned PDF is a picture of a page, so run /ocr-pdf on it first.
Body — multipart/form-data
filefilerequiredPDF file, 50 MB maximum.
Returns
Word document (.docx)
application/vnd.openxmlformats-officedocument.wordprocessingml.documentcurl -X POST "https://papyrio-pdf-tools.p.rapidapi.com/pdf-to-word" \ -H "X-RapidAPI-Key: $RAPIDAPI_KEY" \ -H "X-RapidAPI-Host: papyrio-pdf-tools.p.rapidapi.com" \ -F "file=@document.pdf" \ --output output.docximport osimport requests url = "https://papyrio-pdf-tools.p.rapidapi.com/pdf-to-word"headers = { "X-RapidAPI-Key": os.environ["RAPIDAPI_KEY"], "X-RapidAPI-Host": "papyrio-pdf-tools.p.rapidapi.com",} files = {"file": open("document.pdf", "rb")} response = requests.post(url, headers=headers, files=files, timeout=120)response.raise_for_status() with open("output.docx", "wb") as out: out.write(response.content)import { createReadStream } from "node:fs";import { writeFile } from "node:fs/promises"; const form = new FormData();form.append("file", await openAsFile("document.pdf")); const response = await fetch("https://papyrio-pdf-tools.p.rapidapi.com/pdf-to-word", { method: "POST", headers: { "X-RapidAPI-Key": process.env.RAPIDAPI_KEY, "X-RapidAPI-Host": "papyrio-pdf-tools.p.rapidapi.com", }, body: form,}); if (!response.ok) { const { error } = await response.json(); throw new Error(`${error.code}: ${error.message}`);} await writeFile("output.docx", Buffer.from(await response.arrayBuffer())); // Node 20+: wrap a path as a File the FormData can stream.async function openAsFile(path) { const { openAsBlob } = await import("node:fs"); return openAsBlob(path);}package main import ( "bytes" "io" "mime/multipart" "net/http" "os") func main() { var body bytes.Buffer writer := multipart.NewWriter(&body) if err := addFile(writer, "file", "document.pdf"); err != nil { panic(err) } writer.Close() req, _ := http.NewRequest(http.MethodPost, "https://papyrio-pdf-tools.p.rapidapi.com/pdf-to-word", &body) req.Header.Set("Content-Type", writer.FormDataContentType()) req.Header.Set("X-RapidAPI-Key", os.Getenv("RAPIDAPI_KEY")) req.Header.Set("X-RapidAPI-Host", "papyrio-pdf-tools.p.rapidapi.com") resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() out, _ := os.Create("output.docx") defer out.Close() io.Copy(out, resp.Body)} func addFile(w *multipart.Writer, field, path string) error { f, err := os.Open(path) if err != nil { return err } defer f.Close() part, err := w.CreateFormFile(field, path) if err != nil { return err } _, err = io.Copy(part, f) return err}Extract plain text from a PDF
/pdf-to-textReturns the text layer as UTF-8. Fast and exact when the PDF already carries selectable text. A scanned PDF has no text layer, so use /ocr-pdf instead.
Body — multipart/form-data
filefilerequiredPDF file, 50 MB maximum.
Returns
Extracted text
text/plaincurl -X POST "https://papyrio-pdf-tools.p.rapidapi.com/pdf-to-text" \ -H "X-RapidAPI-Key: $RAPIDAPI_KEY" \ -H "X-RapidAPI-Host: papyrio-pdf-tools.p.rapidapi.com" \ -F "file=@document.pdf" \ --output output.txtimport osimport requests url = "https://papyrio-pdf-tools.p.rapidapi.com/pdf-to-text"headers = { "X-RapidAPI-Key": os.environ["RAPIDAPI_KEY"], "X-RapidAPI-Host": "papyrio-pdf-tools.p.rapidapi.com",} files = {"file": open("document.pdf", "rb")} response = requests.post(url, headers=headers, files=files, timeout=120)response.raise_for_status() print(response.text)import { createReadStream } from "node:fs";import { writeFile } from "node:fs/promises"; const form = new FormData();form.append("file", await openAsFile("document.pdf")); const response = await fetch("https://papyrio-pdf-tools.p.rapidapi.com/pdf-to-text", { method: "POST", headers: { "X-RapidAPI-Key": process.env.RAPIDAPI_KEY, "X-RapidAPI-Host": "papyrio-pdf-tools.p.rapidapi.com", }, body: form,}); if (!response.ok) { const { error } = await response.json(); throw new Error(`${error.code}: ${error.message}`);} console.log(await response.text()); // Node 20+: wrap a path as a File the FormData can stream.async function openAsFile(path) { const { openAsBlob } = await import("node:fs"); return openAsBlob(path);}package main import ( "bytes" "io" "mime/multipart" "net/http" "os") func main() { var body bytes.Buffer writer := multipart.NewWriter(&body) if err := addFile(writer, "file", "document.pdf"); err != nil { panic(err) } writer.Close() req, _ := http.NewRequest(http.MethodPost, "https://papyrio-pdf-tools.p.rapidapi.com/pdf-to-text", &body) req.Header.Set("Content-Type", writer.FormDataContentType()) req.Header.Set("X-RapidAPI-Key", os.Getenv("RAPIDAPI_KEY")) req.Header.Set("X-RapidAPI-Host", "papyrio-pdf-tools.p.rapidapi.com") resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() out, _ := os.Create("output.txt") defer out.Close() io.Copy(out, resp.Body)} func addFile(w *multipart.Writer, field, path string) error { f, err := os.Open(path) if err != nil { return err } defer f.Close() part, err := w.CreateFormFile(field, path) if err != nil { return err } _, err = io.Copy(part, f) return err}Convert PDF pages to JPG images
/pdf-to-jpgRenders every page as a JPG and returns them as a ZIP archive.
Body — multipart/form-data
filefilerequiredPDF file, 50 MB maximum.
Returns
ZIP archive, one JPG per page
application/zipcurl -X POST "https://papyrio-pdf-tools.p.rapidapi.com/pdf-to-jpg" \ -H "X-RapidAPI-Key: $RAPIDAPI_KEY" \ -H "X-RapidAPI-Host: papyrio-pdf-tools.p.rapidapi.com" \ -F "file=@document.pdf" \ --output pages.zipimport osimport requests url = "https://papyrio-pdf-tools.p.rapidapi.com/pdf-to-jpg"headers = { "X-RapidAPI-Key": os.environ["RAPIDAPI_KEY"], "X-RapidAPI-Host": "papyrio-pdf-tools.p.rapidapi.com",} files = {"file": open("document.pdf", "rb")} response = requests.post(url, headers=headers, files=files, timeout=120)response.raise_for_status() with open("pages.zip", "wb") as out: out.write(response.content)import { createReadStream } from "node:fs";import { writeFile } from "node:fs/promises"; const form = new FormData();form.append("file", await openAsFile("document.pdf")); const response = await fetch("https://papyrio-pdf-tools.p.rapidapi.com/pdf-to-jpg", { method: "POST", headers: { "X-RapidAPI-Key": process.env.RAPIDAPI_KEY, "X-RapidAPI-Host": "papyrio-pdf-tools.p.rapidapi.com", }, body: form,}); if (!response.ok) { const { error } = await response.json(); throw new Error(`${error.code}: ${error.message}`);} await writeFile("pages.zip", Buffer.from(await response.arrayBuffer())); // Node 20+: wrap a path as a File the FormData can stream.async function openAsFile(path) { const { openAsBlob } = await import("node:fs"); return openAsBlob(path);}package main import ( "bytes" "io" "mime/multipart" "net/http" "os") func main() { var body bytes.Buffer writer := multipart.NewWriter(&body) if err := addFile(writer, "file", "document.pdf"); err != nil { panic(err) } writer.Close() req, _ := http.NewRequest(http.MethodPost, "https://papyrio-pdf-tools.p.rapidapi.com/pdf-to-jpg", &body) req.Header.Set("Content-Type", writer.FormDataContentType()) req.Header.Set("X-RapidAPI-Key", os.Getenv("RAPIDAPI_KEY")) req.Header.Set("X-RapidAPI-Host", "papyrio-pdf-tools.p.rapidapi.com") resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() out, _ := os.Create("pages.zip") defer out.Close() io.Copy(out, resp.Body)} func addFile(w *multipart.Writer, field, path string) error { f, err := os.Open(path) if err != nil { return err } defer f.Close() part, err := w.CreateFormFile(field, path) if err != nil { return err } _, err = io.Copy(part, f) return err}Make a scanned PDF searchable
/ocr-pdfRuns Tesseract over every page at 300 DPI and adds a text layer, which makes the document searchable and selectable. Scan quality drives the result: 300 DPI or higher reads well, below 150 DPI falls apart.
Body — multipart/form-data
filefilerequiredPDF file, 50 MB maximum.
Returns
Searchable PDF with a text layer
application/pdfcurl -X POST "https://papyrio-pdf-tools.p.rapidapi.com/ocr-pdf" \ -H "X-RapidAPI-Key: $RAPIDAPI_KEY" \ -H "X-RapidAPI-Host: papyrio-pdf-tools.p.rapidapi.com" \ -F "file=@scanned.pdf" \ --output output.pdfimport osimport requests url = "https://papyrio-pdf-tools.p.rapidapi.com/ocr-pdf"headers = { "X-RapidAPI-Key": os.environ["RAPIDAPI_KEY"], "X-RapidAPI-Host": "papyrio-pdf-tools.p.rapidapi.com",} files = {"file": open("scanned.pdf", "rb")} response = requests.post(url, headers=headers, files=files, timeout=120)response.raise_for_status() with open("output.pdf", "wb") as out: out.write(response.content)import { createReadStream } from "node:fs";import { writeFile } from "node:fs/promises"; const form = new FormData();form.append("file", await openAsFile("scanned.pdf")); const response = await fetch("https://papyrio-pdf-tools.p.rapidapi.com/ocr-pdf", { method: "POST", headers: { "X-RapidAPI-Key": process.env.RAPIDAPI_KEY, "X-RapidAPI-Host": "papyrio-pdf-tools.p.rapidapi.com", }, body: form,}); if (!response.ok) { const { error } = await response.json(); throw new Error(`${error.code}: ${error.message}`);} await writeFile("output.pdf", Buffer.from(await response.arrayBuffer())); // Node 20+: wrap a path as a File the FormData can stream.async function openAsFile(path) { const { openAsBlob } = await import("node:fs"); return openAsBlob(path);}package main import ( "bytes" "io" "mime/multipart" "net/http" "os") func main() { var body bytes.Buffer writer := multipart.NewWriter(&body) if err := addFile(writer, "file", "scanned.pdf"); err != nil { panic(err) } writer.Close() req, _ := http.NewRequest(http.MethodPost, "https://papyrio-pdf-tools.p.rapidapi.com/ocr-pdf", &body) req.Header.Set("Content-Type", writer.FormDataContentType()) req.Header.Set("X-RapidAPI-Key", os.Getenv("RAPIDAPI_KEY")) req.Header.Set("X-RapidAPI-Host", "papyrio-pdf-tools.p.rapidapi.com") resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() out, _ := os.Create("output.pdf") defer out.Close() io.Copy(out, resp.Body)} func addFile(w *multipart.Writer, field, path string) error { f, err := os.Open(path) if err != nil { return err } defer f.Close() part, err := w.CreateFormFile(field, path) if err != nil { return err } _, err = io.Copy(part, f) return err}Secure
Password-protect a PDF
/protect-pdfEncrypts the document with AES-256 using the password you supply.
Body — multipart/form-data
filefilerequiredPDF file, 50 MB maximum.
passwordstringrequiredPassword required to open the resulting PDF.
Returns
Encrypted PDF
application/pdfcurl -X POST "https://papyrio-pdf-tools.p.rapidapi.com/protect-pdf" \ -H "X-RapidAPI-Key: $RAPIDAPI_KEY" \ -H "X-RapidAPI-Host: papyrio-pdf-tools.p.rapidapi.com" \ -F "file=@document.pdf" \ -F "password=s3cret" \ --output output.pdfimport osimport requests url = "https://papyrio-pdf-tools.p.rapidapi.com/protect-pdf"headers = { "X-RapidAPI-Key": os.environ["RAPIDAPI_KEY"], "X-RapidAPI-Host": "papyrio-pdf-tools.p.rapidapi.com",} files = {"file": open("document.pdf", "rb")} data = {"password": "s3cret"} response = requests.post(url, headers=headers, files=files, data=data, timeout=120)response.raise_for_status() with open("output.pdf", "wb") as out: out.write(response.content)import { createReadStream } from "node:fs";import { writeFile } from "node:fs/promises"; const form = new FormData();form.append("file", await openAsFile("document.pdf"));form.append("password", "s3cret"); const response = await fetch("https://papyrio-pdf-tools.p.rapidapi.com/protect-pdf", { method: "POST", headers: { "X-RapidAPI-Key": process.env.RAPIDAPI_KEY, "X-RapidAPI-Host": "papyrio-pdf-tools.p.rapidapi.com", }, body: form,}); if (!response.ok) { const { error } = await response.json(); throw new Error(`${error.code}: ${error.message}`);} await writeFile("output.pdf", Buffer.from(await response.arrayBuffer())); // Node 20+: wrap a path as a File the FormData can stream.async function openAsFile(path) { const { openAsBlob } = await import("node:fs"); return openAsBlob(path);}package main import ( "bytes" "io" "mime/multipart" "net/http" "os") func main() { var body bytes.Buffer writer := multipart.NewWriter(&body) if err := addFile(writer, "file", "document.pdf"); err != nil { panic(err) } writer.WriteField("password", "s3cret") writer.Close() req, _ := http.NewRequest(http.MethodPost, "https://papyrio-pdf-tools.p.rapidapi.com/protect-pdf", &body) req.Header.Set("Content-Type", writer.FormDataContentType()) req.Header.Set("X-RapidAPI-Key", os.Getenv("RAPIDAPI_KEY")) req.Header.Set("X-RapidAPI-Host", "papyrio-pdf-tools.p.rapidapi.com") resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() out, _ := os.Create("output.pdf") defer out.Close() io.Copy(out, resp.Body)} func addFile(w *multipart.Writer, field, path string) error { f, err := os.Open(path) if err != nil { return err } defer f.Close() part, err := w.CreateFormFile(field, path) if err != nil { return err } _, err = io.Copy(part, f) return err}Remove a PDF password
/unlock-pdfDecrypts a password-protected PDF. You supply the correct password; this does not break encryption.
Body — multipart/form-data
filefilerequiredPDF file, 50 MB maximum.
passwordstringrequiredCurrent password of the PDF.
Returns
Decrypted PDF
application/pdfcurl -X POST "https://papyrio-pdf-tools.p.rapidapi.com/unlock-pdf" \ -H "X-RapidAPI-Key: $RAPIDAPI_KEY" \ -H "X-RapidAPI-Host: papyrio-pdf-tools.p.rapidapi.com" \ -F "file=@locked.pdf" \ -F "password=s3cret" \ --output output.pdfimport osimport requests url = "https://papyrio-pdf-tools.p.rapidapi.com/unlock-pdf"headers = { "X-RapidAPI-Key": os.environ["RAPIDAPI_KEY"], "X-RapidAPI-Host": "papyrio-pdf-tools.p.rapidapi.com",} files = {"file": open("locked.pdf", "rb")} data = {"password": "s3cret"} response = requests.post(url, headers=headers, files=files, data=data, timeout=120)response.raise_for_status() with open("output.pdf", "wb") as out: out.write(response.content)import { createReadStream } from "node:fs";import { writeFile } from "node:fs/promises"; const form = new FormData();form.append("file", await openAsFile("locked.pdf"));form.append("password", "s3cret"); const response = await fetch("https://papyrio-pdf-tools.p.rapidapi.com/unlock-pdf", { method: "POST", headers: { "X-RapidAPI-Key": process.env.RAPIDAPI_KEY, "X-RapidAPI-Host": "papyrio-pdf-tools.p.rapidapi.com", }, body: form,}); if (!response.ok) { const { error } = await response.json(); throw new Error(`${error.code}: ${error.message}`);} await writeFile("output.pdf", Buffer.from(await response.arrayBuffer())); // Node 20+: wrap a path as a File the FormData can stream.async function openAsFile(path) { const { openAsBlob } = await import("node:fs"); return openAsBlob(path);}package main import ( "bytes" "io" "mime/multipart" "net/http" "os") func main() { var body bytes.Buffer writer := multipart.NewWriter(&body) if err := addFile(writer, "file", "locked.pdf"); err != nil { panic(err) } writer.WriteField("password", "s3cret") writer.Close() req, _ := http.NewRequest(http.MethodPost, "https://papyrio-pdf-tools.p.rapidapi.com/unlock-pdf", &body) req.Header.Set("Content-Type", writer.FormDataContentType()) req.Header.Set("X-RapidAPI-Key", os.Getenv("RAPIDAPI_KEY")) req.Header.Set("X-RapidAPI-Host", "papyrio-pdf-tools.p.rapidapi.com") resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() out, _ := os.Create("output.pdf") defer out.Close() io.Copy(out, resp.Body)} func addFile(w *multipart.Writer, field, path string) error { f, err := os.Open(path) if err != nil { return err } defer f.Close() part, err := w.CreateFormFile(field, path) if err != nil { return err } _, err = io.Copy(part, f) return err}Health check
/healthReports API and rendering-engine status. We do not rate limit it, so point an uptime monitor at it.
{ "status": "ok", "gotenberg": "ok", "error": ""}Support
Write to hello@papyrio.net with questions, a file that will not convert, or a quota that does not fit your volume. Tell us the endpoint and the error code, and send the file if you can share it.
The same operations run free in a browser at papyrio.net/tools, with no account.