Skip to main content

Errors

Every error the API answers is an RFC 9457 problem with Content-Type: application/problem+json; charset=utf-8. This one is a synchronous render whose template does not compile:

{
"type": "https://docs.formfeed.dev/errors/template-syntax",
"title": "Template syntax error",
"status": 422,
"detail": "unknown block tag: endfo",
"code": "template_syntax_error",
"location": { "line": 41, "column": 3 },
"render_id": "rnd_7da09ab6cd9946b798c187c2a9350852",
"request_id": "req_4b7c1e9a2f3d4c5b8a6e7f9012"
}

Fields​

Every problem has these fields:

FieldMeaning
codeStable, machine-readable error code. Branch on this, not on title or detail.
statusThe HTTP status, repeated in the body. It always follows from the code, as the table below lists.
titleShort description of the code; the same for every problem with this code.
detailWhat went wrong in this request, for humans and logs. The wording can change, so never parse it.
typeA link to the page for this code.
request_idThe id of this request, req_ followed by 26 hex digits. The same value is in the X-Request-Id response header. Send your own X-Request-Id header (up to 255 letters, digits, _, - and =) and Formfeed uses it instead. Include it when you contact support.

Many problems add fields of their own:

FieldWhere
issuesinvalid_request when the body or the query does not fit the contract: one entry per problem, with path (the field, such as mode or post.watermark.text; empty for the body as a whole) and message.
errorsdata_validation_error and einvoice_data_invalid: every place where the data does not fit, with path, message and, for e-invoices, the rule.
locationOn the problems of a render that failed while it ran. For a template error: line and column in the part that failed (the body, the header or the footer; the problem does not say which). For a Word or PowerPoint template: part (such as word/document.xml) and, where known, paragraph. null when the failure has no place in the template.
fieldThe request field the problem is about, such as output, validate_data, post.merge_after or storage.key.
indexIn a batch, the item that failed (counted from 0). In a PDF tool, the position of the source in sources.
render_idThe rnd_… id of the render the problem is about: a synchronous render or a PDF tool that failed after it was recorded, which the render log and GET /v1/renders/{id} then show, or the source render a PDF tool could not use.
feature, plan, limitforbidden when the plan does not include something: which feature, the plan, and the number the plan allows.
einvoice, accessibilityThe validator's reports of an e-invoice or a PDF/UA-1 file that failed.

Some codes add more, such as page_count on page_limit_exceeded or period_end on quota_exceeded; the page of each code lists them.

Failed async renders​

An async render, a batch item, and a synchronous render that went on in the background (a 202 answer) fail later, so no problem comes back from the request. The failure is kept on the render instead:

  • GET /v1/renders/{id} and the render.failed webhook carry it in error, in a shorter form: code, message (what detail would say) and the fields of the code, such as line and column or page_count. The e-invoice and PDF/UA reports are in the render's own einvoice and accessibility fields.
  • GET /v1/renders/{id}/result answers a failed render with the full problem, as the synchronous request would have, including render_id.

A failed render, its other fields left out:

{
"id": "rnd_7da09ab6cd9946b798c187c2a9350852",
"status": "failed",
"error": {
"code": "page_limit_exceeded",
"message": "Document has 212 pages; the plan allows 200",
"page_count": 212
}
}

A render that failed on Formfeed's side three times has "code": "worker_exhausted", the last attempt's message and attempts; see retrying.

Handling errors in code​

The SDKs turn every problem into one error class. TypeScript:

import { Formfeed, FormfeedError } from '@formfeed/sdk';

const client = new Formfeed({ apiKey: process.env.FORMFEED_API_KEY! });

try {
await client.renders.create({ template: 'invoice', data });
} catch (e) {
if (!(e instanceof FormfeedError)) throw e;
switch (e.code) {
case 'template_syntax_error':
case 'template_runtime_error':
// a template bug: alert whoever owns the template, do not retry
console.error('template error', e.message, e.problem, e.requestId);
break;
case 'quota_exceeded':
// stop sending live renders until the period resets or the plan changes
break;
default:
throw e;
}
}

