DocMake

Developers

One request. One document.

The DocMake API turns a template and a JSON object into a finished DOCX or PDF. Authenticate with a bearer token, then make a document (POST https://app.docmake.io/api/v1/render) out of a template id and the values that fill in the blanks (the data payload): the response body is the file. Every failure answers in one error envelope, and a document that rendered with something missing says so in a response header instead of quietly shipping a gap. Template CRUD, DOCX import, batch jobs, assets and usage are on every plan, the free one included.

Quick start

Three steps, about a minute. The request and the response below were run against production and pasted back unedited, with the key redacted.

  1. 01 Create a key in Settings → API Keys. Keys start with dm_ and are shown once; only a hash is stored. A key belongs to one workspace and can only see that workspace.
  2. 02 Open a template in the editor and copy its id from the URL, or call GET /api/v1/templates. Ids look like tmpl_ow4Jl7QC7tEw.
  3. 03 POST the id and your data to /api/v1/render. The body of the response is the file. Set "format": "docx" for Word, and "filename" to name the download.
Request
curl -sS -D - -X POST https://app.docmake.io/api/v1/render \
  -H "Authorization: Bearer dm_xxx" \
  -H "Content-Type: application/json" \
  --output quote.pdf \
  -d '{
  "template_id": "tmpl_ow4Jl7QC7tEw",
  "format": "pdf",
  "data": {
    "date": "2026-09-02",
    "client": {
      "name": "Northwind Trading SRL",
      "address": "Str. Exemplu 10, Bucuresti",
      "cui": "RO12345678",
      "contact": "Maria Ionescu"
    },
    "items": [
      { "product": "Consultanta ISO 9001", "qty": 1, "unit_price": 2500 },
      { "product": "Audit intern", "qty": 2, "unit_price": 800 },
      { "product": "Instruire personal", "qty": 3, "unit_price": 350 }
    ],
    "discount_percent": 10,
    "total": 4635,
    "signer": { "name": "Elena Marin", "title": "Director" }
  }
}'
Response
HTTP/2 200
server: nginx
content-type: application/pdf
content-length: 70104
x-docmake-render-time-ms: 1669
x-docmake-warnings: []
cache-control: public
date: Thu, 03 Sep 2026 05:47:44 GMT
last-modified: Thu, 03 Sep 2026 05:47:44 GMT
content-disposition: attachment; filename=oferta.pdf
accept-ranges: none
x-ratelimit-limit: 30
x-ratelimit-remaining: 29
x-ratelimit-reset: 1788414524
access-control-allow-origin: *
x-frame-options: SAMEORIGIN
x-xss-protection: 1; mode=block
x-content-type-options: nosniff

70 KB of PDF followed the headers into quote.pdf. x-docmake-warnings is an empty array: nothing was missing.

Errors

Every failure under /api answers with the same JSON body, whatever the Accept header says. There is no HTML error page to parse and no raw framework message to guess at.

The envelope
{
  "error": {
    "type": "not_found",
    "message": "No template with id tmpl_doesnotexist in this workspace. Check the id, and that the API key belongs to the team that owns it.",
    "details": []
  }
}

type is the stable machine-readable name to branch on. message is a sentence written for a person. details is an array that is empty unless the error has per-field or per-value specifics. Two error shapes add a key: a failed render adds engine_errors, and a refused strict render adds missing and details entries with a path.

Every type

