API beta

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.

Sample GET /api/v1/configs/{id} response
{
  "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.

GET
/api/v1/configs

List saved configs for the authenticated account.

GET
/api/v1/configs/{id}

Inspect one config's expected columns and mappings before wiring an automation.

POST
/api/v1/configs/{id}/run

Run 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.

GET
/api/v1/jobs/{id}

Poll a job and retrieve `downloadUrl`, status, or structured error.

GET
/api/v1/jobs/{id}/download

Stream 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

List configs
curl https://doctagd.com/api/v1/configs \
  -H "Authorization: Bearer $DOCTAGD_API_KEY"
Run with Excel upload
curl -X POST https://doctagd.com/api/v1/configs/$CONFIG_ID/run \
  -H "Authorization: Bearer $DOCTAGD_API_KEY" \
  -F excel=@responses.xlsx
Run with one JSON row
curl -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"
  }'
Run with multiple JSON rows
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" }
  ] }'
Poll a job
curl https://doctagd.com/api/v1/jobs/$JOB_ID \
  -H "Authorization: Bearer $DOCTAGD_API_KEY"
Inspect expected fields
curl https://doctagd.com/api/v1/configs/$CONFIG_ID \
  -H "Authorization: Bearer $DOCTAGD_API_KEY"
Safe retries (idempotency)
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.
Structured drift response (HTTP 409)
{
  "error": "drift",
  "message": "...",
  "jobId": "…",
  "missingColumns": ["client_name"],
  "suggestions": { "client_name": ["customer_name"] }
}
Usage limit reached (HTTP 402)
{
  "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 IDA 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 jobId repeated in your responses with idempotent: true on 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:

  • doneuse downloadUrl
  • runningthe run took longer than the synchronous window; poll GET /api/v1/jobs/{id} until done
  • errorinspect the error object. Most often this is a replayed cached failure; send a fresh Idempotency-Key to retry properly
Automation tools (Zapier, Make, n8n) treat any 200 as success, so they will happily pass an 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.

1. The request (any platform)
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.
2. The response
{
  "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

01

Trigger

Whatever app collects your data — Typeform, Google Forms, Jotform, Tally, Google Sheets (New Spreadsheet Row), Airtable, etc. One trigger event = one document.

02

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.

03

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:
Data (raw JSON)
{
  "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:
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.

04

Fetch the file

  • App: Webhooks by Zapier (a second action step)
  • Action Event: GET
  • URL: {{step3.downloadUrl}} — the downloadUrl value from the POST response
  • Headers:
Headers
Authorization: Bearer dtgd_<your-key>
A plain "upload from URL" action in your delivery app will not work— those actions can't send custom headers, so the API will return 401. You must fetch the file through a Webhooks GET step first.
05

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:

Delivery fields
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.

SymptomLikely causeFix
401 Authentication requiredMissing Bearer prefix, header name misspelled (Authorisation vs Authorization), key has stray whitespaceRe-check the credential value; regenerate the key if you’re not sure it copied cleanly
409 with missingColumnsJSON keys don’t match the config’s expected columns exactlyGET /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: trueYou're replaying a failed job via a reused Idempotency-KeySend a fresh Idempotency-Key to actually retry
Multiple rows return the same jobId and only one document is generatedThe same Idempotency-Key is being sent for every rowMake the key unique per row ({{ row_number }} or a natural composite)
status: "running" with no downloadUrlThe run exceeded the synchronous windowPoll GET /api/v1/jobs/{id} every few seconds until status: "done"
401 when fetching downloadUrlThe 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_reachedMonthly document quota exhaustedUpgrade your plan or wait until the next UTC month rolls over
429Rate-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 whileGenerated files are retained for 24 hours, then cleaned upFetch and store the file immediately after the run; don’t pass URLs around for later use