Python:

import os
from formfeed import Formfeed, FormfeedError

client = Formfeed(os.environ["FORMFEED_API_KEY"])

try:
client.renders.create(template="invoice", data=data)
except FormfeedError as e:
if e.code in ("template_syntax_error", "template_runtime_error"):
# a template bug: alert whoever owns the template, do not retry
print("template error", e.message, e.problem, e.request_id)
elif e.code == "quota_exceeded":
pass # stop sending live renders until the period resets or the plan changes
else:
raise
TypeScript (FormfeedError)Python (FormfeedError)Meaning
codecodeThe problem's code, or a code of the SDK itself (below)
statusstatusThe HTTP status; 0 when no answer arrived
messagemessageThe problem's detail, else its title
problemproblemThe whole problem body, with every field above; null / None for an SDK error
requestIdrequest_idThe X-Request-Id header of the answer

Codes the SDKs raise themselves, without an API problem: network_error (no answer after the retries, a request timeout included), timeout (only from waitFor / wait_for, which gave up waiting for a render or job), not_ready (a download of a render that has no file), download_failed, invalid_response (TypeScript: a success answer that is not JSON), and http_<status> for an error answer without a code.

renders.waitFor / wait_for does not raise for a failed render: it returns the render with status: "failed", and its error says why.

Retrying​

The SDKs retry 429, 503 and network errors by themselves: up to 3 times (maxRetries / max_retries, 0 turns it off), waiting the seconds of Retry-After when the answer has the header, otherwise 0.5, 1 and 2 seconds, plus up to 0.25 seconds of jitter and never more than 30 seconds at a time. A request that gets no answer within the client's timeout (timeoutMs / timeout, 120 seconds by default) counts as a network error. Every call that creates a render (renders, batches, PDF tools) sends a generated Idempotency-Key, and its retries reuse it. They never retry 500, 502 or 504. See the SDK pages for TypeScript and Python.

Without an SDK, or for what the SDKs leave to you:

AnswerRetry?How
429 rate_limitedyesAfter the seconds in Retry-After, with the same Idempotency-Key.
503 region_unavailable, office_unavailable, conformance_unavailableyesAfter the seconds in Retry-After, with the same Idempotency-Key.
500 worker_exhausted, tool_failedyes, a few timesWith growing pauses (for example 5, 30 and 120 seconds) and the same Idempotency-Key. If it keeps failing, send the request_id to support.
500 pdfa_conversion_failednoThe same document fails the same way; send the render_id to support.
504 render_timeoutonly if the cause passedA slow server for images or fonts can recover; a template or page that is too slow fails again. See the page of the code.
No answer at all (timeout, connection reset)yesWith the same Idempotency-Key: if the first attempt did arrive, you get its answer instead of a second render.
Any other 4xx, and 501noFix what the problem names, then send the request with a new Idempotency-Key or none.

Why a new key after a 4xx: the first answer to a POST with an Idempotency-Key is stored for 24 hours per workspace and returned again, with Idempotent-Replayed: true, for every repeat of that key. That holds for problems too. Only answers of 500 and above are not stored, and neither are a 401 or a 429 of the rate limit, which are answered before the key is looked at. A corrected request sent with the old key therefore gets the old problem back.

Async renders and batch items are retried by the queue itself when they fail on Formfeed's side (a status of 500 or above, except pdfa_conversion_failed): three attempts, the second 5 seconds and the third 10 seconds after the one before. When the third fails too, the render fails with worker_exhausted and keeps the last attempt's message. Every other failure fails the render at once. To try a failed async render again yourself, send the request with a new Idempotency-Key: the old key returns the stored 202 of the failed render.

A failed render costs nothing, whatever the code.

All codes​