type Status When
validation_error 422 A field in the request body fails a rule. details lists one entry per field and message.
missing_data 422 A strict render found a blank with no value. missing and details name every one by path.
type_mismatch 422 A strict render found values of the wrong type and none missing. Same body, missing is empty.
authentication_error 401 The Authorization header is absent, is not a dm_ key, or the key is unknown or revoked.
authorization_error 403 The key resolves to a workspace the request may not act on.
not_found 404 No template, job or asset with that id in this workspace, or the route does not exist.
method_not_allowed 405 The route exists but not for that HTTP method.
bad_request 400 A malformed request the router rejects before a controller sees it.
render_error 400 The document itself is wrong: a broken calculation, an invalid node. engine_errors names each one.
conflict 409 The request contradicts the current state of the resource.
job_not_completed 409 A batch download was asked for while the job is still processing.
download_expired 404 The batch finished but its files are past the download window.
no_output 404 The batch finished and no item produced a file.
payload_too_large 413 The request body is over the server limit.
token_mismatch 419 A session token expired. Only reachable from a browser session, never from a key.
rate_limit_error 429 The per-minute budget for the workspace is spent. Retry-After says how long to wait.
quota_exceeded 429 A plan quota is spent, for example the monthly document allowance. Its body has its own shape, see below.
server_error 500 An unhandled failure. The message is generic and carries no stack trace.
render_failed 500 The renderer failed on a document that is otherwise valid. Safe to retry.
render_busy 503 Rendering capacity is saturated. Retry-After: 5.
unavailable 503 The service is down for maintenance.
request_error other 4xx A client error with a status not in this table, for example 410. The catch-all name so error.type is always a string you can branch on.

Worked examples

Each pair below is a real call and its real answer.

401 authentication_error

curl -sS -X POST https://app.docmake.io/api/v1/render \
  -H "Authorization: Bearer dm_not_a_real_key" \
  -H "Content-Type: application/json" \
  -d '{"template_id":"tmpl_ow4Jl7QC7tEw","data":{}}'
{
  "error": {
    "type": "authentication_error",
    "message": "Invalid API key. Check that your key is correct and not revoked."
  }
}

With no Authorization header at all the message is "Invalid or missing API key." Both are 401. These two are written by the authentication middleware and are the only errors without a details key.

404 not_found

curl -sS -X POST https://app.docmake.io/api/v1/render \
  -H "Authorization: Bearer dm_xxx" \
  -H "Content-Type: application/json" \
  -d '{"template_id":"tmpl_doesnotexist","data":{}}'
{
  "error": {
    "type": "not_found",
    "message": "No template with id tmpl_doesnotexist in this workspace. Check the id, and that the API key belongs to the team that owns it.",
    "details": []
  }
}

A template that belongs to another workspace answers exactly the same way, so the API never confirms that someone else’s template exists.

422 validation_error

curl -sS -X POST https://app.docmake.io/api/v1/render \
  -H "Authorization: Bearer dm_xxx" \
  -H "Content-Type: application/json" \
  -d '{"format":"xlsx"}'
{
  "error": {
    "type": "validation_error",
    "message": "The template id field is required. (and 2 more errors)",
    "details": [
      {
        "field": "template_id",
        "message": "The template id field is required."
      },
      {
        "field": "data",
        "message": "The data field must be present."
      },
      {
        "field": "format",
        "message": "The selected format is invalid."
      }
    ]
  }
}

details carries one entry per field and message, in rule order. data must be present even when it is empty.

405 method_not_allowed

curl -sS -X GET https://app.docmake.io/api/v1/render \
  -H "Authorization: Bearer dm_xxx"
{
  "error": {
    "type": "method_not_allowed",
    "message": "The GET method is not supported for route api/v1/render. Supported methods: POST.",
    "details": []
  }
}

An unknown path under /api is a 404 in the same envelope: "The route api/v1/nope could not be found."

400 render_error

curl -sS -X POST https://app.docmake.io/api/v1/render \
  -H "Authorization: Bearer dm_xxx" \
  -H "Content-Type: application/json" \
  -d '{"template_id":"tmpl_xxx","format":"pdf","data":{ ... }}'
{
  "error": {
    "type": "render_error",
    "message": "Template rendering failed.",
    "engine_errors": [
      {
        "node_id": "f_line",
        "node_kind": "formula",
        "node_label": "Line total",
        "type": "invalid_formula",
        "message": "Multiply by what?",
        "severity": "error",
        "path": null
      }
    ]
  }
}

The document is wrong, not the data: this template had a calculation reading qty * with nothing after the operator. engine_errors uses the same fields as a warning, with severity "error". Nothing is rendered.

429 rate_limit_error

for i in $(seq 1 32); do
  curl -sS -o /dev/null -w "%{http_code}\n" \
    https://app.docmake.io/api/v1/usage \
    -H "Authorization: Bearer dm_xxx"
