Factuarea APIDevelopers

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.

ResourceWhat it isPath
TemplateDesign of the PDF: base preset, columns, titles, sections, page layout./v1/companies/{company}/pdf-templates
Template revisionImmutable snapshot of a template, numbered from 1./v1/companies/{company}/pdf-templates/{template}/revisions/{revision}
Commercial identityBrand: commercial name, logo, brand image, brand mode, email and phone./v1/companies/{company}/pdf-commercial-identities
AssetLogo or brand image you upload; private and immutable./v1/companies/{company}/pdf-templates/assets
Company defaultsTemplate and identity used when nothing more specific applies./v1/companies/{company}/pdf-templates/defaults
Contact defaultsTemplate 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

OperationScope
Read templates, identities, revisions, assets, impact, defaultspdf_templates:read
Create, rename, publish, duplicate, archive, restore, upload, change defaultspdf_templates:write
Preview a template (see PDF previews and artifacts)pdfs:read and pdf_templates:read
  • Roles. Reads are open to the admin and user roles of the company; writes and impact reads need admin. An employee gets 403 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_templates module, available in Esencial, Profesional, Avanzado and Enterprise. The advanced editor (typography, colors, columns, page layout and the corporative and premium presets) is the pdf_templates_editor module, included from Profesional. Without it you can still manage templates that only change branding on the classic, modern and minimal presets; any other change returns 403 feature_not_available_in_plan. Read pdf_capabilities in 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_ and fact_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.

FieldMeaning
name1 to 120 characters.
descriptionOptional, up to 500 characters.
base_presetclassic, modern (default), minimal, corporative or premium.
supported_typesDocument types it can render: invoice, quote, proforma, delivery-note. Default: all four.
supported_purposesPurposes per type. Invoices: complete, simplified, corrective, simplified_corrective; the other types only complete. Default: all of them.
supported_formatsa4 (default), ticket_80, ticket_58. Ticket formats are only for invoice-only templates.
commercial_identity_idBrand it uses by default, or null to inherit the company default identity.
configurationPartial design, merged over the preset defaults (see below).
id, revision_idOptional 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-templates accepts q (name search), state (active by default, archived or all), document_type, purpose, format, page and per_page (1 to 100, default 25). Results are ordered by name. An invalid filter returns 422 with the filter in param; it is never silently ignored.
  • GET /v1/companies/{company}/pdf-templates/{template} returns the template with its current revision and the ETag you need for the next PATCH.
  • 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.

  1. 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, series and recurring), the retained_counts of presentations, prepared outputs and pins that stay untouched, any warnings, and an impact_fingerprint.
  2. Publish with POST /v1/companies/{company}/pdf-templates/{template}/revisions, sending the expected_version of the template, that expected_impact_fingerprint and at least one change among base_preset, commercial_identity_id, supported_types, supported_purposes, supported_formats and configuration.

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).

  1. GET /v1/companies/{company}/pdf-templates/{template}/impact?action=archive returns the associations that depend on it and a fingerprint.

  2. POST /v1/companies/{company}/pdf-templates/{template}/archive with expected_version, expected_impact_fingerprint and replacements, a list (it can be empty) with one decision per association whose action is replace_or_clear or pause:

    DecisionFields
    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 key and version from the association as the impact returned them; key is opaque. Associations marked retained take 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:

KeyNotes
commercial_nameUp to 120 characters.
logo_asset_idAn asset you uploaded with kind: logo.
brand_image_asset_idAn asset you uploaded with kind: imago.
brand_modelogo, imago or none.
contact_email, contact_phoneUp 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_idOptional. 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_idOptional. Version of the brand image for dark backgrounds, an asset with kind: imago. It needs brand_image_asset_id.
dark_background_fallbacksilhouette (default) or plate. See dark backgrounds.
brand_sizesmall, 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:

  1. The dark version, if the brand has one. On a dark background, logo_dark_asset_id (or brand_image_dark_asset_id for the brand image) is painted as it is. On paper it is not used.
  2. 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.
  3. Otherwise, a silhouette or the plate. On a dark background, dark_background_fallback decides: silhouette (default) paints the shape of the brand in white, and plate puts it on a light plate, as long as it reads there (if not, the white silhouette).
  4. 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.

PartRules
kindlogo or imago.
fileJPEG, 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.
idOptional 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"
}
FieldMeaning
company_template_idTemplate used when nothing more specific applies. It must be able to render every document type and purpose.
company_commercial_identity_idBrand used by templates that have none of their own.
type_defaultsList of { "document_type", "purpose", "template_id" }: the template for one document type and purpose.
render_activationRead-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 null clears it.
  • type_defaults replaces the whole list: send every entry you want to keep. Paper format is not part of the key.
  • render_activation is never accepted: sending it, even as null, is a 422. It is part of the representation, so when it changes the ETag changes too and a stale If-Match correctly answers 412.
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:

  1. 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.
  2. Series default, from GET and PATCH /v1/companies/{company}/series/{series}/pdf-template.
  3. Contact default of the canonical contact.
  4. Company default for the document type and purpose (type_defaults).
  5. Company default template (company_template_id).
  6. 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.

OperationPrecondition
PATCH on a resourceIf-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 *.

StatusCodeMeaning and recovery
412pdf_template_precondition_failedThe ETag does not match the current representation. Read the resource again and retry with the new tag.
409pdf_template_version_conflictexpected_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.
428pdf_template_precondition_requiredYou sent neither. Add If-Match or expected_version.
409pdf_library_impact_conflictThe 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

StatusCodeWhen
403pdf_template_access_deniedRole, module or user restriction does not allow it.
403feature_not_available_in_planThe change needs the advanced editor.
404pdf_template_not_found, pdf_commercial_identity_not_found, pdf_template_asset_not_foundMissing or from another company, with the same answer.
409pdf_template_version_conflict, pdf_library_impact_conflictSee concurrency.
409pdf_template_asset_conflictThe id you chose for the asset upload already exists.
409resource_conflictCreating or duplicating a template met another request reserving identifiers at the same moment. Nothing was written; send it again after a few seconds.
412 / 428pdf_template_precondition_failed, pdf_template_precondition_requiredSee concurrency.
422invalid_pdf_template_configurationUnknown key, wrong type, out of range, invalid filter, forbidden content or render_activation in the body. param has the path.
422pdf_commercial_identity_requiredThe template needs a brand (branding.mode logo or imago) and neither it nor the company default has an active one.
422pdf_template_archived, pdf_commercial_identity_unavailableThe template or brand is archived or not selectable.
422pdf_template_default_replacement_requiredThe archive lacks a decision for an association.
422idempotency_key_requiredMissing Idempotency-Key on the asset upload.
500pdf_library_association_coverage_incompleteThe 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.

On this page

Need a hand?Contact support