PDF standards, passwords and signatures
Give your PDFs a title, make them PDF/A or PDF/UA with bookmarks, embed e-invoice XML, and protect them with a password or a digital signature.
Before you start
Section titled “Before you start”- An editable template in Studio. See build a template.
- curl or a client library for the API examples, set up as in the quickstart.
For what the standards require, see PDF/A, PDF/UA and Factur-X.
Set the title and other properties
Section titled “Set the title and other properties”In Studio, open the template and click PDF properties (the sliders button) in the preview
toolbar. Fill in Title, Author and the other fields under Document. Without a
Title, the PDF takes the template’s name. Studio writes the fields into the template’s
page.json:
{ "metadata": { "title": "Invoice {{invoice.number}}", "author": "Example Ltd" }}{{invoice.number}} is filled from each render’s data. To change a property for one render,
send metadata with the request:
curl -X POST "$PDF_URL/v1/renders" \ -H "x-api-key: $API_KEY" \ -H "content-type: application/json" \ -d '{ "templateVersionId": "0b7d5c1e-8f3a-4d2b-9e61-5a4c3b2a1f00", "data": { "invoice": { "number": "2026-0042" } }, "metadata": { "author": "Example Billing" } }'using Typefold.Client;
var client = new TypefoldClient( new Uri(Environment.GetEnvironmentVariable("PDF_URL")!), Environment.GetEnvironmentVariable("API_KEY")!);
byte[] pdf = await client.RenderAsync(new TemplateVersionRenderRequest{ TemplateVersionId = "0b7d5c1e-8f3a-4d2b-9e61-5a4c3b2a1f00", Data = new { invoice = new { number = "2026-0042" } }, Metadata = new PdfMetadata { Author = "Example Billing" },});
await File.WriteAllBytesAsync("invoice.pdf", pdf);import { writeFile } from "node:fs/promises";import { TypefoldClient } from "@typefold/client";
const client = new TypefoldClient({ baseUrl: process.env.PDF_URL!, apiKey: process.env.API_KEY!,});
const pdf = await client.render({ templateVersionId: "0b7d5c1e-8f3a-4d2b-9e61-5a4c3b2a1f00", data: { invoice: { number: "2026-0042" } }, metadata: { author: "Example Billing" },});
await writeFile("invoice.pdf", pdf);import { writeFile } from "node:fs/promises";import { TypefoldClient } from "@typefold/client";
const client = new TypefoldClient({ baseUrl: process.env.PDF_URL, apiKey: process.env.API_KEY,});
const pdf = await client.render({ templateVersionId: "0b7d5c1e-8f3a-4d2b-9e61-5a4c3b2a1f00", data: { invoice: { number: "2026-0042" } }, metadata: { author: "Example Billing" },});
await writeFile("invoice.pdf", pdf);import osfrom pathlib import Path
from typefold import PdfMetadata, TypefoldClient, TemplateVersionRenderRequest
client = TypefoldClient(os.environ["PDF_URL"], os.environ["API_KEY"])
pdf = client.render( TemplateVersionRenderRequest( template_version_id="0b7d5c1e-8f3a-4d2b-9e61-5a4c3b2a1f00", data={"invoice": {"number": "2026-0042"}}, metadata=PdfMetadata(author="Example Billing"), ))
Path("invoice.pdf").write_bytes(pdf)The curl call creates a render. Once it has succeeded,
download the PDF from its result.downloadUrl. The clients
wait and return the file.
The properties are title, author, subject, creator, producer (default typefold),
language (default: the render’s language) and keywords. A value sent with a request replaces
the template’s value for that property. An empty string removes a property (producer and
language go back to their defaults), and "keywords": [] removes the keywords. Only the
template’s values fill {{path}} placeholders; request values are used as written.
Check which standards a document meets
Section titled “Check which standards a document meets”In the preview toolbar, click the shield button (Check accessibility and conformance). Studio lists which PDF/A levels and PDF/UA parts the document meets and what to change for the others. It also lists accessibility problems in the HTML and the order a screen reader reads the PDF.
With the API, add "checkConformance": true to a template preview
(POST /v1/templates/{id}/preview). The pdf-conformance response header lists each standard
with conforms and, when it does not, the issues to fix. A preview render on POST /v1/renders
returns the same list in result.conformance. See the API reference.
Make it PDF/A
Section titled “Make it PDF/A”In PDF properties, under Standards, pick a level for PDF/A: PDF/A-2b for
archiving, PDF/A-3b for e-invoices. Studio writes it into page.json:
{ "pdfa": "2b" }Save the template and click Publish. Every render of the new version is now a PDF/A file at the level you picked.
The levels are 2b, 2u, 2a, 3b, 3u, 3a, 4 and 4f. To pick a level for one render,
set pdf.pdfa in a Reports API request. A request cannot turn off the
template’s level.
Make it PDF/UA
Section titled “Make it PDF/UA”A PDF/UA document needs three things in the template:
- a title (the template’s name, unless you set Title),
- at least one heading (
h1toh6), - alternative text on every image:
<img src="logo.png" alt="Example Ltd logo">.
Then, in PDF properties under Standards, set PDF/UA to PDF/UA-1. page.json now
has both settings:
{ "pdfa": "2b", "pdfua": "1" }Save the template and publish it. PDF/UA-1 pairs with the PDF/A-2 and PDF/A-3 levels, and PDF/UA-2
("pdfua": "2") with 4 and 4f.
Bookmarks
Section titled “Bookmarks”A PDF/UA document gets bookmarks (the PDF outline) from its headings: one per visible h1 to
h6, titled with the heading’s text and nested by level. PDF/UA is the only way to get them: a
plain or PDF/A-only document has none. Pages that other templates
add before or after bring bookmarks for their own headings. A PDF/A or PDF/UA document cannot add
an uploaded PDF file.
Embed e-invoice XML
Section titled “Embed e-invoice XML”Set the invoice template to PDF/A-3b, save it and publish it. Then send the XML file
factur-x.xml with each render as an attachment:
This example uses jq to put the file into the JSON body:
jq -n --rawfile xml factur-x.xml '{ templateVersionId: "0b7d5c1e-8f3a-4d2b-9e61-5a4c3b2a1f00", data: { invoice: { number: "2026-0042", total: 1190.00 } }, waitMs: 10000, attachments: [ { name: "factur-x.xml", content: $xml, mimeType: "text/xml", description: "Factur-X invoice", relationship: "alternative" } ]}' | curl -X POST "$PDF_URL/v1/renders" \ -H "x-api-key: $API_KEY" \ -H "content-type: application/json" \ --data-binary @-using Typefold.Client;
var client = new TypefoldClient( new Uri(Environment.GetEnvironmentVariable("PDF_URL")!), Environment.GetEnvironmentVariable("API_KEY")!);
byte[] pdf = await client.RenderAsync(new TemplateVersionRenderRequest{ TemplateVersionId = "0b7d5c1e-8f3a-4d2b-9e61-5a4c3b2a1f00", Data = new { invoice = new { number = "2026-0042", total = 1190.00 } }, Attachments = new[] { new PdfAttachment { Name = "factur-x.xml", Content = await File.ReadAllTextAsync("factur-x.xml"), MimeType = "text/xml", Description = "Factur-X invoice", Relationship = "alternative", }, },});
await File.WriteAllBytesAsync("invoice.pdf", pdf);import { readFile, writeFile } from "node:fs/promises";import { TypefoldClient } from "@typefold/client";
const client = new TypefoldClient({ baseUrl: process.env.PDF_URL!, apiKey: process.env.API_KEY!,});
const pdf = await client.render({ templateVersionId: "0b7d5c1e-8f3a-4d2b-9e61-5a4c3b2a1f00", data: { invoice: { number: "2026-0042", total: 1190.0 } }, attachments: [ { name: "factur-x.xml", content: await readFile("factur-x.xml", "utf8"), mimeType: "text/xml", description: "Factur-X invoice", relationship: "alternative", }, ],});
await writeFile("invoice.pdf", pdf);import { readFile, writeFile } from "node:fs/promises";import { TypefoldClient } from "@typefold/client";
const client = new TypefoldClient({ baseUrl: process.env.PDF_URL, apiKey: process.env.API_KEY,});
const pdf = await client.render({ templateVersionId: "0b7d5c1e-8f3a-4d2b-9e61-5a4c3b2a1f00", data: { invoice: { number: "2026-0042", total: 1190.0 } }, attachments: [ { name: "factur-x.xml", content: await readFile("factur-x.xml", "utf8"), mimeType: "text/xml", description: "Factur-X invoice", relationship: "alternative", }, ],});
await writeFile("invoice.pdf", pdf);import osfrom pathlib import Path
from typefold import PdfAttachment, TypefoldClient, TemplateVersionRenderRequest
client = TypefoldClient(os.environ["PDF_URL"], os.environ["API_KEY"])
pdf = client.render( TemplateVersionRenderRequest( template_version_id="0b7d5c1e-8f3a-4d2b-9e61-5a4c3b2a1f00", data={"invoice": {"number": "2026-0042", "total": 1190.00}}, attachments=[ PdfAttachment( name="factur-x.xml", content=Path("factur-x.xml").read_text(encoding="utf-8"), mime_type="text/xml", description="Factur-X invoice", relationship="alternative", ) ], ))
Path("invoice.pdf").write_bytes(pdf)- A render can embed up to 10 files, 5 MiB in total. Send text as
contentor bytes ascontentBase64. - In a PDF/A file, every attachment needs
mimeTypeanddescription. Only the levels3b,3u,3aand4fcan embed files.
Factur-X also expects its XMP extension schema. Add it to the template’s page.json, with the
Version and ConformanceLevel of your XML’s profile:
{ "metadata": { "xmp": [ { "name": "Factur-X PDF/A Extension Schema", "prefix": "fx", "namespace": "urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0#", "properties": [ { "name": "DocumentType", "value": "INVOICE", "description": "Document type" }, { "name": "DocumentFileName", "value": "factur-x.xml", "description": "Attached XML file" }, { "name": "Version", "value": "1.0", "description": "Factur-X version" }, { "name": "ConformanceLevel", "value": "EN 16931", "description": "Factur-X profile" } ] } ] }}Passwords and permissions
Section titled “Passwords and permissions”To make readers ask for a password before they open a PDF, or to stop them printing or copying it:
-
Open the template in Studio, click PDF properties and find Security.
-
Enter an Open password.
{{customer.number}}uses each customer’s number from the render’s data. -
Optionally enter an Owner password, which lifts the restrictions, and choose what readers may do under Printing and Readers may.
Studio writes the settings into
page.json:{"encryption": {"openPassword": "{{customer.number}}","permissions": { "copying": false }}} -
Save the template and click Publish. A rendered PDF now asks for the password.
- The permissions are
printing(highResolution,lowResolutionornone),copying,modifying,annotating,fillingForms,documentAssemblyandcontentAccessibility. Each one you leave out is allowed. - Without an owner password, nobody can lift the restrictions.
- A password holds at most 127 bytes.
- PDF/A does not allow passwords or restrictions, and PDF/UA needs Screen readers (PDF/UA requires it) ticked under Readers may.
- A request’s
encryptionchanges the template’s settings for one render.
Digital signatures
Section titled “Digital signatures”Upload a signing certificate once, then pick it in each template that signs.
-
In Studio, open Manage → Signing certificates and click Upload certificate. Enter a Name, such as
Company seal, choose the Certificate file (.p12or.pfx) and enter its File password. -
Open the template, click PDF properties and, under Signature, choose the certificate as Certificate. Optionally fill in Reason, Location and Contact. Studio writes them into
page.json:{"signature": {"certificate": "Company seal","reason": "Invoice {{invoice.number}}"}}The PDF is signed invisibly. To show the signature on a page, turn on Visible signature field and set its Area, then draw the signature block (a stamp or a name) in that area of the template.
-
Save the template and click Publish. PDF readers now show the signature and the signer’s name.
- A signed PDF cannot have a password or be PDF/UA.
- A request’s
signaturechanges the template’s settings for one render. Itsfieldplaces a visible signature:page, thenxMm,yMm,widthMmandheightMmfrom the page’s top-left corner.
Manage signing certificates
Section titled “Manage signing certificates”Everyone in the workspace sees the list under Manage → Signing certificates, with each certificate’s Signer, Issued by and Valid until. Only owners and admins can upload or delete one; see roles.
- Templates find a certificate by its name, ignoring case, and names are unique in a workspace. A render that names a certificate the workspace does not have fails.
- The file holds one RSA or EC private key with its certificate chain, at most 64 KiB. An expired certificate is refused at upload, and the key can never be read back.
- Expiry: once Valid until has passed, the list marks the certificate Expired and renders that sign with it fail.
- Renewal: upload the new file under a new name, choose it under Signature and publish. Or delete the old certificate and upload the new file under the same name: templates keep working without a new version, but renders that sign fail in between.
- Deleting a certificate cannot be undone. Templates that sign with it fail to render until a certificate with the same name is uploaded.
- Environments: certificates are not copied by an environment import. Upload one with the same name in each environment.
- A workspace holds up to 50 certificates. To manage them with the API, use
/v1/signing-certificateswith a key that has thecertificate:managescope (listing needs onlytemplate:read); see the API reference.
Test renders
Section titled “Test renders”A test render keeps every setting on this page. Its “TEST” watermark is marked as decoration, not content, and passwords and signatures work as usual. Don’t archive test PDFs.
If a render fails
Section titled “If a render fails”A document that cannot meet the standard fails, and the error message lists what to change. Fix
the template, or turn the standard off, and render again. A setting that cannot work together,
such as a password on a PDF/A file, answers 400 BAD_REQUEST at once. See
errors.
Next steps
Section titled “Next steps”- API reference: every field of these settings.
- Reports API and Renders API: the request forms.
- Combine templates: a cover, terms or a letterhead.