Document API
The document API fills a template with data you send as JSON (from an ERP, a web form, a script) with no record behind it. You get a link to the finished document back, in the template’s output format or the one you ask for.
It works with every kind of template. The template’s own data source is simply not used: the JSON you send is the record.
Authentication
Section titled “Authentication”Send an API key in the X-Webhook-API-Key header. Two kinds of key work:
- The template’s own key: on the Template tab, under Document API (the same key as the HTTP trigger). It opens that one template.
- A company API key: created by an admin in the Company Portal (in a Lark company, a Lark app admin), under Company → API keys. It opens every template of your company. It is shown once when it is created; store it like a password. Approval templates have no key of their own, so use a company key for them.
Revoking a company key, or resetting a template’s key, stops it working at once.
Create documents
Section titled “Create documents”POST https://api.automadoc.cc/api/v1/templates/{templateId}/documentsThe template id is in the endpoint shown on the template’s Document API card.
| Field | Required | What it does |
|---|---|---|
data |
yes | The record: its keys are the placeholders. A list of objects makes one document per item; for a template that makes one document from many records, it makes one document from all of them (see Several documents at once). |
format |
no | "pdf", "png" (the first page as an image) or "native" (the template’s own file, e.g. .docx). Defaults to the template’s setting. |
filename |
no | A filename pattern for this request, with {Field} placeholders, in place of the template’s. |
recordId |
no | What {Record_ID} prints, for a single document. Defaults to the document’s id. |
mode |
no | "separate", "list", "continuous" or "labels": what the template makes, sent as a check. Left out, the template decides (see Several documents at once). |
Add ?wait=20 (up to 25 seconds) to hold the response until the document is ready, so one request is enough. Without it, the response comes at once and you poll the status URL.
Workflow tools that follow the 202 + Location polling convention (Power Automate, Logic Apps and similar) can add ?async=true instead: while the document is being made, the 202 response carries a Location header to poll and a Retry-After header (5 seconds). Polling that location answers 202 until the document is completed (200) or failed (422, with "code": "DOCUMENT_FAILED" and the reason in error). A document that has already failed when the request is answered gets that 422 straight away. The status endpoint takes ?async=true too. async follows one document, so it cannot be used with a list in data for a template that makes a document per item (INVALID_ASYNC). The Power Automate connector uses it.
Response: 202 Accepted while the document is being made, 200 OK when wait saw it finish:
{ "generationId": "5d2f0c1e-6a1b-4c7e-9f0a-2b8c3d4e5f60", "templateId": "9b1c…", "status": "processing", "statusUrl": "https://api.automadoc.cc/api/v1/documents/5d2f0c1e-6a1b-4c7e-9f0a-2b8c3d4e5f60", "createdAt": 1791878400000}Get a document
Section titled “Get a document”GET https://api.automadoc.cc/api/v1/documents/{generationId}Use the same key as for the request. status is processing, completed or failed:
{ "generationId": "5d2f0c1e-6a1b-4c7e-9f0a-2b8c3d4e5f60", "status": "completed", "filename": "Invoice-INV-2026-001.pdf", "downloadUrl": "https://api.automadoc.cc/api/generations/…/download?expires=…&signature=…", "downloadUrlExpiresAt": 1792137600000}The downloadUrl needs no key. It stays valid as long as your company keeps generated documents (set by your plan), or 30 days if they are kept indefinitely, and stops working when the document is deleted. Each read of the status gives a fresh link. A failed document has an error with the reason. A completed document may have warnings: what was left out of it, such as a picture whose URL could not be fetched, named by its field and the URL’s host. Placeholders your data had no value for are listed together in one line (“3 fields had no value in the data: …”) after the other warnings.
This endpoint also reports documents started by the HTTP trigger: its response lists their generationIds.
A key only sees documents of the templates it opens. Documents generated by hand in the extension are not visible through the API.
Download the file with the key
Section titled “Download the file with the key”GET https://api.automadoc.cc/api/v1/documents/{generationId}/fileThe finished file itself, with the same key, for a tool that takes a file rather than a link. It answers 409 with DOCUMENT_NOT_READY while the document is being made, 422 with DOCUMENT_FAILED and the reason in error if it failed, and 404 with DOCUMENT_FILE_MISSING once your company’s retention has removed the file. Content-Type and Content-Disposition (the file name) describe it.
List templates and their data
Section titled “List templates and their data”GET https://api.automadoc.cc/api/v1/templatesGET https://api.automadoc.cc/api/v1/templates/{templateId}The first lists every template of your company: id, name, source (base or approval), templateFormat, mode (what a request makes, as in Several documents at once), outputFormat and ready (false while a template has no file, or has errors to fix). It needs a company API key; a template’s own key is refused with COMPANY_KEY_REQUIRED.
The second describes one template (either key works) and adds dataSchema, a JSON schema of the data it takes (one property per placeholder: text, a number or a picture URL, a yes/no for a condition, a list for a loop), sampleData, and documentsUrl. Workflow tools use it to show one input per placeholder.
The full description of these routes is an OpenAPI document at https://api.automadoc.cc/api/v1/openapi.json.
How your data fills the template
Section titled “How your data fills the template”Names. Each key is turned into a placeholder name exactly the way a Base field’s name is: spaces, dots and other symbols become _, letters and digits of any language are kept, and case matters.
| Key you send | Placeholder it fills |
|---|---|
"Customer" |
{Customer} |
"Invoice No." |
{Invoice_No_} |
"Unit price" |
{Unit_price} |
"客户名称" |
{客户名称} |
Two keys of the same object that become the same name, such as "Unit price" and "Unit-price", are refused with DATA_NAME_COLLISION rather than one silently winning.
Values.
- Text and numbers print as sent. Numbers stay numbers, so filters such as
number,currencyandsumwork on them. true/falsework with conditions:{#if Paid}…{/if}.nulland an empty list print nothing, and count as empty in{#if}.- An object fills dotted placeholders:
{"Customer": {"Name": "Acme"}}fills{Customer.Name}. - A list of objects is a loop:
{"Items": [{"Product": "Pen", "Qty": 2}]}fills{#Items}{Product} × {Qty}{/Items}. - A list of plain values prints joined with commas:
["red", "blue"]printsred, blue. - Pictures: where the template has a picture placeholder (a picture whose Alt Text is
{Logo}, also inside a loop row), send anhttps://URL, or a list of URLs for{Photos[1]},{Photos[2]}. The URL must be reachable from the internet. Anywhere else a URL is just text. - Attachments:
{ATTACH:Field}adds the files of a Base attachment field or an approval form only. The document API cannot send files for it: it adds nothing, and URLs sent for it print as text. {QR:Field}and{BARCODE:Field}print a code of the text you send.
Record_ID, Template_Name and Print_Time are set by AutomaDoc; a key of that name is replaced (the response warns you).
For an approval template, the variables listed on its page are the names to send. Its default filename names the approval request, which JSON does not have, so documents made through the API are named {Template_Name}-{Record_ID} unless you send filename.
Several documents at once
Section titled “Several documents at once”Send data as a list of up to 50 objects to make one document per item. The response lists them:
{ "documents": [ { "generationId": "…", "status": "processing", "statusUrl": "…" }, … ] }If any item is refused, nothing is made. recordId cannot be used with a list.
One document from all the items
Section titled “One document from all the items”If the template is set to make one document from many records (a list, a page per record, or labels), a list in data makes one document instead: each item is a record of its {#Records} loop, a page or a label, in the order sent. Up to 500 items; a single object makes a one-item document. The response is that one document, with recordCount:
{ "generationId": "…", "status": "processing", "statusUrl": "…", "recordCount": 120 }{Record_ID}prints each item’s position,1to the number of items.- The filename pattern reads the first item, plus
{RecordCount}and{Date}. Without one, the document is named{Template_Name}-{Date}. "format": "png"makes a PDF: a picture would hold the first page only.- It counts as one document. Against the company’s 600 documents a minute it counts one per item for a page per record, and one per ten items for a list or labels.
- It cannot be sent for signature (
ESIGN_COMBINED).
The template’s Records per document setting decides. Send mode: "separate" for one document per item, or "list", "continuous" (a page per record) or "labels". A template set otherwise refuses the request with MODE_MISMATCH, rather than return a different number of files than your script expects.
Example: curl
Section titled “Example: curl”curl -X POST 'https://api.automadoc.cc/api/v1/templates/TEMPLATE_ID/documents?wait=20' \ -H 'X-Webhook-API-Key: YOUR_API_KEY' \ -H 'Content-Type: application/json' \ -d '{ "data": { "Invoice No.": "INV-2026-001", "Customer": { "Name": "Acme Corp", "Address": "1 Main St" }, "Items": [ { "Product": "Consulting", "Qty": 10, "Price": 120 }, { "Product": "Travel", "Qty": 1, "Price": 340 } ], "Logo": "https://example.com/logo.png" }, "format": "pdf" }'The template’s Document API card copies a request like this one, built from the template’s own placeholders.
Example: Node.js
Section titled “Example: Node.js”const API = 'https://api.automadoc.cc'const headers = { 'X-Webhook-API-Key': process.env.AUTOMADOC_KEY, 'Content-Type': 'application/json' }
async function generate(templateId, data) { const res = await fetch(`${API}/api/v1/templates/${templateId}/documents?wait=20`, { method: 'POST', headers, body: JSON.stringify({ data, format: 'pdf' }), }) let doc = await res.json() if (!res.ok) throw new Error(`${doc.code}: ${doc.error}`)
// Still processing after the wait: poll the status URL. while (doc.status === 'processing') { await new Promise((r) => setTimeout(r, 2000)) doc = await (await fetch(doc.statusUrl, { headers })).json() } if (doc.status === 'failed') throw new Error(doc.error) return doc.downloadUrl}Send the document for signature
Section titled “Send the document for signature”Add a sign block to send the document out for signature as soon as it is generated. It costs nothing beyond the document.
sign works on a Lark Base template: a Word template with signature fields (see E-signature) that makes one document per record, with data sent as one object. Every other template refuses it with SIGN_BASE_TEMPLATES_ONLY: Excel, SharePoint list, Dataverse, Google Sheets and API-only templates, and Lark Approval templates, which are signed in the approval itself. Only a Lark account has Lark Base templates, so in a Microsoft, Google or email account no template takes sign.
{ "data": { "Customer": "Acme Corp" }, "sign": { "signers": { "Customer": { "name": "Budi Santoso", "contact": "budi@acme.example" }, "Supplier": { "larkUserId": "ou_1234…" } }, "order": "sequential" }}| Field | What it does |
|---|---|
signers |
One signer per signature role of the template, by role name. Every role needs one: there is no record to read a signer from. Someone outside your company is { "name", "contact"? }: a name, and optionally a contact note kept with the request, such as an email address (nothing is sent to it). A colleague is { "larkUserId" }: their Lark open_id, for a person of your Lark company who uses AutomaDoc. |
order |
"parallel" (everyone at once) or "sequential" (in the template’s order). Defaults to the template’s setting. |
generatePasswords |
A generated password for each outside signer. Default true. |
locale |
Language of the bot messages and the audit page, e.g. "en". |
The request starts when the document is ready. Colleagues get a message from the AutomaDoc bot in Lark. Outside signers’ links, and their generated passwords, are returned once, by the first GET /api/v1/documents/{generationId} after the request has started:
{ "status": "completed", "signing": { "status": "started", "requestId": "…", "links": [{ "role": "Customer", "displayName": "Budi Santoso", "url": "https://…/sign/…", "password": "K7mQ…" }] }}Send them on yourself, the password separately if you can. Later reads show the same entries with url and password set to null and "linksRedacted": true. If a link is lost, issue a new one from the Signatures page. signing.status is queued until the document is done, briefly starting while the request opens, then started or failed (with an error code).
sign is refused with a list in data (SIGN_SINGLE_DOCUMENT) and on a template that makes one document from many records (ESIGN_COMBINED).
When the document is ready
Section titled “When the document is ready”If the template has a webhook callback, it is called for API documents too, with the download link. This needs Pro (or a trial) or a Business seat, as for every automated document.
Errors
Section titled “Errors”Every refusal has a code and an English error:
| Status | code |
Why |
|---|---|---|
| 400 | INVALID_JSON, DATA_REQUIRED, DATA_NOT_OBJECT |
The body is not JSON, has no data, or data is not an object or a list of objects |
| 400 | DATA_NAME_COLLISION, DATA_INVALID_NAME |
Two keys fill the same placeholder; a key has no letter or digit |
| 400 | DATA_MIXED_ARRAY |
A list mixes objects and plain values |
| 400 | DATA_TOO_DEEP, DATA_ARRAY_TOO_LONG, DATA_TOO_MANY_KEYS, DATA_STRING_TOO_LONG |
Over a limit |
| 400 | IMAGE_URL_NOT_ALLOWED, TOO_MANY_IMAGES |
A picture URL is not a public http(s) address; too many pictures |
| 400 | BATCH_EMPTY, BATCH_TOO_LARGE |
An empty list; more than 50 items (500 for a template that makes one document from many records) |
| 400 | INVALID_FORMAT, INVALID_FILENAME, INVALID_RECORD_ID, INVALID_WAIT, INVALID_MODE, INVALID_ASYNC |
An option is not valid |
| 400 | TEMPLATE_VALIDATION_ERRORS |
The template file has errors to fix first (listed in validationErrors) |
| 400 | MODE_MISMATCH |
mode is not what the template makes |
| 401 | API_KEY_MISSING |
No X-Webhook-API-Key header |
| 403 | API_KEY_INVALID |
The key is wrong, revoked, or does not open this template. It is also the answer for a template id that does not exist |
| 403 | QUOTA_EXCEEDED |
No documents left this month; buy a pack or a seat |
| 403 | TEMPLATE_DISABLED |
The template is above your plan’s template limit |
| 403 | COMPANY_KEY_REQUIRED |
Listing templates needs a company API key, not a template’s own key |
| 404 | TEMPLATE_NOT_FOUND, DOCUMENT_NOT_FOUND |
No such template or document for this key |
| 404 | DOCUMENT_FILE_MISSING |
The document’s file has been removed by your company’s retention |
| 409 | TEMPLATE_FILE_MISSING, TEMPLATE_NO_OWNER |
The template has no file yet; it has no owner (open and save it once, or use a company key) |
| 409 | DOCUMENT_NOT_READY |
Downloading the file of a document still being made |
| 413 | BODY_TOO_LARGE |
The body is over 1 MB |
| 422 | DOCUMENT_FAILED |
Downloading the file of a document that failed, or a request or status read with async=true for one; error says why |
| 429 | RATE_LIMITED |
More than 60 requests a minute for one template, 600 documents a minute for one company, 60 status reads or 60 downloads a minute for one document, or 60 template lists a minute for one company. Documents from the HTTP trigger count toward the company’s 600; its requests have their own 300 a minute per template |
| 400 | SIGN_INVALID, SIGN_SINGLE_DOCUMENT, SIGN_BASE_TEMPLATES_ONLY |
The sign block is not valid, data is a list, or the template is not a Lark Base template, so its documents cannot be sent for signature |
| 400, 409 | ESIGN_… |
Refused by e-signature: ESIGN_ROLES_MISMATCH (a role without a signer, or an unknown one), ESIGN_SIGNER_FIELD (a signer read from a field), ESIGN_INVALID_SIGNER (a larkUserId that is not a user of your company), ESIGN_NO_MASTER (the template has no signature fields), ESIGN_COMBINED (the template makes one document from many records), ESIGN_RECORD_UNREADABLE (the template has no owner to send the request as) |
| 502 | WORKER_UNAVAILABLE |
The document service could not be reached; try again shortly |
| 503 | TEMPLATE_FORMAT_UNAVAILABLE |
The template is an Excel or PowerPoint file the document service cannot open yet; try again later |
| 503 | COMBINED_UNAVAILABLE |
One document from many records cannot be made right now; try again later |
| 503 | ATTACHMENTS_UNAVAILABLE |
The template makes one document from many records and adds attachments to the PDF, which cannot be done right now; try again later |
Limits
Section titled “Limits”| Request body | 1 MB |
| Documents per request | 50, or 500 items in one document from many records |
| Nesting of objects and lists | 8 levels |
| Items in a list | 1,000 |
| Keys in an object | 500 |
| Characters in a value | 100,000 |
| Pictures per document | 50; 500 across all the items of one document from many records |
wait |
25 seconds |
| Requests per template | 60 a minute; the HTTP trigger has its own 300 a minute |
| Status reads per document | 60 a minute; file downloads have their own 60 |
| Template list | 60 a minute per company; one template’s description 60 a minute |
| Documents per company | 600 a minute, the document API and the HTTP trigger together |