done
HTTP/2 429
x-ratelimit-limit: 30
x-ratelimit-remaining: 0
x-ratelimit-reset: 1788414728
retry-after: 8

{
  "error": {
    "type": "rate_limit_error",
    "message": "Rate limit of 30 requests per minute exceeded. Retry in 8s."
  }
}

The budget is per workspace, per minute, and every response carries the three X-RateLimit headers so you can pace yourself before you hit it.

The one exception: quota

A spent plan allowance answers 429 with a flat body rather than the envelope, so error is a string and not an object. Branch on that before you read error.type.

error
The literal string "quota_exceeded", not an object.
feature
Which allowance ran out: doc_render, template_create, api_call, ai.
reason_code
Why, for example monthly_limit_exceeded or daily_rate_limit.
message
A sentence naming the number used, the limit and the plan.
upgrade_cta
What the next plan up allows, or null on the top plan.
used
How much of the allowance is spent.
limit
The allowance for the current plan.

Warnings: a gap is never silent

A blank (a variable in the API) that has no value in your payload renders as nothing at all. The document still comes back, and the response carries X-DocMake-Warnings: a JSON array naming everything that was missing or mistyped. An empty array means a clean render.

Every entry names the node it came from: a blank, an Only when part (a conditional node), a One per item part (a loop node), a calculation, an image or the document itself. The things to check before you send are in two places: this header, and POST /api/v1/templates/{id}/validate, which reports the same list without making a document.

Response header, as sent
x-docmake-warnings: [{"node_id":"dc8fc2ff2ffa","node_kind":"variable","node_label":"Name","type":"missing_data","message":"client.name has no value and rendered blank","severity":"warning","path":"client.name"},{"node_id":"d7c835fd890e","node_kind":"variable","node_label":"unit_price","type":"missing_data","message":"items[1].unit_price has no value and rendered blank","severity":"warning","path":"items[1].unit_price"}]

Two values were left out of the payload: the client name, and the unit price of the second item.

The same array, formatted
[
  {
    "node_id": "dc8fc2ff2ffa",
    "node_kind": "variable",
    "node_label": "Name",
    "type": "missing_data",
    "message": "client.name has no value and rendered blank",
    "severity": "warning",
    "path": "client.name"
  },
  {
    "node_id": "d7c835fd890e",
    "node_kind": "variable",
    "node_label": "unit_price",
    "type": "missing_data",
    "message": "items[1].unit_price has no value and rendered blank",
    "severity": "warning",
    "path": "items[1].unit_price"
  }
]

Fields

node_id
The id of the node in the template that produced it. Stable across renders.
node_kind
What kind of node: variable, formula, aggregation, loop, image, conditional, document.
node_label
The label shown on that node in the editor, or null.
type
What went wrong. See the table below.
message
One sentence, already written for a person to read.
severity
Always "warning" in this header: the document rendered. "error" appears in engine_errors on a refused render, and in the errors list that validate returns.
path
Where the value lives in your payload, with list indexes: client.name, items[1].unit_price. Null when the warning is not about one value.

Reading the path

path is the exact place in your own payload, so you can jump to it without searching. Nested objects use dots (client.name) and rows of a list carry their index (items[1].unit_price, the second row). When a calculation is the thing that failed, the path names the operand that stopped it, written the way the calculation reads it, which inside a repeated row is the short form (unit_price rather than items[1].unit_price).

What to do with them

Log the array against the document you just produced, and alert on it if a gap should never happen in your flow. If a gap should never happen at all, do not log it: render with strict and get a 422 instead of a file. The array is also stored on the document in your Documents history, so it is there after the fact.

Types you can receive

