PDF previews and artifacts
Preview a template before using it, prepare and render the exact PDF of a document, download it with ETag and 304, and read pdf_output_state with a correct retry loop.
Every PDF the API produces from the template library is an artifact: an exact, immutable PDF with its own identifier, state and checksum. Two kinds exist.
purpose | What it is | Lifetime |
|---|---|---|
document | The original PDF of a document: it is rendered from the presentation the document was captured with, and its bytes never change. | Kept with the document. |
preview | A draft rendering to check how a template, a brand or a document will look. It is never the official PDF of anything. | Expires 24 hours after it is created (expires_at). |
Four groups of operations cover the flow. Library management, defaults and precedence are in the PDF template library guide.
| Operation | Scopes | Idempotency-Key |
|---|---|---|
POST /v1/companies/{company}/pdf-template-previews | pdfs:read and pdf_templates:read | Required |
POST /v1/companies/{company}/pdf-artifacts (prepare) | pdfs:read | Required |
POST /v1/companies/{company}/pdf-artifacts/{artifact}/render | pdfs:read | Required |
GET /v1/companies/{company}/pdf-artifacts/{artifact} (metadata) | pdfs:read | Not applicable |
GET /v1/companies/{company}/pdf-artifacts/{artifact}/download | pdfs:read | Not applicable |
GET /v1/companies/{company}/invoices/{invoice}/pdf and its quote, proforma and delivery note equivalents | pdfs:read | Not applicable |
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.
Artifacts need the admin or user role and the module section of the document
family (invoices, quotes, proformas or delivery_notes). Preview artifacts
also follow the pdf_templates module and the pdf_templates:read scope, so
reading one needs that scope too. Generating a PDF consumes the same render budget
as the other PDF endpoints: new work can return 429 pdf_generation_budget_exceeded
with Retry-After, while replays and already rendered artifacts cost nothing more
(see rate limits).
Preview a template
POST /v1/companies/{company}/pdf-template-previews renders a PDF without saving anything: it does not
publish a template, change a default or touch a document. The body has five required
fields and a closed source:
| Field | Value |
|---|---|
document_type | invoice, quote, proforma or delivery-note. |
purpose | Invoices: complete, simplified, corrective, simplified_corrective. Other types: complete. |
format | a4, ticket_80 or ticket_58 (tickets for invoices only). |
locale | es, en or ca. |
source | Where the data comes from, see below. |
Depending on the source, three more fields describe the design to render:
base_preset, layout_version (always "1") and configuration (the complete
normalized configuration, exactly as a template revision returns it).
source.kind | Data it renders | Visual block |
|---|---|---|
example | Sample data: scenario is standard, long, simplified, corrective, simplified_corrective, f3 or unvalued_delivery_note. Optional commercial_identity: { "mode": "inherit" } (default), { "mode": "exact_revision", "identity_id", "revision_id" } or { "mode": "draft", "configuration": { ... } } with the six brand keys and, optionally, logo_dark_asset_id, brand_image_dark_asset_id, dark_background_fallback and brand_size. | All three required. |
unsaved_form | The form object you send (series, contact, document, recipient, lines, payment terms...), nothing persisted. | All three required. |
persisted_draft | An existing draft document: { "kind": "persisted_draft", "document_id" }. | All three or none. |
presentation_revision | An issued document and its retained presentation: document_id, parent_id, template_id (null to inherit), source_policy (captured or current_legal) and expected_version. Accepts a format other than the retained one to ask for another paper of the same issued document (receipt or A4), without creating a revision. | All three or none. |
invoice_substitution | A detached preview of the invoice that would substitute simplified invoices: document_id (null for a new one), source_invoice_uuids and line_uuids (same length and order, up to 100), client_uuid, notes, prepared_date and template_id (null to inherit). Only with document_type: invoice, purpose: complete and format: a4; nothing is saved or substituted. | None: omit all three. |
When a persisted source omits the visual block, the server previews the template
that would apply today. Sending only one or two of the three fields is a
422 pdf_presentation_invalid, with the missing one in param. The draft brand
selector needs the admin role and assets you already uploaded.
For document_type: invoice, source.form.document.exemption_reason is an optional header exemption cause (catalogue in GET /v1/tax-catalog): its legal mention opens the automatic mentions of the preview, as when the invoice is saved. Omit it or send null for no cause. An unknown cause, or one sent on another document type, is rejected.
A good way to preview a design you are editing is to reuse the revision you read:
curl -s https://api.factuarea.com/v1/companies/$COMPANY_ID/pdf-templates/0199f3a2-7c4e-7b1a-9d3e-5a2c8f1b6e40 \
-H "Authorization: Bearer fact_test_3pXnR2VbY7TcA9eFmN5z8KqW" \
| jq '{
document_type: "invoice", purpose: "complete", format: "a4", locale: "en",
base_preset: .current_revision.base_preset,
layout_version: .current_revision.layout_version,
configuration: .current_revision.configuration,
source: { kind: "example", scenario: "standard" }
}' > preview.json
curl -X POST https://api.factuarea.com/v1/companies/$COMPANY_ID/pdf-template-previews \
-H "Authorization: Bearer fact_test_3pXnR2VbY7TcA9eFmN5z8KqW" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
--data-binary @preview.jsonThe answer is the artifact, wrapped in data. It is 200 when the PDF is ready and
202 while it is prepared, queued or processing; in the second case poll
metadata_url as described in the retry loop (abbreviated):
{
"data": {
"id": "0199f405-91ab-7c42-8d6e-2a9f3b5c7e10",
"purpose": "preview",
"state": "ready",
"source_kind": "example",
"context_authority": "illustrative",
"sha256": "7a265f5f003866f84e23c7e237301879b7d4a4ecc0ab80d2041a3c980b12310a",
"size_bytes": 48210,
"expires_at": "2026-10-15T09:20:00.000000Z",
"geometry": { "format": "a4", "orientation": "portrait", "width_mm": 210, "height_mm": 297 },
"metadata_url": "https://api.factuarea.com/v1/companies/0193f1a0-5c2e-7b41-8d06-9a1e2f3c4d5e/pdf-artifacts/0199f405-91ab-7c42-8d6e-2a9f3b5c7e10",
"download_url": "https://api.factuarea.com/v1/companies/0193f1a0-5c2e-7b41-8d06-9a1e2f3c4d5e/pdf-artifacts/0199f405-91ab-7c42-8d6e-2a9f3b5c7e10/download",
"retry_after": null,
"error": null,
"unavailable_reason": null
}
}The artifact also reports the effective_font and warnings of the rendering,
context_fingerprint, artifact_fingerprint and the tokens described next.
Authority: what a preview can authorize
context_authority tells how far you can trust the preview.
context_authority | Origin | Use |
|---|---|---|
illustrative | example, unsaved_form, or a persisted source with a different visual block. | Look only. It has no document_id, source_token or selection_version. |
delivery_precondition | persisted_draft previewing the template that applies. | It returns source_token, selection_version, template_revision_id, commercial_identity_revision_id and expected_preview_context_fingerprint: the values of the presentation precondition of the issue, send and accept operations. See sending documents. |
revision_precondition | presentation_revision. | The same values, valid only for an explicit revision of that document. They never authorize issuing, sending, accepting or signing. |
substitution_precondition | invoice_substitution. | It returns the same values as the other preconditions, specific to the substitution of that invoice. No v1 operation consumes them (POST /v1/companies/{company}/invoices/substitute-simplified takes no precondition), and they never authorize issuing, sending, accepting or signing. |
captured | The original document artifacts. | The retained PDF of a captured document. |
A preview is never converted into the official PDF. If the source, the selection, the
brand, the configuration or the rendering environment changes after you previewed,
the precondition no longer matches and the action answers 409; preview again.
Previews and the company switch
New previews and new document artifacts need revisioned output to be active for your
company: the platform switch on and your company's initialization finished (the
read-only render_activation of the defaults
reports it). Otherwise the call returns 422 pdf_presentation_not_ready and creates
nothing. Nothing else is lost: retrying later works, and reads and the classic
rendering keep working.
Prepare, render and read a document PDF
The official artifact of a document is created from its retained presentation:
the id of the presentation block that the document carries (see
Document presentations), together with its
document type, purpose and paper format.
Prepare. POST /v1/companies/{company}/pdf-artifacts with document_type, document_id,
presentation_id, purpose, format and variant. It creates or finds the
artifact for exactly that presentation, so repeating the call returns the same one.
curl -X POST https://api.factuarea.com/v1/companies/$COMPANY_ID/pdf-artifacts \
-H "Authorization: Bearer fact_test_3pXnR2VbY7TcA9eFmN5z8KqW" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"document_type": "invoice",
"document_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a42",
"presentation_id": "0199f401-33de-7a8c-9e15-b7c4d2a60f38",
"purpose": "complete",
"format": "a4",
"variant": "official"
}'Render. POST /v1/companies/{company}/pdf-artifacts/{artifact}/render with { "mode": "sync" } to
wait for the PDF in the request, or { "mode": "async" } to queue it and poll.
Rendering a ready artifact does not render again or charge again.
Read the state. GET /v1/companies/{company}/pdf-artifacts/{artifact} always answers 200 with the
metadata, whatever the state. These reads accept no query string, no body and no
If-Match.
Download. GET /v1/companies/{company}/pdf-artifacts/{artifact}/download returns the bytes once the
state is ready.
prepare and render answer 200 with the artifact when it is already ready,
and 202 while it is prepared, queued or processing. state is one of
prepared, queued, processing, ready, failed, unavailable and corrupt.
A failed artifact carries error with the code (pdf_generation_failed,
pdf_engine_profile_unavailable, pdf_image_resource_unavailable or
pdf_payload_resource_unavailable), a retryable flag and a correlation_id to
give to support. retry_after is the number of seconds to wait while it is pending.
Original and derived PDFs
- The
officialvariant is the original: the PDF of the presentation captured with the document, with an immutable checksum. It is the onepdf_output_statereports. Changing a template, a default or a brand later never alters it. - A different look for an already issued document is a new presentation: you revise the document explicitly, which creates a derived presentation with its own artifact. The original stays readable.
- The
alternativevariant is another artifact of the same presentation in an authorized paper format. It never replaces the original, never changes what the document publishes and is not the onepdf_output_statereports. A different style always needs a new presentation. - If the bytes of an original are lost or fail their checksum, you get
500 pdf_original_unavailableor500 pdf_original_corrupt. Nothing is silently regenerated in its place. - A derived presentation (a signature, the legal QR or refreshed legal data) keeps
the layout of its original. If the current release no longer serves that layout,
preparing it answers
500 pdf_original_layout_unavailable: the original is intact and nothing is prepared. Contact support; an explicit, authorized revision creates another artifact without replacing the original.
How downloads answer
| Status | When | What to do |
|---|---|---|
200 | The PDF is ready. application/pdf, Content-Disposition: attachment, Cache-Control: private, no-store and a strong ETag: the SHA-256 of the bytes between quotes. | Store it; keep the ETag. |
304 | You sent If-None-Match with the current ETag. It is decided after authorization, privacy and a check of the stored bytes. | Use your copy. |
202 | GET /v1/companies/{company}/{documents}/{id}/pdf only: the PDF is still being generated, including a render that failed but can be retried, which this request claimed again (a retryable failure is never answered with the earlier error). JSON data with pdf_url and pdf_output_state, and a Retry-After header. | Wait and repeat the same request. |
503 | GET .../download of an artifact that is not ready: pdf_artifact_not_ready with Retry-After. The metadata still answers 200. The same 503 comes from a preview, prepare, render, GET /v1/companies/{company}/{documents}/{id}/pdf or GET /v1/companies/{company}/invoices/{invoice}/pdf-link while another request is still preparing that same PDF; that request created nothing. | Wait Retry-After seconds and repeat the same request. |
503 | pdf_generation_failed: the direct document endpoints could not produce the PDF; the document and its data are intact. | Wait and repeat the same request. |
410 | pdf_signature_personal_data_forgotten: the signature data of the document was erased. Terminal. | Do not retry; the artifact has no download any more. |
404 | pdf_artifact_not_found: unknown, from another company, or an expired preview. | Create a new preview. |
500 | pdf_original_unavailable, pdf_original_corrupt, pdf_original_layout_unavailable, or a render that failed terminally: pdf_engine_profile_unavailable, pdf_payload_resource_unavailable, pdf_image_resource_unavailable. Nothing is enqueued. | Contact support with request_id. |
Document-level GET /v1/companies/{company}/invoices/{invoice}/pdf, GET /v1/companies/{company}/quotes/{quote}/pdf,
GET /v1/companies/{company}/proformas/{proforma}/pdf and GET /v1/companies/{company}/delivery-notes/{delivery_note}/pdf
return the same 200 with the bytes, the 304 with If-None-Match and the 202
above. They accept download=1 for an attachment disposition, and invoices also
format. See the reference of
invoices,
quotes,
proformas and
delivery notes.
When the original cannot be served they answer an error instead of a 202: the statuses above apply, plus 409 when the retained presentation cannot be delivered as requested: pdf_presentation_conflict (it changed since you read it), pdf_presentation_context_changed (for example, the requested format differs from the retained one) and, for invoices, pdf_legal_source_changed (revise it with source_policy: "current_legal"). Read the document presentation again before requesting the PDF. Before a document has a retained presentation, or while your
company still renders with the classic pipeline, the PDF is served as before.
The pdf_output_state block
Invoices, quotes, proformas and delivery notes carry a read-only pdf_output_state
object that summarizes their original PDF. It never prepares or renders anything.
{
"state": "processing",
"presentation_id": "0199f401-33de-7a8c-9e15-b7c4d2a60f38",
"artifact_id": "0199f405-91ab-7c42-8d6e-2a9f3b5c7e10",
"format": "a4",
"variant": "official",
"unavailable_reason": null,
"error": null,
"retry_after": 1,
"download_url": null
}| Field | Meaning |
|---|---|
state | prepared, queued and processing (pending), ready, failed, unavailable, corrupt, or null when the document has no captured original yet. |
presentation_id, artifact_id | The retained presentation and its original artifact; artifact_id is what you pass to /v1/companies/{company}/pdf-artifacts/{artifact}. |
format, variant | Paper format and official. |
unavailable_reason | privacy_erased (state unavailable, the original is gone for good) or original_corrupt (state corrupt). |
error | { code, retryable, correlation_id } for failed, corrupt and erased originals. |
retry_after | Seconds to wait while pending, otherwise null. |
download_url | Only when ready: the relative v1 URL /v1/companies/{company}/pdf-artifacts/{id}/download, never a storage path. Resolve it against the API host, or build the call from artifact_id. |
All the keys are always present; a document without a captured original has them
all null. A privacy erasure shows as state: unavailable with
error.code: pdf_signature_personal_data_forgotten, no retry_after and no
download_url.
A correct retry loop
Poll the cheap read, wait what the server asks, bound the attempts, and stop on terminal answers. Do not regenerate to "speed it up".
async function downloadOriginal(companyId, artifactId, key) {
const base = 'https://api.factuarea.com/v1/companies/' + companyId + '/pdf-artifacts/' + artifactId;
const headers = { Authorization: 'Bearer ' + key };
for (let attempt = 1; attempt <= 12; attempt++) {
const res = await fetch(base + '/download', { headers });
if (res.status === 200) return Buffer.from(await res.arrayBuffer());
if (res.status === 503) {
const wait = Number(res.headers.get('Retry-After')) || 2 * attempt;
await new Promise((r) => setTimeout(r, wait * 1000));
continue;
}
const { error } = await res.json();
// 410 is terminal, 500 needs support, 404 means it expired or never existed.
throw new Error(error.code + ' (' + res.status + ') ' + error.request_id);
}
throw new Error('The PDF is still not ready; check the artifact metadata later.');
}- Respect
Retry-Afterand theretry_afterfield. They are seconds. Add a small increase between attempts and a cap on the number of attempts. - Poll, do not re-create. Repeat the same
GET. Callingprepareorrenderon every poll only adds noise: they already return the same artifact. - Retry a failure on purpose. When
stateisfailedanderror.retryableistrue, callrenderagain with a newIdempotency-Key(the same key would replay the stored failure). Ifretryableisfalse, change the cause first: the asset, the amount of data or the configuration. - A new key is a new intention. Reuse the same key only to retry the same request after a timeout.
- Stop on
410. It will not change. - Cache with the
ETag. SendIf-None-Matchto get a free304.
Idempotency
preview, prepare and render need Idempotency-Key. For these three the
fingerprint is computed on the raw body, so the same key with a body that differs
by a single byte (key order or whitespace included) returns 422 idempotency_key_reused.
A replay returns the stored response, with Idempotent-Replayed: true, before any
work and even if the live data changed since; the authorization of the caller is still
checked first. Two keys with equivalent bodies produce the same artifact. See
Idempotency.
Errors
| Status | Code | Meaning |
|---|---|---|
403 | pdf_template_access_denied | The role is not allowed, or the module section of the document type is not available to you. |
404 | pdf_artifact_not_found, pdf_presentation_not_found | Missing, from another company or expired. |
409 | pdf_presentation_conflict, pdf_presentation_context_changed | The selection, source, brand or environment changed after the preview. Preview again. |
409 | pdf_legal_source_changed | The legal data of an issued invoice changed since it was captured; the original stays readable and the new delivery is blocked until you revise explicitly. |
412 / 428 | pdf_presentation_etag_mismatch, pdf_presentation_precondition_required | Presentation preconditions on the operations that use them. |
410 | pdf_signature_personal_data_forgotten | Terminal erasure. |
422 | pdf_presentation_invalid | A field is invalid. param has the path, such as source.kind. |
422 | pdf_presentation_not_ready | Revisioned output is not active for your company yet. |
422 | idempotency_key_required | The key is missing. |
429 | pdf_generation_budget_exceeded | Render budget; wait Retry-After. |
500 | pdf_original_unavailable, pdf_original_corrupt, pdf_original_layout_unavailable, pdf_engine_profile_unavailable, pdf_image_resource_unavailable, pdf_payload_resource_unavailable | The original, its layout or the rendering resources are not available. The last two are not retryable. |
503 | pdf_artifact_not_ready | Not ready yet; wait Retry-After. |
503 | pdf_generation_failed | The direct document PDF endpoints could not produce the PDF. The document is intact; repeat later. |
Branch on code, not on message. See the error model.
MCP
The five artifact operations are the tools preview_pdf_template,
prepare_pdf_artifact, render_pdf_artifact, get_pdf_artifact and
read_pdf_artifact, which returns an authorized download link instead of the bytes.
They use the same scopes, accept idempotency_key and follow the same states. See
MCP tools.