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.

FieldTypeRequiredDescription
pdfFile (PDF binary)YesExactly 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_passwordStringAt least one of the two passwordsThe password needed to open the output. 1 to 32 characters, each in the Latin-1 range (U+0020 to U+00FF).
owner_passwordStringAt least one of the two passwordsThe password that lifts the permission restrictions. 1 to 32 characters, each in the Latin-1 range (U+0020 to U+00FF).
permissionsStringNoComma 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.
passwordStringNoOpens 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 codeStatusTrigger
invalid_multipart400Request body could not be parsed as multipart/form-data.
missing_pdf400The request does not have exactly one pdf file field.
pdf_too_large400The pdf file is over 4 MB.
invalid_pdf400The PDF could not be parsed (corrupt, or not a PDF).
encrypted_pdf400The 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_large400Object streams inside the PDF decompress to over 32 MB.
too_many_pages400The PDF has over 2,000 pages.
missing_password400Neither user_password nor owner_password was sent.
invalid_password400A 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_permissions400permissions has a name that is not one of the five. details names it.
encrypt_failed500The PDF could not be encrypted (internal error). Retryable.
save_failed500PDF serialization failed. Retryable.
request_too_large413The whole request body is over 4.5 MB (Vercel's platform request limit). Rejected before anything is parsed.
invalid_api_key401An 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)429No X-API-Key header and the per-IP limit (100/month) is exhausted. Retry-After gives seconds until next calendar month.
rate_limited (keyed)429Your 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.

Get a free API key

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

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.

Get a free API key