DocMake

How to generate a PDF invoice from JSON with an API

Turn a JSON payload into a finished PDF invoice with one POST request: authentication, the exact request body, strict mode, error shapes, and working curl, Node.js and Python code.

DocMake Team 6 min read

To generate a PDF invoice from JSON with DocMake, POST the invoice data to https://app.docmake.io/api/v1/render with a bearer API key, a template_id, and "format": "pdf". The response body is the finished PDF file. There is no job to poll and no second call to fetch the result: a single render returns the binary synchronously.

You need three things before the first call: a DocMake account (the free plan includes full API access), an API key from Settings, API Keys, and a template ID. Everything else in this guide is a parameter on that one endpoint.

Step 1: get an API key

Keys are created in the app under Settings, API Keys. Every key is prefixed dm_ and is shown exactly once, because only a SHA-256 hash of it is stored. If you lose it, revoke it and create another.

A key belongs to a team, not to a person. Any request made with it acts as that team’s workspace, which is what you want for a server-side integration. The Free plan allows 2 keys; Pro is unlimited.

Step 2: get a template ID

A template is the document design: the layout, the fixed text, and the fill-in fields. You build it once in the visual editor (or import an existing Word file), then reuse it for every render. Template IDs look like tmpl_ne1EfQns2YBH, and you copy one from the editor.

If you want to see the fields a template expects before you write any mapping code, ask the API:

curl https://app.docmake.io/api/v1/templates/tmpl_ne1EfQns2YBH/form-schema \
  -H "Authorization: Bearer dm_your_api_key"

That returns template_id, name, and a fields array. Each field carries a key (the dotted path where its value belongs in your data payload), a label, a type, whether it is required, and its default. Array fields nest their per-item fields under their own fields key, so the response is a map of exactly the JSON you need to send.

The invoice template in our gallery is a good reference shape: scalar fields for the header block and totals, plus an items array that expands into one table row per entry.

Step 3: render the PDF

curl -X POST https://app.docmake.io/api/v1/render \
  -H "Authorization: Bearer dm_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "template_id": "tmpl_ne1EfQns2YBH",
    "format": "pdf",
    "strict": true,
    "data": {
      "invoice_number": "INV-2026-0142",
      "issue_date": "August 8, 2026",
      "due_date": "September 7, 2026",
      "client_name": "Acme Corp",
      "client_address": "540 Market Street, San Francisco, CA 94104",
      "items": [
        { "description": "Design services", "qty": "12", "unit_price": "$150.00", "amount": "$1,800.00" },
        { "description": "Development sprint", "qty": "1", "unit_price": "$2,600.00", "amount": "$2,600.00" }
      ],
      "total": "$4,400.00",
      "payment_terms": "30 days"
    }
  }' \
  --output invoice.pdf

That is the whole integration. invoice.pdf is on disk.

The request body, parameter by parameter

ParameterRequiredNotes
template_idyesThe tmpl_ public ID of the template to render.
datayesThe key must be present. An empty object is legal; every variable then falls back to its default or fallback text.
formatnodocx or pdf. Defaults to docx, so pass pdf explicitly.
filenamenoOutput name without extension, max 255 characters. Defaults to the template name.
localenoBCP 47 tag such as en-US or de-DE, used for date and number formatting. Defaults to the workspace setting.
page_sizenoa4, letter or legal. Defaults to the workspace setting.
strictnoFail with a list of missing variables instead of rendering blanks. Defaults to false.

The one that trips people up is format. Omit it and you get a DOCX, because the API was built DOCX-first. If you want a PDF, say so.

What comes back

On success you get a 200 and the raw file, plus a few headers worth reading:

  • Content-Type: application/pdf, or the Word MIME type for DOCX.
  • Content-Disposition: attachment; filename="...", so you can reuse the server-side filename.
  • X-DocMake-Render-Time-Ms: how long the engine took, in milliseconds.
  • X-DocMake-Warnings: a JSON array of non-fatal engine warnings, for example a variable that resolved to nothing.
  • X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset: your per-minute budget.

X-DocMake-Warnings is the header to log. A render can succeed and still be wrong, because a missing variable resolves to its fallback text or to an empty string. The warnings tell you which ones.

Use strict mode so blanks fail loudly

Passing "strict": true runs the payload against the template’s variable contract before rendering. If anything has no value and no default, you get a 422 instead of a document with holes in it:

{
  "error": {
    "type": "missing_data",
    "message": "2 variable(s) have no data. Provide values or render without strict.",
    "missing": [
      { "key": "client_name", "message": "Fill in 'client_name'." },
      { "key": "due_date", "message": "Fill in 'due_date'." }
    ]
  }
}

A strict failure does not count against your monthly document quota, so there is no cost to being careful. Variables that declare a default in the editor are filled in before the check runs, so a defaulted variable never counts as missing.

The other error responses

  • 401 with error.type of authentication_error: the key is missing, malformed, or revoked. Keys must start with dm_.
  • 404: no template with that ID in this team’s workspace.
  • 422: either the strict-mode shape above, or Laravel-style validation with message and an errors object when a parameter is the wrong type.
  • 429: two different shapes. Per-minute throttling returns error.type of rate_limit_error with a Retry-After header. Blowing your monthly document or daily API-call quota returns a flat body with "error": "quota_exceeded", plus used and limit.
  • 503 with render_busy and Retry-After: 5: the render pool is saturated. Retry.

The same call in Node.js and Python

import { writeFile } from 'node:fs/promises';

const res = await fetch('https://app.docmake.io/api/v1/render', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.DOCMAKE_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    template_id: 'tmpl_ne1EfQns2YBH',
    format: 'pdf',
    strict: true,
    data: invoice,
  }),
});

if (!res.ok) {
  throw new Error(`Render failed: ${res.status} ${await res.text()}`);
}

console.log('rendered in', res.headers.get('X-DocMake-Render-Time-Ms'), 'ms');
await writeFile('invoice.pdf', Buffer.from(await res.arrayBuffer()));
import os
import requests

res = requests.post(
    "https://app.docmake.io/api/v1/render",
    headers={"Authorization": f"Bearer {os.environ['DOCMAKE_API_KEY']}"},
    json={
        "template_id": "tmpl_ne1EfQns2YBH",
        "format": "pdf",
        "strict": True,
        "data": invoice,
    },
)

if res.status_code == 422:
    raise ValueError(res.json()["error"]["missing"])
res.raise_for_status()

with open("invoice.pdf", "wb") as f:
    f.write(res.content)

Billing runs: use the batch endpoint

Invoicing is bursty. Instead of firing 400 single renders at the start of the month, POST once to /api/v1/render/batch with a template_id and an items array of up to 100 entries, each with its own data and optional filename. You get a 202 back with a job_id, then either poll GET /api/v1/render/jobs/{job_id} or supply a webhook_url and let the job call you.

When the job completes, GET /api/v1/render/jobs/{job_id}/download returns the single file, or a ZIP if the batch produced several. Download links stay valid for the 7-day document history window. Per-item status is reported individually, so one bad row does not sink the run.

Rate limits, in plain numbers

Free allows 30 requests per minute and 500 API calls per day, against a 1,000 document monthly quota. Pro allows 120 requests per minute, 50,000 API calls per day, and 10,000 documents a month. GET /api/v1/usage returns where you stand right now: plan, billing period, and used-versus-limit counts for renders, templates, credits and API calls, with null meaning unlimited.

Where to go next

Run this against your own account

The Free plan includes full API access, 1,000 documents a month, no card and no expiry. Create a key in Settings, API Keys, and the snippets above work as written.

Get an API key