Flatten a PDF in Python: Turn Filled Forms Into Static PDFs
Blog / PDF Processing
PDF Processing

Flatten a PDF in Python: Turn Filled Forms Into Static PDFs

Demetra Theodorou · Oct 21, 2026

Turn a filled PDF form into a static document from Python with the SDK or requests, lock only chosen fields, generate and lock a form from HTML, and serve it from Flask. Tested code throughout.

To flatten a PDF is to merge its form into the page. The entered values stay where they are, the input fields are gone, and tools that open the file see nothing but content. That is the right state for a completed form that is about to be stored, printed, emailed or signed.

This article shows how to flatten a PDF in Python with the Flatten PDF API and the official Python SDK. It covers a whole document, a chosen set of fields, a form your own code fills from HTML, the same call with requests and no SDK, and a Flask route. Every sample on this page was run against the API before publishing.

If you have tried this with pypdf or PyMuPDF, you know the catch. pypdf can fill fields but cannot flatten them without a second library, and PyMuPDF can, but it carries a native dependency and an AGPL licence. With the API there is nothing to compile and no licence to check. Your code uploads, calls one method, and downloads the result.

Key takeaways

  • A flattened PDF keeps what was typed and loses the fields it was typed into. Readers get a plain document.
  • In Python it is three calls: upload_file, flatten_pdf, get_file. Nothing native to compile and no AGPL licence to check.
  • field_names limits the operation to the fields you name and leaves the others open.
  • One requests.post with the file attached does upload and flatten together, no SDK needed.

What flattening does

  • Every text field, checkbox, radio button, dropdown and text area is written into the page content together with its value.
  • The form structure is deleted. In pypdf, PdfReader(path).get_fields() on the result is None.
  • The output is a fresh document with its own ID, and the input is left alone, so you keep both the fillable and the locked file.
  • With field_names only the named fields are flattened. The others stay interactive, which lets you lock a customer ID while a signature line remains open.

A password does not remove a form, it only restricts access to it. For a file that is both locked and encrypted, flatten it first and send the result to the Protect PDF API.

Install the Python SDK

The pdfgate package on PyPI needs Python 3.9 or newer and depends only on httpx.

1pip install pdfgate

Keys come from the dashboard. The SDK routes test_ keys to the sandbox and live_ keys to production on its own. The listings take the key from os.environ.

Flatten a PDF file

Three calls handle an existing file. upload_file stores it and returns an ID, flatten_pdf produces a new document from that ID, and get_file downloads the new document as bytes.

1import os23from pdfgate import (4    FileParam,5    FlattenPDFParams,6    GeneratePDFParams,7    GetFileParams,8    PDFGate,9    UploadFileParams,10)1112client = PDFGate(os.environ["PDFGATE_API_KEY"])1314# 1. Upload the PDF you want to flatten15with open("form.pdf", "rb") as f:16    uploaded = client.upload_file(17        UploadFileParams(file=FileParam.pdf(name="form.pdf", data=f.read()))18    )1920# 2. Flatten every form field21flattened = client.flatten_pdf(FlattenPDFParams(document_id=uploaded["id"]))2223# 3. Download the flattened PDF24pdf = client.get_file(GetFileParams(document_id=flattened["id"]))25with open("flattened.pdf", "wb") as f:26    f.write(pdf)2728print(f"Flattened {flattened['id']} ({flattened['size']} bytes)")
  • Parameters are dataclasses, so editors autocomplete them and a misspelled field is a TypeError before any request goes out.
  • upload_file wants a FileParam. FileParam.pdf() fills in the MIME type, and the name only labels the upload.
  • Results are dictionaries. flattened["id"] is the new document, flattened["size"] its length, flattened["derived_from"] the upload's ID.
  • get_file returns bytes, which is fine for forms that are rarely larger than a few megabytes.
  • Problems raise pdfgate.errors.PDFGateError. One except around the three calls is enough for logging or a retry.

With the two-page form from the fillable fields benchmark as input, the field count drops from 12 to 0 and all values remain visible.

Flatten only some fields

Pass a list as field_names to flatten part of a form. The named fields are written into the page and removed, everything else keeps working. The listing locks three of twelve fields.

1partial = client.flatten_pdf(2    FlattenPDFParams(3        document_id=uploaded["id"],4        field_names=["full_name", "customer_id", "start_date"],5    )6)7with open("partially-flattened.pdf", "wb") as f:8    f.write(client.get_file(GetFileParams(document_id=partial["id"])))910print(f"Partially flattened {partial['id']}")
  • The names are the PDF's field names, usually the inputs' name attributes. extract_pdf_form_data returns a dictionary keyed by them.
  • If one of the names does not exist the API returns a 422 listing the missing ones and leaves the document unchanged.
  • A signature field nobody signed has nothing to render and is removed without a trace.

Fill a form from HTML, then flatten

In a Python app the form is frequently generated rather than received, an order confirmation from a Jinja template with the customer's data already in it. There is nothing to fill in that case. Render the HTML with the values in the inputs, pass enable_form_fields=True so they become real fields, and flatten the generated document to lock it.

