DEV Community

Cover image for Flattening a PDF Form Is Not the Same as Protecting It
PDF4me
PDF4me

Posted on

Flattening a PDF Form Is Not the Same as Protecting It

A finance team collects a signed intake form, flattens it so the fields stop looking like an editable form, and emails it out as the "final" version. Six months later someone asks why the flattened copy opened without a password, printed without restriction, and could still be pulled apart in any PDF editor that touches raw content streams. Nothing was ever protected. The team confused two operations that PDF4me ships as two entirely separate endpoints for a reason.

Flattening and protecting solve different problems. One changes what a document looks like. The other changes who can open it and what they're allowed to do once it's open. Treating the first as a substitute for the second shows up constantly in automated document pipelines built on Power Automate, Make, Zapier, and n8n, where "flatten" is often the last node before a file gets shipped.

What Flatten PDF actually does

The Flatten PDF REST endpoint (POST /api/v2/FlattenPdf) merges AcroForm fields, XFA form data, annotations, signatures, and comments into the static page content. A filled text field stops being a field and becomes plain rendered text. A checkbox becomes a small rendered mark instead of an interactive control. The output has no form left to interact with, so nothing can be refilled or resubmitted through a form-aware viewer.

The request body only needs two fields, per the endpoint's own parameter table: File Content (Base64) and File Name. Worth flagging honestly: the table names them File Content and File Name, but the request-example payload on the same docs page, and the official Python sample below, both use docContent and docName as the actual JSON keys, a naming mismatch this docs site carries on more than one endpoint. Match the payload example, not the table header.

import requests, base64

api_key = "YOUR_API_KEY"
with open("unflattened-sample.pdf", "rb") as f:
    pdf_base64 = base64.b64encode(f.read()).decode("utf-8")

payload = {
    "docContent": pdf_base64,
    "docName": "Flatten_output.pdf",
    "isAsync": True
}
headers = {"Authorization": f"Basic {api_key}", "Content-Type": "application/json"}

response = requests.post("https://api.pdf4me.com/api/v2/FlattenPdf", json=payload, headers=headers)
# 200 -> binary PDF in response.content
# 202 -> poll the Location header URL until it returns 200
Enter fullscreen mode Exit fullscreen mode

Note the field name is isAsync (lowercase i) in the real sample code, not IsAsync. That's the entire feature: no password field, no permission flag, no encryption step. Flatten PDF was never designed to restrict access, and its own documentation doesn't claim otherwise.

Where the confusion starts

The mix-up usually isn't accidental. It's inherited from a nearby field that does double duty. The Fill a PDF Form endpoint has its own KeepPdfEditable parameter, optional, defaulting to false. Leave it at the default and the filled PDF comes back with its form fields already flattened into static text, "useful for delivering a locked record of what was submitted," in the docs' own words. Set it to true and the recipient can reopen the same fields in any PDF viewer and change the values.

That single boolean does exactly what Flatten PDF does on its own, bundled as a convenience step at the end of a fill operation. It's easy to read "flattens the form into static text" and file that mentally under "now it's secure." It isn't. A flattened PDF with KeepPdfEditable: false is just as printable, copyable, and re-editable at the byte level (new text boxes, redaction bypass, image extraction) as any other unprotected file. It only stops the specific action of refilling the original form fields through form tools.

What actually restricts access

That job belongs to Protect Document (POST /api/v2/Protect). It takes a password and a pdfPermission value and returns a new AES-encrypted PDF. The two fields protect against different things: the password gates opening the file at all, while pdfPermission gates what a user can do once they're past the password. pdfPermission is allow-list, not deny-list, so setting it to None blocks printing, copying, editing, annotating, and form filling in one move, and setting it to Fill Forms opens exactly one door while every other action stays shut.

import requests, base64

api_key = "YOUR_API_KEY"
with open("sample.pdf", "rb") as f:
    pdf_base64 = base64.b64encode(f.read()).decode("utf-8")

payload = {
    "docName": "output.pdf",
    "docContent": pdf_base64,
    "password": "Str0ng-P@ss!",    "pdfPermission": "Fill Forms",
    "isAsync": True
}
headers = {"Authorization": f"Basic {api_key}", "Content-Type": "application/json"}

response = requests.post("https://api.pdf4me.com/api/v2/Protect", json=payload, headers=headers)
Enter fullscreen mode Exit fullscreen mode

The docs' own parameter table calls this field async, but the official sample code sends isAsync, the same lowercase-i naming this docs site uses on the Flatten endpoint too, so treat that as the working name rather than what the table prints.

It's telling that PDF4me's own Fill a PDF Form documentation names Protect Document as the natural next step, not an alternative: "apply password protection and permissions after filling," recommended specifically for filled forms going out over email. That's the sequence the platform itself points toward, and it's worth testing both endpoints interactively before wiring them into a pipeline, through the Flatten PDF API Tester and the Protect Document API Tester, to see the request and response shape before any code gets written. Authentication for either endpoint follows the same Connect to the PDF4me V2 API pattern: an API key in the Authorization header, nothing exotic.

The three-step pattern, not a one-step shortcut

Put together, a filled document that genuinely needs to leave the building locked down runs through three separate operations, each doing one job:

  1. Fill the form with the submitted data.
  2. Flatten it (or set KeepPdfEditable: false during fill) so the visual record can't be casually refilled.
  3. Protect it with a password and a pdfPermission value so it can't be opened, printed, or copied by anyone who shouldn't have it.

Skipping step three because step two already happened is the exact mistake this article opened with. Flattening is a formatting decision. Protecting is an access-control decision. A document can be flattened and wide open, or unflattened and locked down tight (Protect Document doesn't care whether the source PDF still has live form fields), and most real workflows want both, in that order, for different reasons.

The same gap exists in no-code workflows

None of this is unique to raw API calls. The same Flatten PDF operation is exposed as a dedicated module or node in Make, Power Automate, Zapier, and n8n, each one wrapping the identical REST call behind a visual drag-and-drop step. If a workflow builder drops a Flatten action at the end of a Fill Form scenario and calls it done, the resulting document has exactly the same access-control gap as the raw API version. No-code doesn't change what the endpoint does; it just makes it faster to skip the protection step by accident, since there's no error, no warning, and no failed request. The flatten succeeds every time, whether or not anything downstream actually needed it to.

The honest limit

None of this makes a flattened-and-protected PDF unbreakable. pdfPermission restrictions are enforced by compliant PDF readers, not by the file format itself, the same limitation every PDF permission system has always had. A password gates who can open the file; it does not gate what happens to the bytes after a determined reader ignores its own enforcement. For document distribution and archival, that's still a meaningfully higher bar than an unprotected flattened file sitting in an inbox, but it's worth knowing what the guarantee actually is before promising it to a compliance team.

The fix here isn't a new feature. It's reading past the name of the endpoint you're already calling, and asking what problem it was actually built to solve before assuming it solved a different one.

Website: pdf4me.com
Documentation: docs.pdf4me.com
Developer portal: dev.pdf4me.com

Top comments (0)