Endpoint reference
POST /api/protect
Encrypt a PDF. Set a password to open it, an owner password that lifts the restrictions, or both, and choose which actions stay allowed. Returns the encrypted PDF as application/pdf.
Request
Method: POST. URL: https://pdfops.dev/api/protect. Body: multipart/form-data.
| Field | Type | Required | Description |
|---|---|---|---|
pdf | File (PDF binary) | Yes | Exactly one PDF, up to 4 MB and 2,000 pages. The whole request must stay under 4.5 MB, Vercel's platform request limit. |
user_password | String | At least one of the two passwords | The password needed to open the output. 1 to 32 characters, each in the Latin-1 range (U+0020 to U+00FF). |
owner_password | String | At least one of the two passwords | The password that lifts the permission restrictions. 1 to 32 characters, each in the Latin-1 range (U+0020 to U+00FF). |
permissions | String | No | Comma list of what stays allowed, from print, copy, modify, annotate, fill-forms. Default: everything allowed, except modify when owner_password is set. An empty value allows none. |
password | String | No | Opens an input PDF that needs a user password. Not needed for owner-locked files with an empty user password. |
The optional X-API-Key header works as on /api/merge: no header is anonymous, 100 requests per IP per month; a valid key uses your tier's quota; an invalid or revoked key returns 401 invalid_api_key. A successful (2xx) call counts 1 request. Check consumption with GET /api/usage.
Response
On success: 200 OK, Content-Type: application/pdf, body is the encrypted PDF.
On any failure: 4xx or 500, Content-Type: application/json, body is { "error": "<code>", "details": "<explanation>" }.
Error codes
| Error code | Status | Trigger |
|---|---|---|
invalid_multipart | 400 | Request body could not be parsed as multipart/form-data. |
missing_pdf | 400 | The request does not have exactly one pdf file field. |
pdf_too_large | 400 | The pdf file is over 4 MB. |
invalid_pdf | 400 | The PDF could not be parsed (corrupt, or not a PDF). |
encrypted_pdf | 400 | The PDF needs a password we were not given. Owner-locked files with an empty user password (most government blanks) open on their own; for a real user password send it as the password form field. |
decompressed_too_large | 400 | Object streams inside the PDF decompress to over 32 MB. |
too_many_pages | 400 | The PDF has over 2,000 pages. |
missing_password | 400 | Neither user_password nor owner_password was sent. |
invalid_password | 400 | A password is longer than 32 characters or has a character outside U+0020 to U+00FF. The format stores 32 bytes, so a longer password would be cut silently and a different one would not open the file; it is rejected instead. details names the field. |
invalid_permissions | 400 | permissions has a name that is not one of the five. details names it. |
encrypt_failed | 500 | The PDF could not be encrypted (internal error). Retryable. |
save_failed | 500 | PDF serialization failed. Retryable. |
request_too_large | 413 | The whole request body is over 4.5 MB (Vercel's platform request limit). Rejected before anything is parsed. |
invalid_api_key | 401 | An X-API-Key header was sent but the key is unknown or revoked. No fallback to the anonymous IP cap. Get a fresh key at /pricing. |
rate_limited (anonymous) | 429 | No X-API-Key header and the per-IP limit (100/month) is exhausted. Retry-After gives seconds until next calendar month. |
rate_limited (keyed) | 429 | Your key's tier quota (Free 250 / Indie 4,000 / Pro 25,000 per month) is exhausted. Retry-After gives seconds until next calendar month. |
Example
curl -X POST https://pdfops.dev/api/protect \
-F "pdf=@statement.pdf" \
-F "user_password=open-sesame" \
-F "owner_password=admin-only" \
-F "permissions=print,fill-forms" \
-o protected.pdf \
-H "X-API-Key: pdfops_live_..." # optional, raises your quota to your tier
Keyless calls work: 100 requests per IP per month with no key.
Using this for real? A free API key raises the keyless 100 calls per IP to 250 requests a month, with no card.
Code examples
Have an API key? Add an X-API-Key header to any request. It is optional, and it raises your quota from 100/IP/month to your tier's.
JavaScript (Node.js 22+) — built-in fetch + FormData
import { readFile, writeFile } from 'node:fs/promises';
const fd = new FormData();
fd.append('pdf', new Blob([await readFile('statement.pdf')], { type: 'application/pdf' }), 'statement.pdf');
fd.append('user_password', 'open-sesame');
fd.append('owner_password', 'admin-only');
fd.append('permissions', 'print,fill-forms');
const res = await fetch('https://pdfops.dev/api/protect', {
method: 'POST',
headers: { 'X-API-Key': process.env.PDFOPS_API_KEY }, // optional
body: fd,
});
if (!res.ok) {
const err = await res.json();
throw new Error(`PDFops protect failed: ${err.error}: ${err.details}`);
}
await writeFile('protected.pdf', Buffer.from(await res.arrayBuffer()));
Behavioral notes
- Send at least one password. With only
owner_password, anyone can open the file and the restrictions apply. With onlyuser_password, opening needs the password. - Restrictions only bind a viewer when an owner password is set. Viewers honour them voluntarily, so treat them as a request, not a lock.
- The output is always AES-128 encrypted. The input's PDF header version does not matter: a PDF 1.3, 1.4 or 2.0 file is written as PDF 1.7 with AES-128, never the weak 40-bit or 128-bit RC4 ciphers that older headers would otherwise select. Passwords are limited to 32 Latin-1 characters because that is what the format can store.
- The output is not byte-deterministic. Encryption uses a random document ID and salts, so the same input gives different bytes each call. Dates and producer are still pinned.
- Limits: 4 MB per PDF, 2,000 pages, 4.5 MB per request (Vercel's platform limit).
FAQ
What is the difference between the two passwords?
The user password is needed to open the file. The owner password lifts the permission restrictions, such as printing or copying.
Can I protect a PDF that already has a password?
Yes. Send the current open password in password. The output is encrypted again with the new passwords.
Related: Remove restrictions · Add a text watermark · Read PDF properties.
Get a free API key. Keyless calls get 100 requests per IP per month; a free key raises that to 250, with no card. Enter your email and the key arrives in your inbox.