type Meaning
missing_data A blank the document reads has no value in the payload, so it rendered blank. Also raised when a calculation stops because one of its operands has no value.
type_mismatch A value is present but does not fit its declared type, for example the text "two" where a number is declared.
fallback_used A blank had no value and rendered the default text typed on it in the editor. Strict mode does not refuse on this one.
division_by_zero A calculation divided by zero, so it rendered blank.
invalid_image_source An image node points at something the renderer cannot read.
insecure_image_url An image node points at a plain http URL.
invalid_formatter A format on a blank or a calculation could not be applied to the value.
invalid_loop_target A repeated section points at something that is not a list.
invalid_formula A calculation could be read but not evaluated here, for example one that refers to itself, or a total asked for over something that is not a list. It renders blank.
non_numeric_majority A total was asked for over values that are mostly not numbers.
A wrong type, non strict
x-docmake-warnings: [{"node_id":"","node_kind":"variable","node_label":"qty","type":"type_mismatch","message":"items[1].qty: expected a number, got \"two\"","severity":"warning","path":"items[1].qty"}]

The same request with "qty": "two" on the second item. The document still rendered.

Strict mode: refuse instead of shipping a gap

Send "strict": true alongside data and nothing is rendered when a value is missing or has the wrong type. You get 422 and the full list instead of a document with holes in it. Two values missing, two entries.

Request
curl -sS -X POST https://app.docmake.io/api/v1/render \
  -H "Authorization: Bearer dm_xxx" \
  -H "Content-Type: application/json" \
  -d '{
  "template_id": "tmpl_ow4Jl7QC7tEw",
  "format": "pdf",
  "strict": true,
  "data": {
    "date": "2026-09-02",
    "client": {
      "address": "Str. Exemplu 10, Bucuresti",
      "cui": "RO12345678",
      "contact": "Maria Ionescu"
    },
    "items": [
      { "product": "Consultanta ISO 9001", "qty": 1, "unit_price": 2500 },
      { "product": "Audit intern", "qty": 2 },
      { "product": "Instruire personal", "qty": 3, "unit_price": 350 }
    ],
    "discount_percent": 10,
    "total": 4635,
    "signer": { "name": "Elena Marin", "title": "Director" }
  }
}'
Response
HTTP/2 422

{
  "error": {
    "type": "missing_data",
    "message": "Nothing was rendered: 2 values are missing. Fix the values listed in details, or render without strict.",
    "missing": [
      {
        "key": "client.name",
        "path": "client.name",
        "label": "Name",
        "message": "client.name has no value"
      },
      {
        "key": "items.unit_price",
        "path": "items[1].unit_price",
        "label": "unit_price",
        "message": "items[1].unit_price has no value"
      }
    ],
    "details": [
      {
        "path": "client.name",
        "key": "client.name",
        "label": "Name",
        "type": "missing_data",
        "message": "client.name has no value"
      },
      {
        "path": "items[1].unit_price",
        "key": "items.unit_price",
        "label": "unit_price",
        "type": "missing_data",
        "message": "items[1].unit_price has no value"
      }
    ]
  }
}

missing keeps the older key-based list. details is the full list, missing values and wrong types together, in the shape a form can walk.

Nothing missing, only a wrong type

The envelope type switches to type_mismatch and missing comes back empty. Numeric strings count as numbers and "true" and "false" count as booleans, because that is how values arrive from forms and from JSON alike.

What strict checks

  • • Only what the document actually reads. A definition nothing on the page uses is never missing.
  • • A calculation is an output, never an input, so it is never reported as missing.
  • • Absent, null and the empty string are all "no value". Zero and false are values.
  • • A blank that has a default value applied is filled in before the check, so it never counts as missing.
  • • A blank carrying its own default text on the chip renders that text and warns as fallback_used, which strict does not refuse.
Response
HTTP/2 422

{
  "error": {
    "type": "type_mismatch",
    "message": "Nothing was rendered: 1 value has the wrong type. Fix the values listed in details, or render without strict.",
    "missing": [],
    "details": [
      {
        "path": "items[1].qty",
        "key": "items.qty",
        "label": "qty",
        "type": "type_mismatch",
        "message": "items[1].qty: expected a number, got \"two\""
      }
    ]
  }
}

Over MCP the polarity is the other way round: render_document is strict unless you pass strict: false, so an assistant asks you for a value rather than inventing one. Over REST the default is off, so an existing integration keeps behaving the way it did.

Defaults and examples are different things

