Skip to content

How to restrict what a reader can do with a PDF

Declare a security block in the template to encrypt every PDF rendered from it and to state what a reader may do with the document. Conforming PDF readers honour the restrictions — they prevent modification, copying or printing in those readers. Restrictions are not proof of who produced the document.

Draft renders are never protected

A draft is never encrypted or restricted, whatever the template's security block declares. Do not rely on a draft to check the protection — see Preview without a password.

Add the security block

Place <meta> as a direct child of <press>, beside <data>, <styles> and <assets>. Read the owner password from data, so that each render supplies its own, and state all seven permissions:

xml
<press>
  <meta>
    <pdf>
      <security>
        <owner-password>{{ data.ownerPassword }}</owner-password>
        <permissions>
          <printing>high-resolution</printing>
          <modifying>false</modifying>
          <copying>false</copying>
          <annotating>false</annotating>
          <filling-forms>false</filling-forms>
          <content-accessibility>true</content-accessibility>
          <document-assembly>false</document-assembly>
        </permissions>
      </security>
    </pdf>
  </meta>

  <data type="json">
  {
    "customer": "Acme Co",
    "invoiceNumber": "INV-1042",
    "ownerPassword": "example-not-for-production"
  }
  </data>

  <document format="A4" page-margin="2cm">
    <page>
      <h1>Invoice {{ data.invoiceNumber }}</h1>
      <p>Billed to {{ data.customer }}</p>
    </page>
  </document>
</press>

The owner password is the key that lifts the restrictions. Leave <owner-password> out and Papermill generates a fresh one for each render and never returns it, so nobody can lift them. Leave <permissions> out to encrypt the document without restricting anything.

Always drive the password from data rather than writing it into the template, so each render supplies its own and no secret is stored in the template. Write it as a single data reference, or build one from fixed text and a reference — the value below resolves to papermill- followed by the customer id. Every reference must resolve to a non-empty value, so a missing field is rejected rather than silently shortening the password.

xml
<owner-password>papermill-{{ data.customerId }}</owner-password>

Write every setting as an element. An attribute on any element in the block is rejected, show-if included. A permission's value may be a literal or a single data reference, so a permission can vary per render like a password — read it from data and send the value in each payload:

xml
<copying>{{ data.allowCopy }}</copying>

The template usually carries the security block. A payload may carry its own <meta><pdf><security> too: each setting it declares — <owner-password>, <user-password> or <permissions> — overrides the template's, and any it leaves out falls back to the template's. So a payload can set a per-render password without restating the template's permissions.

Add <user-password> to require a password to open the document at all — a reader must type it before the PDF displays. Read it from data like the owner password, and set an owner password too so the two differ. With no <user-password>, the document opens with no prompt and the permissions still bind through the owner password.

Supply the password in the payload

Send the password as a JSON string in every production render:

shell
curl -X POST "https://api.papermill.io/v2/pdf?template=acme-invoice" \
  -H "Authorization: Bearer $PAPERMILL_API_KEY" \
  -H "Content-Type: application/json" \
  -o invoice.pdf \
  -d '{"customer": "Acme Co", "invoiceNumber": "INV-1042", "ownerPassword": "9f3k-Lm2q-Xt8v"}'

Quote the value. An unquoted whole number is accepted and used as its digits, but a decimal, or a number too long to write out exactly, is rejected. A password holds at most 127 bytes; keep to letters, digits and punctuation, since a PDF cannot store every character.

Payload data merges over the template's defaults, so a payload that leaves ownerPassword out renders under the template's placeholder. To reject such a payload instead, require the field in a <schema>:

xml
<schema type="json">
{
  "type": "object",
  "required": ["ownerPassword"],
  "errorMessage": "ownerPassword must be sent on every render",
  "properties": {
    "ownerPassword": { "type": "string" }
  }
}
</schema>

Preview without a password

