PDF template library
Manage your company's PDF templates, brand identities and defaults — revisions, impact, per-contact defaults, asset uploads, precedence, ETag concurrency and idempotency.
The PDF template library is where your company keeps the designs and brands that shape every document PDF the API renders. You create a template (layout, preset, columns, texts), give it a commercial identity (logo and brand), and then decide where it applies: as the company default, per document type, per series or per contact.
Everything in the library is versioned. A template or an identity is a head
(name, state, version) that points to its current revision; a revision is
an immutable snapshot of the design or the brand. Publishing never edits a
revision, it adds the next one. A document keeps the PDF it was captured with:
changing, archiving or restoring library resources only affects the documents you
create or revise afterwards.
| Resource | What it is | Path |
|---|---|---|
| Template | Design of the PDF: base preset, columns, titles, sections, page layout. | /v1/companies/{company}/pdf-templates |
| Template revision | Immutable snapshot of a template, numbered from 1. | /v1/companies/{company}/pdf-templates/{template}/revisions/{revision} |
| Commercial identity | Brand: commercial name, logo, brand image, brand mode, email and phone. | /v1/companies/{company}/pdf-commercial-identities |
| Asset | Logo or brand image you upload; private and immutable. | /v1/companies/{company}/pdf-templates/assets |
| Company defaults | Template and identity used when nothing more specific applies. | /v1/companies/{company}/pdf-templates/defaults |
| Contact defaults | Template preferred by one contact for one document type and purpose. | /v1/companies/{company}/pdf-templates/contact-defaults/{contact}/{document_type}/{purpose} |
All the routes in this guide hang off /v1/companies/{company}/…: {company} is the id (UUID v7)
of the tax ID you operate on, and in the curl examples it is $COMPANY_ID.
Reach a tax ID explains how to resolve it.
A commercial identity never replaces the issuer's tax identity. Legal name, tax
ID, tax data and bank accounts come from your company and from each document;
the library only holds how they look. A template or identity that tries to carry
tax or bank data, HTML, CSS, scripts or URLs is rejected with 422.
Library resources are returned as the bare object (no data envelope) and
lists as { "items": [...], "meta": { "page", "per_page", "total" } }. Every
identifier is a UUID v7 and is exposed as id.
Access: module, plan, scopes and roles
| Operation | Scope |
|---|---|
| Read templates, identities, revisions, assets, impact, defaults | pdf_templates:read |
| Create, rename, publish, duplicate, archive, restore, upload, change defaults | pdf_templates:write |
| Preview a template (see PDF previews and artifacts) | pdfs:read and pdf_templates:read |
- Roles. Reads are open to the
adminanduserroles of the company; writes and impact reads needadmin. Anemployeegets403 pdf_template_access_denied. An API key acts with the role of the user it belongs to, and the role and the module restrictions are checked again on every request. - Module and plan. The library is the
pdf_templatesmodule, available in Esencial, Profesional, Avanzado and Enterprise. The advanced editor (typography, colors, columns, page layout and thecorporativeandpremiumpresets) is thepdf_templates_editormodule, included from Profesional. Without it you can still manage templates that only change branding on theclassic,modernandminimalpresets; any other change returns403 feature_not_available_in_plan. Readpdf_capabilitiesin account personalization to know in advance. A downgrade keeps what you already stored; only new advanced changes need the editor. - Test mode. The library works the same with
fact_test_andfact_live_keys.
Templates
Create a template
POST /v1/companies/{company}/pdf-templates creates a template with its first revision. Only name
is required; the rest has defaults.
| Field | Meaning |
|---|---|
name | 1 to 120 characters. |
description | Optional, up to 500 characters. |
base_preset | classic, modern (default), minimal, corporative or premium. |
supported_types | Document types it can render: invoice, quote, proforma, delivery-note. Default: all four. |
supported_purposes | Purposes per type. Invoices: complete, simplified, corrective, simplified_corrective; the other types only complete. Default: all of them. |
supported_formats | a4 (default), ticket_80, ticket_58. Ticket formats are only for invoice-only templates. |
commercial_identity_id | Brand it uses by default, or null to inherit the company default identity. |
configuration | Partial design, merged over the preset defaults (see below). |
id, revision_id | Optional UUID v7 values if you want to choose the identifiers yourself. |
configuration is a closed object with the groups branding (mode:
logo, imago or none; size: small, medium, large or inherit, see
brand size), typography, colors, page, watermark,
titles, columns, company_fields, client_fields, custom_field_definitions
and sections. An unknown key, a wrong type or a value outside its range is
rejected with 422 invalid_pdf_template_configuration and the param points at
the exact path, for example configuration.typography.font_size. When
branding.mode is logo or imago, the template needs an effective brand: its
own commercial_identity_id or a company default identity that is active. Without one, the request returns 422 pdf_commercial_identity_required.
curl -X POST https://api.factuarea.com/v1/companies/$COMPANY_ID/pdf-templates \
-H "Authorization: Bearer fact_test_3pXnR2VbY7TcA9eFmN5z8KqW" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"name": "Invoices with logo",
"base_preset": "modern",
"supported_types": ["invoice"],
"commercial_identity_id": "0199f39e-1a20-7f6b-a2c8-3d5e9b0c4a71",
"configuration": { "branding": { "mode": "logo", "size": "medium" } }
}'The answer is 201 with the template and an ETag header (abbreviated here):
{
"id": "0199f3a2-7c4e-7b1a-9d3e-5a2c8f1b6e40",
"name": "Invoices with logo",
"description": null,
"state": "active",
"version": 1,
"availability": { "selectable": true, "reason": null },
"supported_types": ["invoice"],
"supported_purposes": { "invoice": ["complete", "simplified", "corrective", "simplified_corrective"] },
"supported_formats": ["a4"],
"commercial_identity_id": "0199f39e-1a20-7f6b-a2c8-3d5e9b0c4a71",
"current_revision": {
"id": "0199f3a2-7c52-7d03-8a41-6b9e0c2d7f15",
"template_id": "0199f3a2-7c4e-7b1a-9d3e-5a2c8f1b6e40",
"number": 1,
"base_preset": "modern",
"layout_version": "1",
"schema_version": 1,
"configuration": { "...": "complete normalized configuration" }
},
"created_at": "2026-10-14T09:12:41.000000Z",
"updated_at": "2026-10-14T09:12:41.000000Z"
}List and read
GET /v1/companies/{company}/pdf-templatesacceptsq(name search),state(activeby default,archivedorall),document_type,purpose,format,pageandper_page(1 to 100, default 25). Results are ordered by name. An invalid filter returns422with the filter inparam; it is never silently ignored.GET /v1/companies/{company}/pdf-templates/{template}returns the template with its current revision and theETagyou need for the nextPATCH.GET /v1/companies/{company}/pdf-templates/{template}/revisions/{revision}returns one exact revision, including archived ones.
Rename: metadata only
PATCH /v1/companies/{company}/pdf-templates/{template} changes only name and description
(description: null clears it). It never touches the design: sending
configuration here is a 422. A request that changes nothing creates no new
version. The precondition is mandatory, see concurrency.
curl -X PATCH https://api.factuarea.com/v1/companies/$COMPANY_ID/pdf-templates/0199f3a2-7c4e-7b1a-9d3e-5a2c8f1b6e40 \
-H "Authorization: Bearer fact_test_3pXnR2VbY7TcA9eFmN5z8KqW" \
-H "Content-Type: application/json" \
-H 'If-Match: "00295d3c3bb1b0b0ce5187e46fc6af39f41f507558608c8d14312fba4a710dc6"' \
-d '{ "name": "Invoices with logo (2026)" }'Publish a revision
Changing the design means publishing a new revision, in two steps.
- Ask for the impact:
GET /v1/companies/{company}/pdf-templates/{template}/impact?action=publish. The answer lists the associations that point at the template (company_default,type_default,contact_default,seriesandrecurring), theretained_countsof presentations, prepared outputs and pins that stay untouched, anywarnings, and animpact_fingerprint. - Publish with
POST /v1/companies/{company}/pdf-templates/{template}/revisions, sending theexpected_versionof the template, thatexpected_impact_fingerprintand at least one change amongbase_preset,commercial_identity_id,supported_types,supported_purposes,supported_formatsandconfiguration.
Objects in configuration are merged deeply over the current revision; lists such
as columns or custom_field_definitions are replaced completely. The answer is
201 with the new revision, and the template version goes up by one. If the
associations changed between the two calls, the fingerprint no longer matches and
you get 409 pdf_library_impact_conflict: read the impact again and decide.
curl -X POST https://api.factuarea.com/v1/companies/$COMPANY_ID/pdf-templates/0199f3a2-7c4e-7b1a-9d3e-5a2c8f1b6e40/revisions \
-H "Authorization: Bearer fact_test_3pXnR2VbY7TcA9eFmN5z8KqW" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"expected_version": 2,
"expected_impact_fingerprint": "b8a9f15005a46cf651f5652419b2edba941b3090090a1934a96f37c9cd0b973f",
"configuration": { "page": { "show_page_numbers": true }, "sections": { "footer_text": "Thank you for your business" } }
}'New documents resolve to the new revision. Documents already captured, prepared outputs and recurring pins keep the revision they had.
Duplicate
POST /v1/companies/{company}/pdf-templates/{template}/duplicate with name and the source
expected_version creates a new template (version 1, revision 1, active) with
the source's current configuration. The source can be archived; the copy is always
active. The copy does not inherit defaults, series assignments or contact
preferences, and it needs the brand the source would need today (plan capabilities
are checked at the moment of copying).
Archive and restore
An archived template stays readable and keeps its revisions and assets, but it is
no longer selectable (availability.selectable is false).
-
GET /v1/companies/{company}/pdf-templates/{template}/impact?action=archivereturns the associations that depend on it and a fingerprint. -
POST /v1/companies/{company}/pdf-templates/{template}/archivewithexpected_version,expected_impact_fingerprintandreplacements, a list (it can be empty) with one decision per association whoseactionisreplace_or_clearorpause:Decision Fields Replace by another template { "key", "expected_version", "action": "replace", "replacement_id" }Clear the link { "key", "expected_version", "action": "clear", "replacement_id": null }Pause (recurring invoices that follow the latest template) { "key", "expected_version", "action": "pause", "replacement_id": null }Copy
keyandversionfrom the association as the impact returned them;keyis opaque. Associations markedretainedtake no decision.
Every decision and every association is validated before anything changes, and
the archive is applied as one atomic change. A missing decision returns
422 pdf_template_default_replacement_required. POST /v1/companies/{company}/pdf-templates/{template}/restore
(only expected_version) makes the template active again; it does not reinstall the
defaults, series links or contact preferences that the archive replaced or cleared.
Commercial identities
A commercial identity is the brand a template shows. It has the same lifecycle as a
template, with the same operations under /v1/companies/{company}/pdf-commercial-identities: GET
(list, with q, state, page, per_page), POST (create), GET /{identity},
PATCH /{identity} (name and description), GET /{identity}/impact,
POST /{identity}/revisions, POST /{identity}/archive,
POST /{identity}/restore and GET /{identity}/revisions/{revision}.
The brand lives in configuration, which accepts exactly these keys:
| Key | Notes |
|---|---|
commercial_name | Up to 120 characters. |
logo_asset_id | An asset you uploaded with kind: logo. |
brand_image_asset_id | An asset you uploaded with kind: imago. |
brand_mode | logo, imago or none. |
contact_email, contact_phone | Up to 254 and 50 characters. When the template shows the company's email or phone, the brand's one replaces it in the contact line of the issuer; when the template hides it, neither is printed. There is no separate commercial contact block. |
logo_dark_asset_id | Optional. Version of the logo for dark backgrounds, an asset with kind: logo. It needs logo_asset_id: when you clear logo_asset_id, send null here too, or the request answers 422 invalid_pdf_template_configuration. |
brand_image_dark_asset_id | Optional. Version of the brand image for dark backgrounds, an asset with kind: imago. It needs brand_image_asset_id. |
dark_background_fallback | silhouette (default) or plate. See dark backgrounds. |
brand_size | small, medium (default) or large. See brand size. |
Every key accepts null except dark_background_fallback and brand_size. A brand has no address: the issuer address on the PDF is always the company's, a legal datum the brand cannot replace.
Assets must belong to your company and have the right kind; otherwise the request
fails (404 pdf_template_asset_not_found or 422). Publishing a revision takes
expected_version, expected_impact_fingerprint and a partial configuration with
at least one key. The impact of a brand also lists the templates that use it
(template associations), and archiving it asks for a decision per association,
exactly like archiving a template (a decision on a template association can also carry revision_id).
curl -X POST https://api.factuarea.com/v1/companies/$COMPANY_ID/pdf-commercial-identities \
-H "Authorization: Bearer fact_test_3pXnR2VbY7TcA9eFmN5z8KqW" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"name": "Main brand",
"configuration": {
"commercial_name": "Acme Studio",
"logo_asset_id": "0199f39d-52b1-7e34-9c07-1f6a8d3b2e90",
"logo_dark_asset_id": "0199f39d-6a14-7c52-8e31-2b7c9e4d1f05",
"brand_mode": "logo",
"contact_email": "hello@acme.example"
}
}'Brand on dark backgrounds
Some designs place the brand on a dark background, such as the frame of the
corporative preset or the header band of the control document. For every new capture,
the image the template shows (the one of brand_mode) is painted in this order:
- The dark version, if the brand has one. On a dark background,
logo_dark_asset_id(orbrand_image_dark_asset_idfor the brand image) is painted as it is. On paper it is not used. - The image as it is, if it reads. An image with its own background (a JPEG, or a PNG without transparency) is painted as it is, and so is one whose ink contrasts with the background.
- Otherwise, a silhouette or the plate. On a dark background,
dark_background_fallbackdecides:silhouette(default) paints the shape of the brand in white, andplateputs it on a light plate, as long as it reads there (if not, the white silhouette). - On a light background, such as the paper, an image that would disappear (a white logo, for example) is painted as a black silhouette.
The silhouette keeps the shape and the transparency of the image; only the colour
of its ink changes. In practice: upload your colour logo as logo_asset_id and
its white version as logo_dark_asset_id. Documents captured before this rule keep
the look they were captured with.
Brand size
brand_size is the default size of the brand: small, medium (default) or
large. A template sets its own in configuration.branding.size, or uses inherit,
the value of new templates, to take the size of its brand; without a brand,
medium. Templates that already stored a size keep it. Each size is an optical
area of the ink, not a maximum box: a wide logo gets lower and wider, a tall one
somewhat taller, and the transparent margin around the image does not count. The
same size applies to every design, the ticket and the control document.
Upload assets
POST /v1/companies/{company}/pdf-templates/assets uploads a logo or a brand image as
multipart/form-data. Idempotency-Key is required on this operation.
| Part | Rules |
|---|---|
kind | logo or imago. |
file | JPEG, PNG or WebP, up to 2 MiB, up to 6000 px per side and 24 megapixels. The real type and size are checked on the bytes, not on the extension. |
id | Optional UUID v7. |
curl -X POST https://api.factuarea.com/v1/companies/$COMPANY_ID/pdf-templates/assets \
-H "Authorization: Bearer fact_test_3pXnR2VbY7TcA9eFmN5z8KqW" \
-H "Idempotency-Key: $(uuidgen)" \
-F kind=logo \
-F file=@logo.png{
"id": "0199f39d-52b1-7e34-9c07-1f6a8d3b2e90",
"kind": "logo",
"mime": "image/png",
"sha256": "7a265f5f003866f84e23c7e237301879b7d4a4ecc0ab80d2041a3c980b12310a",
"size_bytes": 18342,
"created_at": "2026-10-14T09:05:12.000000Z"
}Assets are private and immutable; GET /v1/companies/{company}/pdf-templates/assets/{asset} returns
this metadata as JSON, never a storage path. With an image/* Accept header it returns the stored bytes inline, with Cache-Control: private, no-store. EXIF, GPS and text metadata are
removed from JPEG and PNG. Archiving a template or a brand does not delete its
assets, and each asset counts once toward your storage allowance even if several
brands share it.
Through MCP, upload_pdf_template_asset takes the image as image_base64 (up to
2048 KiB once decoded), the declared mime (image/jpeg, image/png or
image/webp) and a filename.
Company defaults
GET /v1/companies/{company}/pdf-templates/defaults returns the singleton of your company:
{
"id": "0199e0aa-4b10-7c33-9a52-6d8e1f3b7a24",
"version": 1,
"company_template_id": null,
"company_commercial_identity_id": null,
"type_defaults": [],
"render_activation": "legacy"
}| Field | Meaning |
|---|---|
company_template_id | Template used when nothing more specific applies. It must be able to render every document type and purpose. |
company_commercial_identity_id | Brand used by templates that have none of their own. |
type_defaults | List of { "document_type", "purpose", "template_id" }: the template for one document type and purpose. |
render_activation | Read-only. legacy or revisioned: whether the revisioned output built on this library is effectively active for your company. |
Until you write something, the read is virtual: id is your company UUID, version
is 1 and nothing is stored. The first PATCH /v1/companies/{company}/pdf-templates/defaults stores it
at version 2.
- An omitted field keeps its value and
nullclears it. type_defaultsreplaces the whole list: send every entry you want to keep. Paper format is not part of the key.render_activationis never accepted: sending it, even asnull, is a422. It is part of the representation, so when it changes theETagchanges too and a staleIf-Matchcorrectly answers412.
curl -X PATCH https://api.factuarea.com/v1/companies/$COMPANY_ID/pdf-templates/defaults \
-H "Authorization: Bearer fact_test_3pXnR2VbY7TcA9eFmN5z8KqW" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"expected_version": 1,
"company_template_id": "0199f3a2-7c4e-7b1a-9d3e-5a2c8f1b6e40",
"type_defaults": [
{ "document_type": "quote", "purpose": "complete", "template_id": "0199f3b0-2a41-7c88-9e02-d4f6a8b1c3e5" }
]
}'An archived template cannot be a default (422 pdf_template_archived), and a
template that does not support the document type or purpose is rejected with 422.
The classic account setting pdf_template in
account personalization is a different, older
layer. It is not replaced by these defaults: see
which template a document uses.
Contact defaults
A contact can prefer a template for each document type and purpose:
GET /v1/companies/{company}/pdf-templates/contact-defaults/{contact}/{document_type}/{purpose}PATCH /v1/companies/{company}/pdf-templates/contact-defaults/{contact}/{document_type}/{purpose}
document_type is invoice, quote, proforma or delivery-note, and purpose
must belong to that type (for invoices complete, simplified, corrective or
simplified_corrective; for the other types complete).
{
"id": "0199f2c1-8d10-7a55-8b3e-4c7d2e9f1a06",
"document_type": "invoice",
"purpose": "complete",
"template_id": null,
"version": 1,
"persisted": false
}id is the contact. If the contact was merged into another one, configure the
surviving contact: the preference of the merged one is not inherited. The contact
must be one of your company's customers. Without a stored preference the read is
virtual (version 1, persisted: false); the first write stores it at version 2.
PATCH takes template_id (null clears it) and the precondition. An unknown or
foreign contact returns 404, identical to a missing one.
Which template a document uses
When a document is created, issued or sent, Factuarea picks its template with this order, and the first level that has an available template for the document type, purpose and paper format wins:
- Manual selection on the document (
selection_mode: manual). It is exclusive: if that template is no longer available the operation fails, it never falls back. - Series default, from
GETandPATCH /v1/companies/{company}/series/{series}/pdf-template. - Contact default of the canonical contact.
- Company default for the document type and purpose (
type_defaults). - Company default template (
company_template_id). - Legacy: the account personalization (
pdf_template,accent_color, logo) and the classic renderer.
If an inherited level points at a template that is archived or incompatible, the
next level is used and the document reports a warning in presentation.warnings:
pdf_series_template_unavailable when it was the series, and
pdf_inherited_candidate_unavailable for any other inherited level. The brand is
resolved apart: the template's own commercial_identity_id, otherwise the company
default identity. How a single document reads and changes its selection is in
Document presentations.
Concurrency: versions, ETag and If-Match
Every mutable library resource has a version, and every representation that you
can later change carries a strong ETag: the SHA-256 of its full JSON, so it also
changes when availability or render_activation change. The same rules apply to
templates, identities, company defaults and contact defaults.
| Operation | Precondition |
|---|---|
PATCH on a resource | If-Match with the ETag, or expected_version in the body. At least one is mandatory. |
POST actions (revisions, duplicate, archive, restore) | expected_version in the body. If-Match is not accepted as a substitute. |
If you send both on a PATCH, the tag is evaluated first, then the version.
If-Match accepts one or several entity tags, weak ones (W/"...") and *.
| Status | Code | Meaning and recovery |
|---|---|---|
412 | pdf_template_precondition_failed | The ETag does not match the current representation. Read the resource again and retry with the new tag. |
409 | pdf_template_version_conflict | expected_version is old. error.details has current_version and recovery: reload_or_keep_local: reload and reapply your change, or keep your local copy and decide. |
428 | pdf_template_precondition_required | You sent neither. Add If-Match or expected_version. |
409 | pdf_library_impact_conflict | The impact fingerprint is stale. Read the impact again. |
A conflict never changes the resource, and current_version only comes from your
own company's resource. Series use the same pattern with their own codes:
series_template_precondition_failed, series_template_version_conflict and
series_template_precondition_required.
Idempotency
Idempotency-Key is required on POST /v1/companies/{company}/pdf-templates/assets and optional,
but recommended, on the other library writes. A replay with the same key and body
returns the stored response with Idempotent-Replayed: true before the version
check, so a retry after a timeout does not fail with 409 because your own first
attempt already advanced the version. The authority is checked again before a
replay: if you lost the role or the access, you get the authorization error, not the
cached response. Reusing a key
with a different body returns 422 idempotency_key_reused. See
Idempotency for the window and the format.
Errors
| Status | Code | When |
|---|---|---|
403 | pdf_template_access_denied | Role, module or user restriction does not allow it. |
403 | feature_not_available_in_plan | The change needs the advanced editor. |
404 | pdf_template_not_found, pdf_commercial_identity_not_found, pdf_template_asset_not_found | Missing or from another company, with the same answer. |
409 | pdf_template_version_conflict, pdf_library_impact_conflict | See concurrency. |
409 | pdf_template_asset_conflict | The id you chose for the asset upload already exists. |
409 | resource_conflict | Creating or duplicating a template met another request reserving identifiers at the same moment. Nothing was written; send it again after a few seconds. |
412 / 428 | pdf_template_precondition_failed, pdf_template_precondition_required | See concurrency. |
422 | invalid_pdf_template_configuration | Unknown key, wrong type, out of range, invalid filter, forbidden content or render_activation in the body. param has the path. |
422 | pdf_commercial_identity_required | The template needs a brand (branding.mode logo or imago) and neither it nor the company default has an active one. |
422 | pdf_template_archived, pdf_commercial_identity_unavailable | The template or brand is archived or not selectable. |
422 | pdf_template_default_replacement_required | The archive lacks a decision for an association. |
422 | idempotency_key_required | Missing Idempotency-Key on the asset upload. |
500 | pdf_library_association_coverage_incomplete | The server could not inspect every association; nothing was changed. Retry later. |
Branch on code, never on message. See the error model.
MCP
The library has an MCP tool for each operation: list_pdf_templates,
get_pdf_template, create_pdf_template, update_pdf_template,
publish_pdf_template_revision, duplicate_pdf_template, archive_pdf_template,
restore_pdf_template, get_pdf_template_impact, get_pdf_template_revision, the
equivalents for brands without duplication (list_pdf_commercial_identities,
get_pdf_commercial_identity, create_pdf_commercial_identity,
update_pdf_commercial_identity, publish_pdf_commercial_identity_revision,
archive_pdf_commercial_identity, restore_pdf_commercial_identity,
get_pdf_commercial_identity_impact and get_pdf_commercial_identity_revision),
get_pdf_template_defaults, set_pdf_template_defaults,
get_pdf_template_contact_default, set_pdf_template_contact_default,
upload_pdf_template_asset and get_pdf_template_asset. They need the same scopes
and roles, take the version as expected_version in the arguments (there are no
headers in MCP) and accept idempotency_key. See MCP tools.