Create Document via API
curl --request POST \
--url https://api.docsautomator.co/createDocument \
--header 'Content-Type: application/json' \
--data '
{
"automationId": "<string>",
"data": {},
"approverEmails": [
"<string>"
],
"documentName": "<string>",
"async": true,
"idempotencyKey": "<string>",
"webhookParams": {},
"skipEsign": true,
"recipients": [
{}
],
"actingUserEmail": "<string>",
"existingPdfs": [
"<string>"
],
"docTemplateLink": "<string>",
"wordTemplateLink": "<string>",
"wordTemplateBase64": "<string>",
"pdfTemplateLink": "<string>",
"pdfTemplateBase64": "<string>"
}
'import requests
url = "https://api.docsautomator.co/createDocument"
payload = {
"automationId": "<string>",
"data": {},
"approverEmails": ["<string>"],
"documentName": "<string>",
"async": True,
"idempotencyKey": "<string>",
"webhookParams": {},
"skipEsign": True,
"recipients": [{}],
"actingUserEmail": "<string>",
"existingPdfs": ["<string>"],
"docTemplateLink": "<string>",
"wordTemplateLink": "<string>",
"wordTemplateBase64": "<string>",
"pdfTemplateLink": "<string>",
"pdfTemplateBase64": "<string>"
}
headers = {"Content-Type": "application/json"}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
automationId: '<string>',
data: {},
approverEmails: ['<string>'],
documentName: '<string>',
async: true,
idempotencyKey: '<string>',
webhookParams: {},
skipEsign: true,
recipients: [{}],
actingUserEmail: '<string>',
existingPdfs: ['<string>'],
docTemplateLink: '<string>',
wordTemplateLink: '<string>',
wordTemplateBase64: '<string>',
pdfTemplateLink: '<string>',
pdfTemplateBase64: '<string>'
})
};
fetch('https://api.docsautomator.co/createDocument', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.docsautomator.co/createDocument",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'automationId' => '<string>',
'data' => [
],
'approverEmails' => [
'<string>'
],
'documentName' => '<string>',
'async' => true,
'idempotencyKey' => '<string>',
'webhookParams' => [
],
'skipEsign' => true,
'recipients' => [
[
]
],
'actingUserEmail' => '<string>',
'existingPdfs' => [
'<string>'
],
'docTemplateLink' => '<string>',
'wordTemplateLink' => '<string>',
'wordTemplateBase64' => '<string>',
'pdfTemplateLink' => '<string>',
'pdfTemplateBase64' => '<string>'
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.docsautomator.co/createDocument"
payload := strings.NewReader("{\n \"automationId\": \"<string>\",\n \"data\": {},\n \"approverEmails\": [\n \"<string>\"\n ],\n \"documentName\": \"<string>\",\n \"async\": true,\n \"idempotencyKey\": \"<string>\",\n \"webhookParams\": {},\n \"skipEsign\": true,\n \"recipients\": [\n {}\n ],\n \"actingUserEmail\": \"<string>\",\n \"existingPdfs\": [\n \"<string>\"\n ],\n \"docTemplateLink\": \"<string>\",\n \"wordTemplateLink\": \"<string>\",\n \"wordTemplateBase64\": \"<string>\",\n \"pdfTemplateLink\": \"<string>\",\n \"pdfTemplateBase64\": \"<string>\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.docsautomator.co/createDocument")
.header("Content-Type", "application/json")
.body("{\n \"automationId\": \"<string>\",\n \"data\": {},\n \"approverEmails\": [\n \"<string>\"\n ],\n \"documentName\": \"<string>\",\n \"async\": true,\n \"idempotencyKey\": \"<string>\",\n \"webhookParams\": {},\n \"skipEsign\": true,\n \"recipients\": [\n {}\n ],\n \"actingUserEmail\": \"<string>\",\n \"existingPdfs\": [\n \"<string>\"\n ],\n \"docTemplateLink\": \"<string>\",\n \"wordTemplateLink\": \"<string>\",\n \"wordTemplateBase64\": \"<string>\",\n \"pdfTemplateLink\": \"<string>\",\n \"pdfTemplateBase64\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.docsautomator.co/createDocument")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Content-Type"] = 'application/json'
request.body = "{\n \"automationId\": \"<string>\",\n \"data\": {},\n \"approverEmails\": [\n \"<string>\"\n ],\n \"documentName\": \"<string>\",\n \"async\": true,\n \"idempotencyKey\": \"<string>\",\n \"webhookParams\": {},\n \"skipEsign\": true,\n \"recipients\": [\n {}\n ],\n \"actingUserEmail\": \"<string>\",\n \"existingPdfs\": [\n \"<string>\"\n ],\n \"docTemplateLink\": \"<string>\",\n \"wordTemplateLink\": \"<string>\",\n \"wordTemplateBase64\": \"<string>\",\n \"pdfTemplateLink\": \"<string>\",\n \"pdfTemplateBase64\": \"<string>\"\n}"
response = http.request(request)
puts response.read_bodyDocuments
Create Document via API
Pass data directly via the API to generate documents
POST
/
createDocument
Create Document via API
curl --request POST \
--url https://api.docsautomator.co/createDocument \
--header 'Content-Type: application/json' \
--data '
{
"automationId": "<string>",
"data": {},
"approverEmails": [
"<string>"
],
"documentName": "<string>",
"async": true,
"idempotencyKey": "<string>",
"webhookParams": {},
"skipEsign": true,
"recipients": [
{}
],
"actingUserEmail": "<string>",
"existingPdfs": [
"<string>"
],
"docTemplateLink": "<string>",
"wordTemplateLink": "<string>",
"wordTemplateBase64": "<string>",
"pdfTemplateLink": "<string>",
"pdfTemplateBase64": "<string>"
}
'import requests
url = "https://api.docsautomator.co/createDocument"
payload = {
"automationId": "<string>",
"data": {},
"approverEmails": ["<string>"],
"documentName": "<string>",
"async": True,
"idempotencyKey": "<string>",
"webhookParams": {},
"skipEsign": True,
"recipients": [{}],
"actingUserEmail": "<string>",
"existingPdfs": ["<string>"],
"docTemplateLink": "<string>",
"wordTemplateLink": "<string>",
"wordTemplateBase64": "<string>",
"pdfTemplateLink": "<string>",
"pdfTemplateBase64": "<string>"
}
headers = {"Content-Type": "application/json"}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
automationId: '<string>',
data: {},
approverEmails: ['<string>'],
documentName: '<string>',
async: true,
idempotencyKey: '<string>',
webhookParams: {},
skipEsign: true,
recipients: [{}],
actingUserEmail: '<string>',
existingPdfs: ['<string>'],
docTemplateLink: '<string>',
wordTemplateLink: '<string>',
wordTemplateBase64: '<string>',
pdfTemplateLink: '<string>',
pdfTemplateBase64: '<string>'
})
};
fetch('https://api.docsautomator.co/createDocument', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.docsautomator.co/createDocument",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'automationId' => '<string>',
'data' => [
],
'approverEmails' => [
'<string>'
],
'documentName' => '<string>',
'async' => true,
'idempotencyKey' => '<string>',
'webhookParams' => [
],
'skipEsign' => true,
'recipients' => [
[
]
],
'actingUserEmail' => '<string>',
'existingPdfs' => [
'<string>'
],
'docTemplateLink' => '<string>',
'wordTemplateLink' => '<string>',
'wordTemplateBase64' => '<string>',
'pdfTemplateLink' => '<string>',
'pdfTemplateBase64' => '<string>'
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.docsautomator.co/createDocument"
payload := strings.NewReader("{\n \"automationId\": \"<string>\",\n \"data\": {},\n \"approverEmails\": [\n \"<string>\"\n ],\n \"documentName\": \"<string>\",\n \"async\": true,\n \"idempotencyKey\": \"<string>\",\n \"webhookParams\": {},\n \"skipEsign\": true,\n \"recipients\": [\n {}\n ],\n \"actingUserEmail\": \"<string>\",\n \"existingPdfs\": [\n \"<string>\"\n ],\n \"docTemplateLink\": \"<string>\",\n \"wordTemplateLink\": \"<string>\",\n \"wordTemplateBase64\": \"<string>\",\n \"pdfTemplateLink\": \"<string>\",\n \"pdfTemplateBase64\": \"<string>\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.docsautomator.co/createDocument")
.header("Content-Type", "application/json")
.body("{\n \"automationId\": \"<string>\",\n \"data\": {},\n \"approverEmails\": [\n \"<string>\"\n ],\n \"documentName\": \"<string>\",\n \"async\": true,\n \"idempotencyKey\": \"<string>\",\n \"webhookParams\": {},\n \"skipEsign\": true,\n \"recipients\": [\n {}\n ],\n \"actingUserEmail\": \"<string>\",\n \"existingPdfs\": [\n \"<string>\"\n ],\n \"docTemplateLink\": \"<string>\",\n \"wordTemplateLink\": \"<string>\",\n \"wordTemplateBase64\": \"<string>\",\n \"pdfTemplateLink\": \"<string>\",\n \"pdfTemplateBase64\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.docsautomator.co/createDocument")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Content-Type"] = 'application/json'
request.body = "{\n \"automationId\": \"<string>\",\n \"data\": {},\n \"approverEmails\": [\n \"<string>\"\n ],\n \"documentName\": \"<string>\",\n \"async\": true,\n \"idempotencyKey\": \"<string>\",\n \"webhookParams\": {},\n \"skipEsign\": true,\n \"recipients\": [\n {}\n ],\n \"actingUserEmail\": \"<string>\",\n \"existingPdfs\": [\n \"<string>\"\n ],\n \"docTemplateLink\": \"<string>\",\n \"wordTemplateLink\": \"<string>\",\n \"wordTemplateBase64\": \"<string>\",\n \"pdfTemplateLink\": \"<string>\",\n \"pdfTemplateBase64\": \"<string>\"\n}"
response = http.request(request)
puts response.read_bodyWhen your automation’s data source is set to API, you pass all field data directly in the request body. This is the most flexible option, as you control exactly what data goes into the document.
To make an image a clickable link, pass it as a
Why sync mode answers 409 while the first request runs: a second request that waits for the first one can hit your own timeout and leave you where you started. The 409 answer comes back at once, names the running job, and the next retry with the same key returns the finished result.
What counts as the same request: we compare the automation id and everything else you send in the body and the query string, in any key order.
If the e-invoice check itself is not reachable, the message is “The e-invoice check is not available right now. Nothing was sent; please try again in a few minutes.” Send the request again later.
Endpoint
POST https://api.docsautomator.co/createDocument
Request Body
string
required
The automation ID. Find it on the automation’s settings page or via
GET /automations. docId is also accepted, so existing integrations keep working. Send only one of the two: a request with different values in automationId and docId fails with HTTP 400.object
required
Key-value pairs for template placeholders. Keys must match the placeholder names in your template (e.g.,
{{customer_name}} in your template → "customer_name" in the data object). Line items are also included inside this object as line_items_1, line_items_2, etc. See Example with Line Items below.string | string[]
Emails of people who must approve this document before it goes out: one email, several separated by commas, semicolons or line breaks, or a list of up to 10. Send it at the top level of the request, not inside
data. The document waits for approval even when approval is off for the automation. Anyone named can join as an approver, from any email domain. When Approvers need an account is off, a person who is not on your team decides from their own email link without an account; see Approve without an account. Fails with an error on a plan without Approvals. See Approvers in an API call.string
Custom name for the generated document. Overrides the automation’s document name setting.
boolean
default:"false"
By default the request waits and returns the document (HTTP 200 with
pdfUrl). When true, it returns immediately with a jobId (HTTP 202) instead. Poll GET /job/{jobId} for the result.string
Your own key for this document, up to 255 characters, for example
"proposal-1042-v2". When you send the same request again with the same key within 7 days, no new document is created and you get the result of the first request. Also accepted as an Idempotency-Key request header; when both are sent, the body value is used. See Safe retries with an idempotency key.object
Custom parameters passed through to webhook notifications under both
webhookParams and additionalParams (identical content; the latter is kept for backward compatibility).boolean
default:"false"
When
true, skips e-signing for this request even if the automation has e-signing enabled. The document is generated and delivered normally (Drive save, email, webhook), but no signing session is started and no signing invitations are sent. Useful for two-step approval flows: generate a draft for review first, then re-run without skipEsign to send the document for signing. Also accepted as a query parameter (?skipEsign=true) on GET requests.object[]
Who signs or accepts this document. Each entry is
{ "signer": 1, "email": "anna@acme.com", "name": "Anna Weber" }. signer is the signer’s number in the automation: for signing it matches the number in the template tags (1 in {{esign.signature_1}}), for acceptance it is the position in the Recipients list. name is optional; without it the name set in the automation is used.A listed signer’s email and name replace the saved ones for this document only. Signers you leave out keep their saved setup, and nothing is saved on the automation. The automation decides how many signers there are, so recipients never adds a signer. You can leave out signer when the automation has exactly one signer. With two or more signers, every entry needs it.In the automation’s Signing & Acceptance settings, a signer’s email can be set to Given with each request. This option is available for API and MCP automations and automations without a data source. Every request must then name that signer in recipients, or it fails with “Signer 1 needs an email: pass it in recipients”.The request fails before anything is generated when a signer number does not exist, the same signer is listed twice, an email is invalid, or the automation has no signing or acceptance step."recipients": [
{ "signer": 1, "email": "anna@acme.com", "name": "Anna Weber" },
{ "signer": 2, "email": "cfo@acme.com" }
]
string
Email address of the team member generating this document. When the automation’s email delivery (or e-signature email settings) has Send from the team member who generates the document enabled, the email is sent from this member’s own connected Gmail or Outlook account instead of the configured sender. The address must match a mailbox connected to the workspace; otherwise DocsAutomator falls back to the automation’s configured sender. Also accepted as a query parameter (
?actingUserEmail=...) on GET requests or as an X-Acting-User-Email request header (the header takes precedence). The in-app Generate button passes this automatically.string[]
Array of publicly accessible PDF URLs to merge or prepend to the generated document.
string
Override the automation’s Google Doc template URL for this request only. Pass a full Google Docs URL or just the document ID.
string
Word templates only. Use a different Word file for this request only: an https link to a .docx file that our servers can download. The data, mapping, signing and output settings still come from the automation in
docId. The file can be up to 25 MB. Links to private or internal network addresses are refused.string
Word templates only. The same as
wordTemplateLink, but you send the .docx file itself, encoded as base64, in the JSON body. Use this when you cannot host the file. Up to 25 MB before encoding. Send either wordTemplateLink or wordTemplateBase64, not both.string
PDF templates only. Use a different PDF as the background for this request only: an https link to a PDF file that our servers can download. The automation’s saved fields are placed on this PDF at their saved positions, for example to fill in a check or a form that another tool created. The data, mapping, signing and output settings still come from the automation in
docId. The file can be up to 25 MB. Links to private or internal network addresses are refused, and so are password-protected PDFs.The PDF must have every page that holds a saved field. If a field is on a page the PDF does not have, the request fails and the error names the fields and pages. If a page has a different size from the page in the saved template, each field keeps its distance from the top-left corner of the page and is not scaled. The response then lists the affected pages in warnings.string
PDF templates only. The same as
pdfTemplateLink, but you send the PDF itself, encoded as base64, in the JSON body. Use this when you cannot host the file. Up to 25 MB before encoding. Send either pdfTemplateLink or pdfTemplateBase64, not both.Basic Example
curl -X POST https://api.docsautomator.co/createDocument \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"automationId": "YOUR_AUTOMATION_ID",
"data": {
"customer_name": "Acme Corp",
"invoice_number": "INV-2025-001",
"invoice_date": "2025-01-15",
"due_date": "2025-02-15",
"total": "$5,500.00"
}
}'
const response = await fetch("https://api.docsautomator.co/createDocument", {
method: "POST",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
automationId: "YOUR_AUTOMATION_ID",
data: {
customer_name: "Acme Corp",
invoice_number: "INV-2025-001",
invoice_date: "2025-01-15",
due_date: "2025-02-15",
total: "$5,500.00",
},
}),
});
const result = await response.json();
console.log(result.pdfUrl);
import requests
response = requests.post(
"https://api.docsautomator.co/createDocument",
headers={
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
json={
"automationId": "YOUR_AUTOMATION_ID",
"data": {
"customer_name": "Acme Corp",
"invoice_number": "INV-2025-001",
"invoice_date": "2025-01-15",
"due_date": "2025-02-15",
"total": "$5,500.00",
},
},
)
result = response.json()
print(result["pdfUrl"])
Example with Line Items
Pass line items as arrays inside thedata object. Use line_items_1 for the first group, line_items_2 for a second group, and so on. There is no limit on the number of line item groups. Each object’s keys must match the line item placeholder names in your template.
curl -X POST https://api.docsautomator.co/createDocument \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"automationId": "YOUR_AUTOMATION_ID",
"data": {
"customer_name": "Acme Corp",
"invoice_date": "2025-01-15",
"subtotal": "$5,500.00",
"tax": "$495.00",
"total": "$5,995.00",
"line_items_1": [
{ "description": "Consulting", "quantity": 10, "rate": "$150", "amount": "$1,500" },
{ "description": "Development", "quantity": 20, "rate": "$200", "amount": "$4,000" }
]
}
}'
const response = await fetch("https://api.docsautomator.co/createDocument", {
method: "POST",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
automationId: "YOUR_AUTOMATION_ID",
data: {
customer_name: "Acme Corp",
invoice_date: "2025-01-15",
subtotal: "$5,500.00",
tax: "$495.00",
total: "$5,995.00",
line_items_1: [
{ description: "Consulting", quantity: 10, rate: "$150", amount: "$1,500" },
{ description: "Development", quantity: 20, rate: "$200", amount: "$4,000" },
],
},
}),
});
const result = await response.json();
console.log(result.pdfUrl);
import requests
response = requests.post(
"https://api.docsautomator.co/createDocument",
headers={
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
json={
"automationId": "YOUR_AUTOMATION_ID",
"data": {
"customer_name": "Acme Corp",
"invoice_date": "2025-01-15",
"subtotal": "$5,500.00",
"tax": "$495.00",
"total": "$5,995.00",
"line_items_1": [
{"description": "Consulting", "quantity": 10, "rate": "$150", "amount": "$1,500"},
{"description": "Development", "quantity": 20, "rate": "$200", "amount": "$4,000"},
],
},
},
)
result = response.json()
print(result["pdfUrl"])
Nested Line Items
Use thechildren key for nesting (up to 2 levels):
{
"automationId": "YOUR_AUTOMATION_ID",
"data": {
"project_name": "Website Redesign",
"line_items_1": [
{
"phase": "Design",
"children": [
{
"task": "Wireframes",
"hours": 8,
"children": [
{ "detail": "Homepage wireframe", "status": "Complete" }
]
}
]
}
]
}
}
Learn more about setting up line items in your template in the Line Items guide.
Images
Any placeholder whose name containsimage is filled with a picture. Pass a publicly accessible URL as the value (a single URL, or an array for a multi-image placeholder):
{
"automationId": "YOUR_AUTOMATION_ID",
"data": {
"image_logo": "https://example.com/logo.png",
"image_gallery": [
"https://example.com/a.jpg",
"https://example.com/b.jpg"
]
}
}
{ "url", "link" } object instead of a plain URL string. No automation setup is required, and the link survives PDF export (Google Doc and Word templates). Arrays work too, with a link per image:
{
"automationId": "YOUR_AUTOMATION_ID",
"data": {
"image_product": {
"url": "https://example.com/product.jpg",
"link": "https://shop.example.com/products/123"
}
}
}
See Dynamic Images for supported formats, resizing, choosing which image to insert, and clickable links.
Async Mode
For high-volume or latency-sensitive workflows, use async mode. The API returns ajobId immediately, and you poll for the result.
# 1. Start async document creation
curl -X POST https://api.docsautomator.co/createDocument \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"automationId": "YOUR_AUTOMATION_ID",
"async": true,
"data": {
"customer_name": "Acme Corp",
"total": "$5,500.00"
}
}'
# Returns: { "message": "Document creation request queued.", "jobId": "job_abc123", "logId": "..." }
# 2. Poll for the result
curl https://api.docsautomator.co/job/job_abc123 \
-H "Authorization: Bearer YOUR_API_KEY"
# Returns: { "status": "completed", "result": { "pdfUrl": "https://...", ... } }
// 1. Start async document creation
const createResponse = await fetch("https://api.docsautomator.co/createDocument", {
method: "POST",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
automationId: "YOUR_AUTOMATION_ID",
async: true,
data: {
customer_name: "Acme Corp",
total: "$5,500.00",
},
}),
});
const { jobId } = await createResponse.json();
// 2. Poll for the result with exponential backoff
let delay = 2000;
while (true) {
await new Promise((resolve) => setTimeout(resolve, delay));
const jobResponse = await fetch(
`https://api.docsautomator.co/job/${jobId}`,
{ headers: { "Authorization": "Bearer YOUR_API_KEY" } }
);
const job = await jobResponse.json();
if (job.status === "completed") {
console.log("PDF URL:", job.result.pdfUrl);
break;
}
if (job.status === "failed") {
throw new Error(`Job failed: ${job.error}`);
}
delay = Math.min(delay * 2, 30000); // Max 30s between polls
}
import requests
import time
# 1. Start async document creation
create_response = requests.post(
"https://api.docsautomator.co/createDocument",
headers={
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
json={
"automationId": "YOUR_AUTOMATION_ID",
"async": True,
"data": {
"customer_name": "Acme Corp",
"total": "$5,500.00",
},
},
)
job_id = create_response.json()["jobId"]
# 2. Poll for the result with exponential backoff
delay = 2
while True:
time.sleep(delay)
job_response = requests.get(
f"https://api.docsautomator.co/job/{job_id}",
headers={"Authorization": "Bearer YOUR_API_KEY"},
)
job = job_response.json()
if job["status"] == "completed":
print("PDF URL:", job["result"]["pdfUrl"])
break
if job["status"] == "failed":
raise Exception(f"Job failed: {job.get('error')}")
delay = min(delay * 2, 30) # Max 30s between polls
Safe retries with an idempotency key
A network error or a timeout can leave you unsure whether a document was created. Send an idempotency key with the request, and you can send the same request again without creating a second document, a second signing session or a second approval request. Choose one key per document you want, for example the record id plus a version:"proposal-1042-v2". Keys belong to your workspace, so another workspace can use the same key. A key is kept for 7 days after the first request. After that, the same key creates a new document.
| Situation | Answer |
|---|---|
| First request with this key | The document is created as usual. |
| Same key and same request, first request finished | HTTP 200 with the first request’s result: the same logId, pdfUrl and file links, signingSessionId and signingLinks, holdId for approvals, and jobId when the first request was queued. The answer also has "idempotentReplay": true. This is the same in sync and async mode. |
| Same key and same request, first request still running, async mode | HTTP 202 with the running request’s logId and, once it is queued, its jobId. Poll GET /job/{jobId} or send the request again later. |
| Same key and same request, first request still running, sync mode | HTTP 409 with "code": "IDEMPOTENCY_KEY_IN_PROGRESS", the logId and, once it is queued, the jobId. Send the request again in a few seconds to get the result. |
| Same key, different request | HTTP 409 with "code": "IDEMPOTENCY_KEY_REUSED". Nothing is created. Use a new key for a new document. |
| Same key, first request failed | The request runs again. A failed request does not use up its key. |
| Key that is not a string, is empty or is longer than 255 characters | HTTP 400 with "code": "IDEMPOTENCY_KEY_INVALID". |
async, the key itself and your API key are not compared. For an automation with a record source such as Airtable, the request holds the record id, not the record’s values, so a change to the record does not make it a different request. Use a new key when you want a new document from the changed record.
Requests without a key work as before: every request creates a new document.
Airtable script example
This script runs in an Airtable automation (“Run a script” action). It uses the record id and a version field as the key, so running it again for the same record never creates a second proposal, and raising the version creates a new one.const { recordId, version } = input.config();
const response = await fetch("https://api.docsautomator.co/createDocument", {
method: "POST",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
automationId: "YOUR_AUTOMATION_ID",
recId: recordId,
idempotencyKey: `proposal-${recordId}-v${version}`,
}),
});
const result = await response.json();
if (result.code === "IDEMPOTENCY_KEY_IN_PROGRESS") {
// The first run is still creating this document. Run the automation again
// later: the same key then returns the finished result.
throw new Error("Document is still being created. Try again in a minute.");
}
if (response.status !== 200) {
throw new Error(result.message || result.error || `HTTP ${response.status}`);
}
output.set("logId", result.logId || "");
output.set("signingSessionId", result.signingSessionId || "");
output.set("holdId", result.holdId || "");
Response
Sync mode (200)
{
"message": "success",
"pdfUrl": "https://files.docsautomator.co/...",
"documentName": "Invoice #001"
}
message, pdfUrl and documentName are always present. When the automation uses Approvals and the document waits for approval, message and status are "awaiting_approval", pdfUrl and googleDocUrl are null, and the save fields are left out. The answer also has logId, holdId and approval. The webhook fires after the approval. Additional fields are included depending on your automation’s configuration:
| Field | Included when |
|---|---|
googleDocUrl | Save Google Doc is enabled |
savePdfGoogleDriveFolderId | Save PDF to Google Drive is enabled |
savePdfGoogleDriveFileId | Save PDF to Google Drive is enabled |
savePdfGoogleDriveUrl | Save PDF to Google Drive is enabled |
wordFileUrl | Save Word document is enabled (Word templates; download URL for the generated .docx) |
saveWordGoogleDriveFolderId | Save Word document to Google Drive is enabled |
saveWordGoogleDriveFileId | Save Word document to Google Drive is enabled |
signingSessionId | E-signature is enabled |
signingStatus | E-signature is enabled ("created", "queued", or "awaiting_approval" when the automation needs approval) |
signingLinks | E-signature is enabled (array of signer objects with signerIndex, email, name, signingUrl, status). Absent while the session waits for approval |
approval | The document waits for approval (status, rule ("all" or "any"), approvers, each with name, email and, when set, source ("fixed", "field" or "request") and invited, condition, and fieldApprovers when the approvers came from your data or the request) |
signingDeliveryMethod | E-signature is enabled |
esignSkipped | E-signature is enabled but skipped for this request via skipEsign: true |
eInvoice | The automation creates an e-invoice: { "ok": true, "format": "zugferd" }, or with XRechnung { "ok": true, "format": "xrechnung", "xmlUrl": "..." } plus xmlGoogleDriveUrl when the PDF is saved to Google Drive. See E-invoices. |
Async mode (202)
{
"message": "Document creation request queued.",
"jobId": "job_abc123",
"logId": "log_xyz789"
}
E-invoices
When the e-invoice add-on is on for the workspace and the automation, every document you create through the API is also an e-invoice. You send the same request; nothing in the request changes.- Fields: the e-invoice reads the request keys mapped in the automation’s E-invoice section, including the
line_items_1list. Send amounts in euros, as numbers (1059) or as text ("1.059,00 €"). - ZUGFeRD:
pdfUrlis the e-invoice (a PDF/A-3 file with the data embedded). - XRechnung:
pdfUrlis the readable PDF, andeInvoice.xmlUrlis the XML file. - Async mode: the job result carries the same
eInvoicefield. - Test mode: with
isPreview: truethe e-invoice is valid but marked as a test (TEST-invoice number and a test note).
message that starts with “E-invoice not created” and names each problem; in async mode the job ends with status failed and the same text in error:
{
"message": "E-invoice not created: Gross total does not match the line items. The record says €1,060.00; the line items give €1,059.00."
}
Last modified on October 21, 2026