REST API
Document requests API
Full reference for Doclift's Document Requests API: synchronous and asynchronous generation, the payload, statuses, validation errors and limits.
The document request object
- id: unique identifier
- tag: free-form string you send, echoed back
- type:
synchroneorasynchrone(spelled as shown, not the English "synchronous"/"asynchronous"); the value you send at creation, echoed back unchanged - sandbox_mode: set once at creation from the calling key's environment
- status:
in_progress,success, orerror(the request's overall outcome) - documents_generations: present on the list endpoint, on the show endpoint, and on a synchronous create response; absent from an asynchronous create response, since nothing has rendered yet when it answers; each entry has
id,tag,generated_at,created_at,generation_status(created,in_progress,success, orerror), andfile(filename,url,size)
template_id accepts a custom, fillable_form, or workflow template,
as long as it is published. The payload shape is identical for all three.
Workflow templates carry one additional contract on top of everything
below. See Workflow templates.
Synchronous or asynchronous
type: "synchrone" renders exactly one document and answers
200 OK with that document already attached: id, tag, type,
timestamp, and documents_generations. There is nothing else to
implement: no webhook to expose, no URL to configure, no second code
path. This is the mode to pick when a person is waiting for their
document on screen.
type: "asynchrone" accepts one or more entries in
document_generations, queues the lot, and answers immediately with
id, tag, type, sandbox_mode, and status: "in_progress": no
documents_generations key at all, since nothing has rendered yet.
The finished documents arrive later as a webhook. See
Webhooks. This is the batch mode, and the only one that
accepts several documents in a single request.
Asynchronous generation requires a webhook URL configured on the calling external application; without one, the request is refused before anything is created:
{
"error": "Please add a webhook URL to this external application to use asynchronous generation. You can add one from your Doclift.io account."
}
One document per synchronous request
A synchronous request carrying more than one entry in
document_generations is refused with 422. The documents of one
request render one after another: a batch would keep the caller waiting
for the sum of its renders. For several documents, use asynchronous.
How long to wait
A synchronous answer arrives within seconds in the vast majority of cases. The API imposes no maximum, but your own network often does: many connections are cut once they have stayed silent for about a minute, and neither you nor Doclift can do anything about it.
A request whose answer never reached you is not lost. See When the answer does not reach you.
Sandbox forces priority to low
priority (critical, default, or low, default critical) sets how
this request ranks against others waiting to generate. In sandbox mode
it is always forced to low, whatever value you send.
Create a document request
Asynchronously
curl https://app.doclift.io/api/v1/document_requests \
--request POST \
--header "X-Api-Key: <your-api-key>" \
--header "Content-Type: application/json" \
--data '{
"document_request": {
"type": "asynchrone",
"document_generations": [
{
"template_id": 100013,
"tag": "Invoice#42",
"variables": {
"client_name": "John Doe"
}
}
],
"tag": "Batch#1"
}
}'
{
"id": 100004,
"tag": "Batch#1",
"type": "asynchrone",
"sandbox_mode": false,
"status": "in_progress"
}
Synchronously
curl https://app.doclift.io/api/v1/document_requests \
--request POST \
--header "X-Api-Key: <your-api-key>" \
--header "Content-Type: application/json" \
--data '{
"document_request": {
"type": "synchrone",
"document_generations": [
{
"template_id": 100013,
"tag": "Invoice#42",
"variables": {
"client_name": "John Doe"
}
}
],
"tag": "Invoice#42"
}
}'
{
"id": 100005,
"tag": "Invoice#42",
"type": "synchrone",
"timestamp": "2026-09-20T15:23:24+02:00",
"documents_generations": [
{
"id": 100231,
"tag": "Invoice#42",
"generated_at": "2026-09-20T15:23:24+02:00",
"created_at": "2026-09-20T15:23:23+02:00",
"generation_status": "success",
"file": {
"filename": "Invoice-1615737313.pdf",
"url": "https://doclift-production.s3.eu-west-1.amazonaws.com/d207f8fe.pdf?[...]",
"size": 189241
}
}
]
}
The file URL is valid for 2 hours. After that, read the request back through View a document request to get a fresh one.
The fields
| Field | Type | Required | Notes |
|---|---|---|---|
| type | string | yes | synchrone or asynchrone |
| priority | string | no | critical, default, or low; forced to low in sandbox |
| tag | string | no | default ""; the only identifier you choose yourself, and the one you can look a request up by |
| document_generations | array | yes | exactly one entry for synchrone, one or more for asynchrone |
| document_generations[].template_id | integer | yes | must be published |
| document_generations[].tag | string | no | default "" |
| document_generations[].variables | object or null | no | flat key/value map (see Templates) |
There is no idempotency key: submitting the same body twice creates two independent document requests, each reporting its own status.
Validation errors
Each of the following is checked in order; the first failure short-circuits the rest and answers before any document is generated.
| Failure | Status | Notes |
|---|---|---|
| type | 422 | "The requested generation type is invalid. Accepted values: synchrone / asynchrone" |
| priority | 422 | {"error": "..."}, names the accepted values |
| A synchronous request carrying more than one entry | 422 | "You can only generate a single document with this route." |
| webhook_url | 422 | see Synchronous or asynchronous |
| document_generations | 422 | "The following required parameters are missing: document_generations" |
| template_id | 422 | "The following required parameters are missing: template_id" |
| template_id | 422 | names the id; a wrong id and someone else's id answer identically |
| content | 422 | |
| Rendered content exceeds the organization's size limit | 422 | see Limits |
| variables | 422 | |
| document_request | 400 | {"error": "..."}, names the missing parameter when applicable |
Missing/invalid/disabled API key answers 403 on every variant, as
elsewhere in the API. See
Response codes.
The messages above are the English ones, served when you send
Accept-Language: en. Without that header the API answers in French. See
Language of the answers.
Variable validation errors
On a fillable_form template only, each provided value is checked against
the auto-detected allowed_values of its variable (see
Fillable forms). A value outside the allowed list
answers 422 naming every offending field:
{
"error": "Les valeurs fournies pour certaines variables ne sont pas valides (status: 'bad_value' (valeurs autorisées : approved, rejected)).",
"invalid_variables": [
{ "field": "status", "value": "bad_value", "allowed_values": ["approved", "rejected"] }
]
}
custom and workflow templates are not checked against allowed_values
at generation time by this step.
If generation fails for any other reason once the payload is otherwise
valid, you observe it
through GET /api/v1/document_requests/:id's generation_status/
generation_error, or through the webhook payload's
generation_status: "error".
When the answer does not reach you
This section concerns the synchronous mode only: asynchronously, the answer is immediate and the result arrives by webhook.
Three situations leave you without a usable answer. They are all handled
the same way, and this is the rule to remember: the tag you sent lets
you find the request again.
curl "https://app.doclift.io/api/v1/document_requests?tag=Invoice%2342" \
--header "X-Api-Key: <your-api-key>"
| Situation | What you get | What to do |
|---|---|---|
| Your connection drops before the answer | nothing: a network error from your own client | look the request up by its tag before replaying it |
| 429 | {"error": "…"} with a Retry-After header | wait the stated delay and replay |
| 502 | {"code": "outcome_unknown", "tag": …, "retrieve": …} | the request may well have landed: check by its tag before replaying |
| 504 | {"code": "generation_pending", "id": …, "tag": …, "retrieve": …} | generation is still running; read the request back at the given address |
Do not replay without checking
There is no idempotency key. A replayed request creates a second,
independent one, and bills you a second document. A 502 and a dropped
connection both mean "I do not know", not "it failed".
Too many generations pending
An organization may only have so many synchronous requests waiting to be processed at once. Beyond that, further ones are refused straight away rather than left to wait:
{
"error": "Too many synchronous generations are already pending for your organization. Try again shortly."
}
A slot frees as soon as a generation finishes, rather than at the end of
a time window: a Retry-After of one second is enough in almost every
case. This ceiling does not apply to asynchronous generation. See
Limits.
Workflow templates
Workflow templates are generated through this same endpoint. There is no separate workflow generation route. Workflows are a private beta, available on request by writing to [email protected]; see Workflows for the full picture.
On top of everything above, a workflow's variables payload carries a
required-variable contract that no other category enforces:
{
"template_id": 100050,
"variables": {
"investor_type": "natural",
"country": "FR",
"investments": [{ "product": "SCPI", "amount": "10 000 €" }]
},
"tag": ""
}
| Failure | Trigger |
|---|---|
| Missing required variable | a variable declared required has no key at all in the payload |
| Malformed collection | a collection-type variable's value is not an array of flat objects |
| Non-scalar value | a non-collection variable received an array or object |
| Oversized collection | a collection carries more than 200 rows |
| Missing required row field | a required field of a collection row has no key in that row |
| Excludes every section | the payload's conditions leave nothing to render |
| Prints nothing | sections survive the conditions but assemble to blank content |
A key present with an empty string or null counts as answered. Only the
absence of the key triggers the missing-variable check. All applicable
failures across the whole batch are reported together in one 422 {"error": "..."}, naming every affected template and variable at once
(useful when a batch mixes several workflow generations).
allowed_values is not checked for workflow variables at generation time.
List document requests
curl https://app.doclift.io/api/v1/document_requests \
--header "X-Api-Key: <your-api-key>"
[
{
"id": 100004,
"tag": "Batch#1",
"type": "asynchrone",
"external_application": {
"id": 100000,
"name": "Production key",
"environment": "sandbox",
"active": true
},
"documents_generations": [
{
"id": 100230,
"tag": "Invoice#42",
"generated_at": "2024-01-15T10:35:02+01:00",
"created_at": "2024-01-15T10:35:00+01:00",
"generation_status": "success",
"file": {
"filename": "Invoice-1615737313.pdf",
"url": "https://doclift.s3.eu-west-1.amazonaws.com/d207f8fe.pdf?[...]",
"size": 40290
}
}
]
}
]
Lists your organization's document requests, newest first. This follows from the organization-wide association, so requests created through other external applications under the same organization appear too, not only the calling key's. Paginated at 30 per page; see Pagination.
Filter by tag
The tag parameter narrows the list to requests carrying exactly that
tag. It is the only identifier you choose yourself and know in advance,
which makes it the one you use to find a request whose answer never
reached you.
curl "https://app.doclift.io/api/v1/document_requests?tag=Invoice%2342" \
--header "X-Api-Key: <your-api-key>"
The match is exact, not partial. An unknown tag answers 200 with an
empty array, never 404. Nothing requires a tag to be unique: if you
have reused one, the list carries every request sharing it.
| Status | Body | When |
|---|---|---|
| 200 | array | always, including an empty account |
| 403 | {"error": "..."} | missing, unknown, or disabled API key |
View a document request
curl https://app.doclift.io/api/v1/document_requests/100004 \
--header "X-Api-Key: <your-api-key>"
{
"id": 100004,
"tag": "Batch#1",
"type": "asynchrone",
"external_application": {
"id": 100000,
"name": "Production key",
"environment": "sandbox",
"active": true
},
"documents_generations": [
{
"id": 100230,
"tag": "Invoice#42",
"generated_at": "2024-01-15T10:35:02+01:00",
"created_at": "2024-01-15T10:35:00+01:00",
"generation_status": "success",
"generation_duration": 1721,
"generation_error": null,
"sent_payload": { "client_name": "John Doe" },
"file": {
"filename": "Invoice-1615737313.pdf",
"url": "https://doclift.s3.eu-west-1.amazonaws.com/d207f8fe.pdf?[...]",
"size": 40290
},
"template": {
"id": 100013,
"title": "Invoice",
"description": "A template for invoices"
}
}
]
}
This is the richer view: sent_payload (exactly what you sent),
generation_duration, generation_error, and the source template are
only present here, never in the list or in the create response. File URLs
are valid for 20 hours in this view, against 2 hours everywhere else.
| Status | Body | When |
|---|---|---|
| 200 | document request, show view | belongs to your organization |
| 404 | {"error": "Record not found"} | unknown id or another organization's |
| 403 | {"error": "..."} | missing, unknown, or disabled API key |
Limits
| Limit | Value | Scope |
|---|---|---|
| Rendered content size | 800 ,000 characters by default, configurable per organization | custom and workflow templates; fillable_form is never measured |
| Collection rows (workflow) | 200 | per collection variable, per generation |
| Documents per request | 1 for synchrone, unlimited for asynchrone | see Synchronous or asynchronous |
| Synchronous requests pending at once | per organization, adjustable on request | beyond it: 429 with Retry-After |
| Webhook delivery retries | configurable per organization | see Webhooks |
There is no request-rate limiting on this API: nothing counts your requests per minute or per day. The only ceiling is how many synchronous requests your organization may have waiting to be processed at once, described above. It frees at the pace of the generations that finish, and does not apply to asynchronous generation. See Environments.