1html = """2<h1>Order confirmation</h1>3<label>Customer <input type="text" name="customer" value="Acme GmbH"></label>4<label>Seats <input type="number" name="seats" value="25"></label>5<label>Plan6  <select name="plan">7    <option>Starter</option>8    <option selected>Business</option>9  </select>10</label>11<label><input type="checkbox" name="accept_terms" checked> Terms accepted</label>12"""1314generated = client.generate_pdf(GeneratePDFParams(html=html, enable_form_fields=True))15locked = client.flatten_pdf(FlattenPDFParams(document_id=generated["id"]))16with open("order-confirmation.pdf", "wb") as f:17    f.write(client.get_file(GetFileParams(document_id=locked["id"])))1819print(f"Generated {generated['id']} and flattened it as {locked['id']}")

Field values come from the value, selected and checked attributes, and flattening renders them as fixed text and a ticked box. If the document should stay fillable for a recipient, stop before the flatten call. The fillable fields guide lists the supported inputs and how to style them.

Without the SDK (requests)

All of this goes through POST /forms/flatten. The endpoint accepts the PDF as a multipart file, so with requests the upload and the flatten collapse into one call whose response body is the flattened PDF.

1import os23import requests45api_key = os.environ["PDFGATE_API_KEY"]67with open("form.pdf", "rb") as f:8    response = requests.post(9        "https://api.pdfgate.com/forms/flatten",10        headers={"Authorization": f"Bearer {api_key}"},11        files={"file": ("form.pdf", f, "application/pdf")},12        # Optional: flatten only these fields13        data={"fieldNames": '["full_name", "email"]'},14    )1516if response.status_code != 201:17    print(response.json())18    raise SystemExit(1)1920with open("flattened-http.pdf", "wb") as f:21    f.write(response.content)2223print("Saved flattened-http.pdf")
  • Use file or documentId, not both. With a document ID, json= replaces files=.
  • fieldNames is optional and, because form fields cannot hold a list, is sent as a JSON array in a string.
  • A 201 carries the PDF in the body. jsonResponse true returns the document record, and preSignedUrlExpiresIn adds a temporary link to it.
  • test_ keys go to api-sandbox.pdfgate.com. The SDK knows that, requests does not.

In a Flask route

In a web application the flattened PDF usually goes straight back to the caller. Create one PDFGate client at module level and return the bytes from get_file through send_file. In Django the same three lines go in a view that returns HttpResponse(pdf, content_type="application/pdf"). In FastAPI, wrap the bytes in a Response with the same media type.

1import io2import os34from flask import Flask, send_file5from pdfgate import FlattenPDFParams, GetFileParams, PDFGate67app = Flask(__name__)8client = PDFGate(os.environ["PDFGATE_API_KEY"])91011@app.post("/documents/<document_id>/flatten")12def flatten(document_id):13    flattened = client.flatten_pdf(FlattenPDFParams(document_id=document_id))14    pdf = client.get_file(GetFileParams(document_id=flattened["id"]))15    return send_file(16        io.BytesIO(pdf),17        mimetype="application/pdf",18        download_name="flattened.pdf",19    )

Every method has an async twin with an _async suffix, so in FastAPI or another async framework you can await client.flatten_pdf_async(...) and client.get_file_async(...) and keep the event loop free.

Notes and troubleshooting

  • Blank fields after flattening. Flattening draws stored appearances. pypdf does not regenerate them unless asked with auto_regenerate, so there may be nothing to draw. Fill with a tool that updates appearances, such as the free fill PDF form tool, or generate the PDF from HTML as shown.
  • Signed files. Removing fields invalidates a digital signature. Flatten first, sign second. Documents completed via the Digital Signature API are already flattened on return.
  • Quota and size. A single upload can be as large as 500 MB. Only the flatten call is billed against the plan, not the upload that precedes it.
  • Sandbox output carries a PDFGate watermark and is otherwise the same as production.
  • Pre-signed URLs require file storage to be enabled on the account, or the request is rejected with 422.

Parameters and the response format are in the Flatten PDF API reference, the rest of the client in the Python SDK documentation. The same walkthrough exists for C# and Java.

Frequently asked questions

Can I flatten a PDF in Python without pypdf or PyMuPDF?

Yes. The PDFGate Flatten PDF endpoint accepts the PDF as a multipart upload and returns the flattened PDF in the response body, so one requests.post call is enough. The pdfgate package on PyPI wraps the same endpoint in three calls.

Why are my values missing after flattening a form filled with pypdf?

pypdf writes the value but does not regenerate the field's appearance unless you ask it to, so there is nothing to draw. Fill forms with a tool that updates appearances, or generate the PDF from HTML with the values already in the inputs and flatten that.

Can I flatten only some fields and keep the rest editable?

Yes. Pass the names in field_names. Those fields become static content and the rest of the form stays interactive. If a name does not exist in the document the API returns a 422 that lists the missing names.

Does the SDK work with async frameworks like FastAPI?

Yes. Every method has an async twin with an _async suffix, for example flatten_pdf_async and get_file_async, so in FastAPI you can await them directly instead of running the sync calls in a thread.

The other processing endpoints work the same way. Start from the Flatten PDF API page.

This website uses cookies to offer a better browsing experience. By clicking on the "Accept" button you accept our Cookie policy.