Every blank (a variable in the API) carries two value fields, and only one of them ever reaches a real document.

default

Filled in before the render

Set in the editor on the blank's definition. Applied to every render entry point before validation runs: absent, null and empty-string values take the default, and a defaulted value never counts as missing, not even in strict mode. Under a list, the default fills every row that lacks the value. An absent list is never invented.

example

Preview only, never rendered

Sample data, auto-filled so the editor preview has something to show and so the API snippet in the app has a realistic body. It is never injected into a real render. Leave a blank out of your payload and the document shows nothing there, plus a warning; it never shows the sample.

Reading both from the API

GET /api/v1/templates/{id}/form-schema returns the field tree a form can be generated from: the dotted key where each value belongs, its declared type, whether it is required, and both value fields. Definitions the document does not read are listed separately under unused and are never required.

Both fields are set in the editor, not over the API: creating or updating a template through POST or PATCH /api/v1/templates writes the name and the document schema only.

Request
curl -sS https://app.docmake.io/api/v1/templates/tmpl_ow4Jl7QC7tEw/form-schema \
  -H "Authorization: Bearer dm_xxx"
Response, trimmed to two fields
{
  "template_id": "tmpl_ow4Jl7QC7tEw",
  "name": "oferta",
  "fields": [
    {
      "key": "date",
      "name": "date",
      "label": "date",
      "type": "date",
      "required": true,
      "default": null,
      "example": "2026-09-30"
    },
    {
      "key": "discount_percent",
      "name": "discount_percent",
      "label": "discount_percent",
      "type": "number",
      "required": true,
      "default": null,
      "example": "3"
    },
    ...
  ],
  "unused": [ ... ]
}

These two blanks have no default, so leaving them out of a payload renders nothing and warns. The examples, 2026-09-30 and 3, only ever appear in the editor preview.

Rules and calculations

Both are built with pickers in the editor, and both end up as nodes in the document schema you can read and write over the API. Neither needs anything extra in your payload: you send the values, the document does the arithmetic and the choosing.

Only when: a rule that shows or hides

A show-if rule (a docmakeConditional node) wraps part of the document and keeps it only when the rule holds. A rule is either one comparison (field, op, value) or a group with logic set to and or or over a non-empty conditions array. Groups nest up to five deep.

A rule that cannot be read is named rather than ignored: invalid_condition for a missing field, a logic word that is not and/or, an empty group or one nested too deep; invalid_operator for an operator outside the ten below; missing_value for a comparison with no value to compare against. All three are errors, so the render is refused with render_error rather than guessed at.

A rule in the schema
{
  "type": "docmakeConditional",
  "attrs": {
    "condition": { "field": "discount_percent", "op": "gt", "value": "0" }
  }
}
op Reads as Note
eq is Loose comparison, so the number 10 and the text "10" match.
neq is not The opposite of eq.
gt is more than False unless the value in the data is numeric.
gte is at least False unless the value in the data is numeric.
lt is less than False unless the value in the data is numeric.
lte is at most False unless the value in the data is numeric.
in is one of The rule value must be an array.
nin is none of The rule value must be an array.
empty has no value True when absent, null, empty text or an empty list.
nempty has a value The opposite of empty.

Calculate: arithmetic in the document

A calculation (a docmakeFormula node) holds one expression: numbers, field paths, the four operators + - * /, brackets, and the aggregates SUM, AVG, COUNT, MIN and MAX over a list. Its name makes the result a field other calculations can read.

A path resolves innermost first. Inside a row repeated over items, qty is that row's own qty; outside it, SUM(items.line_total) adds up the row calculation across every row, and SUM(items.qty * items.unit_price) evaluates the product once per row and adds the results. A named calculation is read by name: total - total * discount_percent / 100.

Results print plainly: no trailing zeros, at most two decimals, the render locale's decimal separator, and no thousands grouping unless the calculation carries a number or currency format.

A line total in a repeated row
{
  "type": "docmakeFormula",
  "attrs": {
    "id": "f_line",
    "name": "line_total",
    "label": "Line total",
    "expression": "qty * unit_price",
    "formatters": []
  }
}
The rendered document, as text
Quote for Northwind Trading SRL

 Product                          Qty                Unit price   Line total
 Consulting                       1                  2500         2500
 Internal audit                   2                  800          1600
 Training                         3                  350          1050

