Run saved document configs from your automation stack.
Create an API key, list saved configs, run one with a fresh Excel file or JSON rows, then pass the returned download URL to Zapier, n8n, Make, or your own backend.
1. Create a key
Account settings shows the secret once. Store it in your automation platform as a bearer token.
2. Pick a config
Saved configs are the reusable automation unit. Template and quick configs both work.
3. Check what it expects
GET the config to see the exact column keys it needs. Your JSON keys must match exactly — case-sensitive, no spaces, no renames.
4. Run with data
Send multipart form-data with an excel file, or JSON with row / rows, and receive a job plus output URL.
Before wiring your automation, call GET /api/v1/configs/{id} to see the exact column keys this config needs. Your JSON keys must match these exactly — case-sensitive, no spaces, no renames.
{
"id": "9591c556-26d4-4fb2-a2a3-629e469225ae",
"name": "Service Agreement",
"mode": "mapped",
"expectedColumns": [
"agreement_date", "client_name", "start_date", "end_date",
"provider_name", "services_description", "total_fee",
"payment_terms", "jurisdiction",
"provider_signatory_name", "provider_signatory_title",
"client_signatory_name", "client_signatory_title"
]
}If your trigger's field names don't match (e.g. a form sends Client Name but the config expects client_name), add a mapping step between the trigger and the run request — Zapier Formatter, Make Set Variable, or n8n Set / Edit Fields — to rename fields. Skipping this step is the most common reason for a 409 drift error.
Core endpoints
All public API routes accept Authorization: Bearer dtgd_.... Use https://doctagd.com as the API host. Generated outputs are stored as private blobs and returned as download URLs.
/api/v1/configsList saved configs for the authenticated account.
/api/v1/configs/{id}Inspect one config's expected columns and mappings before wiring an automation.
/api/v1/configs/{id}/runRun a saved config with multipart field `excel`, JSON `{ row: {...} }`, JSON `{ rows: [...] }`, or a flat JSON row object. Pass an optional `Idempotency-Key` header so retries are safe.
/api/v1/jobs/{id}Poll a job and retrieve `downloadUrl`, status, or structured error.
/api/v1/jobs/{id}/downloadStream the generated document — a `.docx` for a single-document run, a `.zip` for multi-row. Send the same `Authorization: Bearer dtgd_...` header — the output is private and this proxy is the only way to fetch it.
Copy-paste examples
curl https://doctagd.com/api/v1/configs \
-H "Authorization: Bearer $DOCTAGD_API_KEY"curl -X POST https://doctagd.com/api/v1/configs/$CONFIG_ID/run \
-H "Authorization: Bearer $DOCTAGD_API_KEY" \
-F excel=@responses.xlsxcurl -X POST https://doctagd.com/api/v1/configs/$CONFIG_ID/run \
-H "Authorization: Bearer $DOCTAGD_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"client_name": "Acme Ltd",
"agreement_date": "14 May 2026",
"total_fee": "4800"
}'curl -X POST https://doctagd.com/api/v1/configs/$CONFIG_ID/run \
-H "Authorization: Bearer $DOCTAGD_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "rows": [
{ "client_name": "Acme Ltd", "total_fee": "4800" },
{ "client_name": "Northline", "total_fee": "7200" }
] }'curl https://doctagd.com/api/v1/jobs/$JOB_ID \
-H "Authorization: Bearer $DOCTAGD_API_KEY"curl https://doctagd.com/api/v1/configs/$CONFIG_ID \
-H "Authorization: Bearer $DOCTAGD_API_KEY"curl -X POST https://doctagd.com/api/v1/configs/$CONFIG_ID/run \
-H "Authorization: Bearer $DOCTAGD_API_KEY" \
-H "Idempotency-Key: sheet-row-8842" \
-H "Content-Type: application/json" \
-d '{ "client_name": "Acme Ltd", "total_fee": "4800" }'
# A retry with the same Idempotency-Key returns the original
# job (idempotent: true) without regenerating or re-billing.{
"error": "drift",
"message": "...",
"jobId": "…",
"missingColumns": ["client_name"],
"suggestions": { "client_name": ["customer_name"] }
}{
"error": "Your Free plan has 0 documents remaining…",
"code": "usage_limit_reached",
"plan": "free",
"used": 10,
"limit": 10,
"requested": 3
}Limits, retries, and errors
Request limits
Up to 500 rows and 80 columns per run, 10,000 chars per cell, and a 512 KB JSON body. Larger Excel files are accepted as multipart uploads within the same row/column limits.
Status codes
402 when a run would exceed your monthly document allowance, 404 for an unknown config, 409 with `missingColumns` on header drift, 429 when rate limited (60 req/min).
Idempotency
Send an Idempotency-Key header on every run. A retry with the same key returns the original job and never regenerates or re-bills. Keys are scoped per account.
The key must be unique per logical row but stable across retries of that same row. Getting this wrong is the most common automation bug:
| ✓ Use | ✗ Avoid |
|---|---|
A row ID from your source (e.g. {{ $json.row_number }}, {{ recordId }}) | A constant string |
A natural composite (e.g. {{ client_name }}-{{ agreement_date }}) | The execution / run ID — it's the same across every row in one batch |
| The form submission ID | A fresh timestamp on every request — defeats retry safety |
What happens if you get it wrong
- Same key reused across multiple rows in one batch run — the API returns the firstrow's job for all of them. Only one document gets generated. You'll see the same
jobIdrepeated in your responses withidempotent: trueon every item after the first. - Same key reused after a failed run— you'll get the cached error back (
status: "error",idempotent: true). The drift, quota, or other failure is fixed by sending a new key, not by retrying the old one. - Different key on a legitimate retry — the API treats it as a brand new run and bills you again. This is why a stable per-row key matters.
Always check the status field
A 200 OK response means the API processed your request — not that generation succeeded. Branch on status:
doneusedownloadUrlrunningthe run took longer than the synchronous window; pollGET /api/v1/jobs/{id}untildoneerrorinspect theerrorobject. Most often this is a replayed cached failure; send a freshIdempotency-Keyto retry properly
error payload downstream unless you explicitly check status.Example flow: form submission → Word contract
A common automation: a web form (Typeform, Google Forms, Jotform, Tally) collects engagement details, and each submission auto-produces a branded .docx from a saved template config — no manual Word work. The trigger fires, you POST the response as a flat JSON object to the run endpoint, then fetch the returned downloadUrl with the same Bearer key and email or file the document. One row returns a ready .docx; multi-row runs return a .zip. The same three steps work in Zapier, Make, and n8n.
curl -X POST https://doctagd.com/api/v1/configs/{configId}/run \
-H "Authorization: Bearer dtgd_…" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: form-response-2f9c1a" \
-d '{
"agreement_date": "18 May 2026",
"provider_name": "Doctagd Ltd",
"client_name": "Acme Innovations Ltd",
"services_description": "Monthly document automation retainer",
"start_date": "1 June 2026",
"end_date": "31 May 2027",
"total_fee": "GBP 12,000",
"payment_terms": "Net 30",
"jurisdiction": "England and Wales",
"provider_signatory_name": "S. Khan",
"provider_signatory_title": "Director",
"client_signatory_name": "J. Doe",
"client_signatory_title": "COO"
}'
# JSON keys must match the config's expected columns
# exactly, or you get a 409 with suggested names.
# Set Idempotency-Key to the form's submission ID.{
"jobId": "f1e2d3c4-…",
"status": "done",
"inputType": "json",
"filename": "Acme Innovations Ltd.docx",
"downloadUrl": "https://doctagd.com/api/v1/jobs/f1e2d3c4-…/download",
"idempotent": false
}
# A run that produces ONE document returns a .docx;
# multi-row runs return a .zip (filename tells you which).
# Synchronous — no polling. Fetch downloadUrl with the
# SAME Authorization: Bearer dtgd_… header, then email
# / Drive / Slack it. A retry with the same
# Idempotency-Key returns this same job
# ("idempotent": true, no re-billing).Per-platform setup
Pick your automation platform below. Each walkthrough produces a ready .docx for a single-row run; multi-row runs return a .zip.
5 steps · single-row run returns a ready .docx
Trigger
Whatever app collects your data — Typeform, Google Forms, Jotform, Tally, Google Sheets (New Spreadsheet Row), Airtable, etc. One trigger event = one document.
Map your fields (optional Formatter step)
If your trigger's field names don't match the config's expected columns exactly (e.g. Typeform sends Client Name but the config expects client_name), add a Formatter by Zapierstep (Utilities → Lookup Table, or just map them inline in the next step's Data fields). Without this rename step, you'll hit a 409 drift error. Run GET /api/v1/configs/{id} first to see the canonical keys.
POST the run
- App: Webhooks by Zapier
- Action Event:
Custom Request— use this one, not the plain POST event. Custom Request is the only Webhooks action that lets you send a raw JSON body with custom headers without Zapier reformatting it - Method:
POST - URL:
https://doctagd.com/api/v1/configs/<your-config-id>/run - Data:raw JSON with the config's expected-column keys, mapping each value from the trigger:
{
"client_name": "{{trigger.client_name}}",
"agreement_date": "{{trigger.agreement_date}}",
"total_fee": "{{trigger.total_fee}}"
}(Zapier uses its own field-picker syntax in the actual UI — drag fields in from the previous step.)
- Unflatten: No
- Headers:
Authorization: Bearer dtgd_<your-key>
Content-Type: application/json
Idempotency-Key: <a unique-per-row, stable-on-retry value>For the Idempotency-Key, pick something natural from the trigger — the form submission ID, an Airtable record ID, a Google Sheets row number, or a composite like {{trigger.client_name}}-{{trigger.agreement_date}}. Reusing the same value across rows produces only one document; reusing it after a failed run replays the failure.
Fetch the file
- App: Webhooks by Zapier (a second action step)
- Action Event:
GET - URL:
{{step3.downloadUrl}}— thedownloadUrlvalue from the POST response - Headers:
Authorization: Bearer dtgd_<your-key>Deliver
Pass the binary from step 04 into your final action — Email by Zapier (Attachments field), Gmail → Send Email, Google Drive → Upload File, Slack → Send Channel Message (with file), etc. Use the filenamevalue from step 03's response so the file is named what doctagd named it:
File: {{step4.file}}
Filename: {{step3.filename}}The filename already includes .docx.
Common errors
Stuck? Match your symptom to the row below for the cause and the fix.
| Symptom | Likely cause | Fix |
|---|---|---|
401 Authentication required | Missing Bearer prefix, header name misspelled (Authorisation vs Authorization), key has stray whitespace | Re-check the credential value; regenerate the key if you’re not sure it copied cleanly |
409 with missingColumns | JSON keys don’t match the config’s expected columns exactly | GET /api/v1/configs/{id} for the canonical list; rename keys with a Set / Formatter step before the POST |
Response is 200 but body says status: "error" and idempotent: true | You're replaying a failed job via a reused Idempotency-Key | Send a fresh Idempotency-Key to actually retry |
Multiple rows return the same jobId and only one document is generated | The same Idempotency-Key is being sent for every row | Make the key unique per row ({{ row_number }} or a natural composite) |
status: "running" with no downloadUrl | The run exceeded the synchronous window | Poll GET /api/v1/jobs/{id} every few seconds until status: "done" |
401 when fetching downloadUrl | The GET to the download endpoint is missing the Authorizationheader, or you're using a node/action that doesn't support custom headers (e.g. some "upload from URL" actions) | Use a real HTTP request node with the same Bearer credential — never a plain URL-fetcher |
402 usage_limit_reached | Monthly document quota exhausted | Upgrade your plan or wait until the next UTC month rolls over |
429 | Rate-limited (60 requests/min) | Add a Wait/backoff step, or batch rows into one request with { "rows": [...] } instead of one request per row |
| Download URL returns 404 after a while | Generated files are retained for 24 hours, then cleaned up | Fetch and store the file immediately after the run; don’t pass URLs around for later use |