HTTPCodeMeaningRetry
400invalid_requestThe body or a parameter does not fit the contract, or a combination of fields is not allowed.no
401unauthorizedThe request carries no API key, or the key is unknown, revoked or expired.no
402quota_exceededThe units of the current period are used up and the plan cannot go on with overage, or the overage reached its hard cap.no
403forbiddenThe key lacks the scope this endpoint needs, the plan lacks the feature, or the caller may not do this.no
403test_output_unavailableA Free test render asked a Word or PowerPoint template for its own format; it makes the marked PDF only.no
404not_foundThe render, job, file, webhook, partial or path does not exist in this workspace.no
404template_not_foundNo template with this slug or id has the version the request names in the workspace of the key.no
404channel_not_foundThe request names a release channel the template does not have.no
409conflictA slug is taken, the template or shared partial changed since the version you based your change on, or a device code was used already.no
409schema_breaking_changePublishing or moving a channel would put a version live whose stored data schema breaks callers of the current one.no
409channel_in_useRenders of the last hour used the channel you are deleting; pass force=true.after an hour without renders
409listen_session_limitThree formfeed listen sessions are already running in the workspace.after one ends
409last_workspaceThe workspace you are deleting is the last one of the organisation.no
409storage_not_configuredThe request needs the workspace's own bucket (storage.key), but the workspace has no storage connection.after connecting one
410render_input_expiredThe request of the render was not kept, or its retention period ended.no
410listen_session_endedThe formfeed listen session a resend or delivery names is gone.no
413file_too_largeAn uploaded file is over its limit: 10 MB for the file library, 20 MB for a document to convert or a Word or PowerPoint template.no
415file_type_unsupportedThe file is not an office document Formfeed converts, is macro-enabled, or is not the kind the template takes.no
422channel_name_invalidA release channel name is malformed or reserved.no
422template_syntax_errorThe template could not be compiled.no
422template_runtime_errorThe template compiled, but failed while it ran with your data.no
422data_validation_errorThe data does not match the template's data schema (with validate_data: true).no
422navigation_failedA url render could not load the page.no
422page_limit_exceededThe PDF has more pages than the plan allows.no
422output_too_largeThe rendered file, or its HTML, is larger than the limit.no
422source_not_foundA source of a PDF tool or of post.merge_after does not exist, or its output is gone.no
422source_invalidA source is not a finished, readable PDF (or office document, for a conversion).no
422source_encryptedA source is password-protected.no
422office_document_invalidThe office document's container is damaged or fails a safety check.no
422office_document_too_largeThe office document unpacks to more than Formfeed processes, or a filled template grows too large.no
422office_conversion_failedLibreOffice could not convert the document.no
422unsupported_in_officeA Word or PowerPoint template uses something office templates cannot run, such as an include.no
422einvoice_data_invalidThe _invoice block of an e-invoice is missing or breaks a rule, or the validator refused the finished file.no
422einvoice_profile_unsupportedThe e-invoice options name a profile or a flavour Formfeed does not create.no
422einvoice_display_mismatchWith display_check: "strict", the PDF does not show a value the XML of its e-invoice states.no
422pdfua_validation_failedWith pdf.ua and the strict check, the file fails a rule of PDF/UA-1; accessibility names the rules.no
422storage_test_failedThe test object could not be written to the bucket, so the storage connection was not saved; error carries the provider's code.after fixing the key or bucket
429rate_limitedToo many requests that start work; wait for Retry-After.yes
500tool_failedA PDF tool, or the validation of a PDF/UA file, failed inside Formfeed.once
500pdfa_conversion_failedThe PDF could not be made a valid PDF/A-3b, which an e-invoice has to be.no
500worker_exhaustedA failure on Formfeed's side that the retries did not fix, or an unexpected error.yes
501not_implementedReserved for a documented feature that is not available yet; nothing answers with it today.no
503region_unavailableThe region has no free render slot right now, or no render service at all.after Retry-After
503office_unavailableThe office converter is busy or not reachable.yes
503conformance_unavailableThe service that converts and validates e-invoices and validates PDF/UA files is busy or not reachable.yes
504render_timeoutA step of the render took longer than its limit.only if the cause passed

New codes can be added within /v1. Treat an unknown code by its HTTP status.