Total: 5150

Total after discount: 4635

Three rows, a line total each, SUM(items.line_total) underneath, then total minus its percentage. Nothing in the payload carried a total.

When a calculation cannot finish

A missing, empty or non-numeric operand stops the calculation, and so does dividing by zero. It renders blank and warns; it never quietly sums the rows it could read. The warning names the calculation and the operand that stopped it, and the failure travels up: a broken row total takes the grand total with it.

The warning type says which of the three it was: missing_data, type_mismatch or division_by_zero.

An expression that is not readable at all is a different thing: it is an error, not a warning, and the render is refused. POST /api/v1/templates/{id}/validate reports the same thing without rendering, so you can check a template in CI.

One unit price left out, non strict
[
  {
    "node_id": "v_price",
    "node_kind": "variable",
    "node_label": "unit_price",
    "type": "missing_data",
    "message": "items[1].unit_price has no value and rendered blank",
    "severity": "warning",
    "path": "items[1].unit_price"
  },
  {
    "node_id": "f_line",
    "node_kind": "formula",
    "node_label": "Line total",
    "type": "missing_data",
    "message": "items[1].line_total could not be calculated: unit_price has no value",
    "severity": "warning",
    "path": "unit_price"
  },
  {
    "node_id": "f_total",
    "node_kind": "formula",
    "node_label": "Total",
    "type": "missing_data",
    "message": "total could not be calculated: unit_price has no value",
    "severity": "warning",
    "path": "unit_price"
  },
  {
    "node_id": "f_net",
    "node_kind": "formula",
    "node_label": "Total after discount",
    "type": "missing_data",
    "message": "total_after_discount could not be calculated: unit_price has no value",
    "severity": "warning",
    "path": "unit_price"
  }
]

Four warnings from one gap: the blank itself, its row total, the sum, and the discounted total.

Checking a template
curl -sS -X POST https://app.docmake.io/api/v1/templates/tmpl_xxx/validate \
  -H "Authorization: Bearer dm_xxx" \
  -H "Content-Type: application/json" \
  -d '{"data":{"client":{"name":"Northwind Trading SRL"},"discount_percent":10,
        "items":[{"product":"Consulting","qty":1,"unit_price":2500}]}}'
Response
{
  "valid": false,
  "errors": [
    {
      "node_id": "f_line",
      "node_kind": "formula",
      "node_label": "Line total",
      "type": "invalid_formula",
      "message": "Multiply by what?",
      "severity": "error",
      "path": null
    }
  ]
}

The calculation read "qty * " with nothing after the operator. valid is false, so a render would be refused with render_error.

Fonts

Twenty families ship with every workspace, four faces each: regular, bold, italic and bold italic, so bold and italic are real cuts rather than something the renderer fakes. Text with no font set renders in Inter. The PDF renderer reads faces off disk and never fetches one over the network, so a render never waits on a font CDN. The time it actually took comes back in X-DocMake-Render-Time-Ms.

Sans

  • Inter
  • Arial
  • Calibri
  • Roboto
  • Open Sans
  • Lato
  • Montserrat
  • Poppins
  • Nunito
  • Source Sans 3

Serif

  • Times New Roman
  • Georgia
  • Cambria
  • Merriweather
  • Lora
  • Playfair Display
  • Source Serif 4

Monospace

  • Courier New
  • Roboto Mono
  • JetBrains Mono

Microsoft names, free twins

Six of the bundled families are Microsoft names that cannot be redistributed. They render in metric compatible substitutes: the same widths and line breaks, so a page keeps its shape, and the name in the schema and in the DOCX stays the one you picked.

Family in your template Rendered with
Arial Arimo
Times New Roman Tinos
Courier New Cousine
Calibri Carlito
Cambria Caladea
Georgia Gelasio

Your own fonts

Upload a brand font under Fonts in the app: TTF, OTF, WOFF or WOFF2, up to 5 MB per file and 25 files per workspace, one upload per weight and style. Uploads are per workspace and are not part of the REST API.