A draft render is never encrypted or restricted, whatever the template's security block declares. The editor preview, the template thumbnails, and any API call with draft=true are draft renders: each carries a watermark, opens with no prompt, and grants every operation. Only a production render will encrypt the document and apply its permissions.

Confirm the restrictions on a production render, not on the preview. Any PDF reader that shows document properties reports the encryption and the permissions.

A draft still checks the block itself. A misspelled element, an incomplete <permissions>, or a permission literal outside its set is reported on the preview rather than on the first production render; only the values read from data — a password, or a permission written as a reference — go unread.

Validation does read the password. The editor validates the template as it stands, and POST /v2/validate resolves the owner-password reference as a production render would, so keep a placeholder in the template's default <data> — as in the example above — or the editor reports Password <owner-password> resolved to nothing, which would leave the document unprotected on every edit.

What each permission grants

State all seven. Each one names an operation a conforming reader allows when it is true, and refuses when it is false.

PermissionValuesGrants
<printing>none, low-resolution, high-resolutionPrinting, and at what quality
<modifying>true, falseChanging the document's content
<copying>true, falseCopying or extracting text and images
<annotating>true, falseAdding or editing comments and other annotations
<filling-forms>true, falseFilling in form fields
<content-accessibility>trueExtracting content for assistive technology such as a screen reader
<document-assembly>true, falseInserting, rotating or deleting pages, and creating bookmarks

State <content-accessibility> as true. Papermill rejects false, because the PDF 2.0 standard deprecates denying assistive-technology access.

What Papermill rejects

Every rejection names the element that caused it.

  • A <security> with nothing inside it — Papermill rejects the document with message <security> declares no settings — add <owner-password> or <permissions>, or remove the block.
  • A <permissions> that leaves any permission out — Papermill rejects the document with message <permissions> must state every permission, because anything left out is denied — add <filling-forms>, <content-accessibility>, naming the ones missing.
  • A permission whose value — a literal, or one resolved from data — is outside its set — Papermill rejects the document with message <printing> must be none, low-resolution, high-resolution, but found 'yes' or Permission <copying> must be true or false, but found 'no'.
  • A permission mixing text with a reference, or referencing anything but data — Papermill rejects the document with message Permission <copying> contains {{, so it must be a single reference like {{ data.value }}.
  • <content-accessibility>false</content-accessibility> — Papermill rejects the document with message Papermill always grants <content-accessibility> so a reader can extract text for assistive technology — the PDF 2.0 standard deprecates denying it, so set it to true.
  • An attribute anywhere in the block — Papermill rejects the document with message Attribute 'show-if' is not supported on <copying> — write every PDF security setting as an element.
  • A misspelled element — Papermill rejects the document with message Unrecognized <pdf> section <securty> — did you mean <security>?, Unrecognized <security> setting <owner-passwrod> — did you mean <owner-password>? or Unrecognized permission <printng> — did you mean <printing>?.
  • A password with a malformed reference — a {{ }} that is not a single data. reference, or an unmatched brace — Papermill rejects the document with message Password <owner-password> has a {{ that is not a single data reference — write each as {{ data.field }}, with any fixed text around it.
  • A password whose field is missing, null or empty — Papermill rejects the document with message Password <owner-password> resolved to nothing, which would leave the document unprotected.
  • A password that is not text — Papermill rejects the document with message Password <owner-password> must resolve to text, but found boolean, or, for a number that cannot be written out exactly, Password <owner-password> resolved to a number that cannot be written out exactly — quote it in your data.
  • A password over 127 bytes — Papermill rejects the document with message Password <owner-password> is longer than the 127 bytes a PDF can hold.
  • A password holding a character a PDF cannot keep — Papermill rejects the document with message Password <owner-password> contains the character U+00AD, which a PDF strips or rewrites or Password <owner-password> contains the character U+0009, which a PDF cannot store. An accented password is accepted whether typed composed or decomposed.