What ends up in the file

  • • PDF: every face the document actually uses is embedded in the file as base64, bundled and uploaded alike, plus Inter for anything unstyled. Faces the document does not use are not embedded.
  • • DOCX: only your own uploads are embedded, as obfuscated font parts the way Word writes them, so a recipient who does not have the font still sees it. Bundled families are deliberately not embedded; Word resolves them locally.
  • • A family that is neither bundled nor uploaded has nothing to embed. The editor surfaces it so you can upload the real font instead of finding out from a customer.

Limits, keys and workspaces

Rate limit

One per-minute budget per workspace: 30 requests a minute on Free, 120 on Pro, 600 on Enterprise. Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (a Unix timestamp), so an integration can pace itself rather than guess. Over budget is a 429 with Retry-After in seconds. One MCP message counts as one request against the same budget.

Document allowance

1,000 documents a month on Free and 10,000 on Pro, counted per workspace over the billing period. Templates are unlimited on every plan. Running out answers 429 with the quota body described above.

Keys and scope

A key you create in Settings → API Keys is shown once and stored only as a SHA-256 hash; the key your workspace starts with can be revealed again from that page, and every reveal is written to the team's activity feed. A key carries the workspace it was made in: every request acts as that workspace's owner and can reach nothing else. Two keys on Free, unlimited on Pro. Deleting a key stops it working on the next request. Send it as Authorization: Bearer dm_..., over HTTPS, never in a query string.

GET /api/v1/usage
{
  "data": {
    "plan": "free",
    "billing_period": {
      "start": "2026-09-02T11:57:42+00:00",
      "end": "2026-10-02T11:57:42+00:00"
    },
    "renders": {
      "used": 36,
      "limit": 1000
    },
    "templates": {
      "used": 4,
      "limit": null
    },
    "credits": {
      "used": 0,
      "limit": 20
    },
    "api_calls": {
      "used": 0,
      "limit": 500
    },
    "rate_limit": {
      "requests_per_minute": 30
    }
  }
}

Plan, billing window, documents used against the monthly allowance, template count, AI credits, and the per-minute rate limit for this workspace.

Endpoints

The whole surface. Everything authenticates the same way, with Authorization: Bearer dm_...

Method Path Description
GET /api/v1/templates List templates
POST /api/v1/templates Create a template from a document schema
GET /api/v1/templates/{id} Get a template, schema and variable definitions included
PATCH /api/v1/templates/{id} Update a template name or schema
DELETE /api/v1/templates/{id} Delete a template
POST /api/v1/templates/{id}/duplicate Duplicate a template
GET /api/v1/templates/{id}/form-schema The field tree the template expects, with type, default and example
POST /api/v1/templates/{id}/validate Check a template, and optionally a data payload, without rendering
POST /api/v1/templates/{id}/compute-aggregations Evaluate the calculations in a document against data
POST /api/v1/templates/import Import a DOCX file as a template
POST /api/v1/render Render one document, the response body is the DOCX or PDF file
POST /api/v1/render/batch Queue up to 100 documents as one job
GET /api/v1/render/jobs/{id} Batch job status, per item
GET /api/v1/render/jobs/{id}/download Download batch results, a file or a ZIP
POST /api/v1/assets Upload an image asset
GET /api/v1/assets List assets
DELETE /api/v1/assets/{id} Delete an asset
GET /api/v1/usage Plan, quota and rate limit for the current period

Run it in Postman

The DocMake API collection is public, with a request per endpoint and saved example responses in the shapes on this page, strict renders and warnings included. Fork it into your own workspace, set base_url and your dm_ key, and send the first render before you write any code.

Reading rather than running? The same collection is published as API reference documentation, request by request, alongside the rest of the public DocMake workspace.

Run in Postman

Prefer no code at all?

Connect Claude, Cursor, or any MCP client to DocMake with the official MCP server. The same six capabilities, the same key, and renders that refuse rather than invent.

Try it with 1,000 free documents a month

The Free plan includes full API access. No card, no expiry.

Get an API key