Back to Nodes

DocMint

Last updated Oct 15, 2026

Fill Word, Excel and PowerPoint templates with workflow data and get the file — or a PDF — back on the same node. The template's own placeholders become real n8n fields.

14 Weekly Downloads
280 Monthly Downloads

Included Nodes

DocMint

Description

n8n-nodes-docmint

Fill Word, Excel and PowerPoint templates with data from your workflow and get the
file — or a PDF, or both — back on the same node.

And the part nothing else does: **the node reads your template and turns its
placeholders into real n8n fields.** Typed, expression-capable, one per placeholder,
with the names taken from the document itself. No JSON textarea. No guessing the keys.

DocMint is the API behind it.

One published n8n template is built on this node, and it opens from inside n8n:
Regional sales reports from CRM
exports
.
A webhook takes a flat CRM export, Get Fields checks that the .xlsx template is on
your account, a Code node does the arithmetic, and Fill sends the filled workbook back
on the same request. You supply the .xlsx yourself: upload it once (see
Zero to a filled invoice) and put its name into the
template’s one Set node.

—

Why this node exists

Every document-generation product joins your data to your template by invisible string
matching, and none of them will tell you the field names before you render. So the loop
you are in today is:

> guess the keys → render → open the .docx in Word → find the silently-empty cell → guess again

This node breaks that loop in two places.

1. It shows you the placeholders. DocMint has GET /v1/templates/:name/fields, and the
node wires it to n8n’s resource mapper. Pick a template and the panel fills with the
placeholders that template actually contains — invoice_no, customer.name, issued,
items — each as its own input, with the right widget: {issued|date:...} is a date
field, {total|currency:EUR} a number field, {#items} a list.

The node guesses none of that. DocMint works the types out by **actually rendering the
template**, so what the form shows cannot drift from what a real render would do. Fields
written inside a {#section} are not offered at the top level — they belong to a row —
and the list field’s label says what each row needs: items (rows: description, qty,
unit_price)
. A list inside a list is marked orders (rows: ref, customer, lines[]).

2. It refuses to render a document with a hole in it. If a placeholder has no value,
DocMint answers 422 and names the placeholder and where in the document it is. The node
puts both in the red box in n8n:

> The template uses {customervat} but the data has no “customervat”.
> Placeholder “customer_vat”, at word/document.xml, paragraph 12. Did you mean
> “customervatid”? Fields the template can see here: invoiceno, customervat_id,
> total. Open the Data section: the placeholder named above is one of the fields
> listed there.

You never open Word to find out what went wrong.

If you already have a data object upstream, turn on Send Raw JSON Instead and pass it
through. The mapper is the default; the JSON blob is the escape hatch.

Install

> Installs on n8n Cloud and self-hosted. The node passed n8n’s verification and is
> published as a verified community node, so you no longer need to self-host to use it.
>
> From the nodes panel (Cloud or self-hosted): open a workflow → add node → search
> DocMint → pick it under More from the community → Install. n8n limits that
> button to the instance owner and admin accounts; everyone else on the instance can use
> the node once it is installed.

Self-hosted, from the n8n UI

Settings → Community Nodes → Install → n8n-nodes-docmint

Self-hosted, from the CLI

cd ~/.n8n/nodes
npm install n8n-nodes-docmint

Then restart n8n.

The package is published from
this repository’s GitHub Actions workflow with an npm
provenance attestation, so npm audit signatures can verify which commit built it.

Credential

1. Create an account at . Your API key is shown
once, immediately.
2. In n8n, add a DocMint API credential and paste the key. It starts with dmlive.
3. Press Test. The credential checks itself against GET /v1/usage, which is
authenticated, costs no credits and creates no file.

Base URL only needs changing if you run your own DocMint instance.

Zero to a filled invoice

You need one thing to start: a .docx with placeholders in it. Open Word, type this, save
it as invoice.docx:

INVOICE {invoice_no}
Date: {issued|date:DD MMMM YYYY}

Bill to: {customer.name} {customer.address}

{#items} {description} {qty} x {unitprice|currency:EUR} = {linetotal|currency:EUR} {/items}

Total: {items|sumProduct:qty:unit_price|currency:EUR}

Step 1 — upload it once.

Add a DocMint node, set Resource to Template and Operation to Upload.
Point Input Binary Field at the property holding the file (data if a Read/Write
Files node put it there) and set Name to invoice. Execute it once. The response
tells you the format it detected and lists the placeholders it found.

You never have to do this again — uploading the same name later adds a version rather
than replacing the file, and old versions can be rolled back.

Step 2 — fill it.

Add a second DocMint node after whatever produces your data. Leave Resource on
Document and Operation on Fill, then open the Template dropdown and choose
invoice.

The Data section now lists the placeholders the document actually contains. Fill them
in, or drop an expression into each:

| Placeholder | Value |
|—|—|
| invoice_no | {{ $json.number }} |
| issued | {{ $now }} |
| customer.name | {{ $json.customer }} |
| customer.address | {{ $json.address }} |
| items (rows: description, qty, unit_price) | {{ $json.lines }} |

Set File Name to invoice-{invoice_no} and execute.

The output item carries the filled .docx in the data binary field, named
invoice-INV-2026-0001.docx, ready for Gmail, Drive, S3, Slack or an HTTP Request. No
second download step.

Prefer the whole item mapped automatically? Switch the Data section’s mode to **Map
Automatically** and the incoming item’s JSON is sent as the data.

Operations

Document

| Operation | What it does |
|—|—|
| Fill | Fill a template with data and return the document, a PDF, or both |
| Inspect | List the placeholders a template contains, and where each one is written |

Inspect is worth wiring up on its own. Point it at a template and execute. The output
item carries:

  • fields — the typed, scoped tree: every placeholder with its type, the scope it
  • lives in, whether it repeatings, whether it is required, how often it is used, and
    the locations it appears at.

  • tags — what is literally written in the file, with the location of each
  • (word/document.xml, paragraph 12, Sheet1!C7, slide 3, shape "Title").

  • sample_data — a skeleton payload DocMint built by rendering the template.
  • sample_data is the shortcut. Wire Inspect into a Fill node, turn on **Send Raw JSON
    Instead**, and pass {{ $json.sample_data }}: you get a real document out of an unfamiliar
    template in one step, then replace the dummy values one at a time.

    That the skeleton actually renders is a test, not a hope: DocMint’s suite fills every one
    of its 21 fixture and example templates with the sample_data its own API returned for
    them, and all 21 succeed. It was 20 until the case that failed was fixed — a template
    multiplying two fields that nothing else typed as numbers, where the skeleton offered text
    and sumProduct rightly refused it. Formatter arguments are now read as type facts, so
    {items|sumProduct:qty:unit_price} makes qty a number even though no other tag says so.

    Template

    | Operation | What it does |
    |—|—|
    | Upload | Upload a .docx, .xlsx or .pptx from a binary field, or add a version to an existing template |
    | Get | One template: format, size, current version and its version history |
    | Get Many | The templates on the account |
    | Get Fields | The placeholders a stored template needs |
    | Download | The template file itself, as binary |
    | Delete | Delete a template and every version of it |
    | List Versions | Every stored version, with size, note and date |
    | Rollback | Restore an earlier version as the current one |

    Account

    | Operation | What it does |
    |—|—|
    | Get Usage | Plan, quota, documents remaining, and this month’s breakdown |

    The node is marked usableAsTool, so an n8n AI Agent can call it directly — give the
    agent the node and it can produce a document as part of its own reasoning.

    Many documents in one call

    The node has no batch operation. Wired normally, Fill runs once per incoming item, so
    a month-end run of 500 invoices is 500 executions and 500 HTTP round trips against a
    service that restarts on deploy.

    The API grew two endpoints for this after the node was written, and an HTTP Request
    node reaches both today. They are not node operations, so nothing below is filled in for
    you — but they are the answer if the loop is too slow.

    Synchronous — POST /v1/render/batch. One template, up to 100 datasets, one response.

    {
      "template": "invoice",
      "output": "document",
      "items": [
        { "filename": "invoice-1", "data": { "invoice_no": "INV-2026-0001", "…": "…" } },
        { "filename": "invoice-2", "data": { "invoice_no": "INV-2026-0002", "…": "…" } }
      ]
    }
    

    Set "response": "zip" for one archive instead of JSON, and "on_error": "continue" to
    let the good rows through instead of failing the call. Both together return a ZIP holding
    the documents that rendered plus an errors.json naming the ones that did not, and the
    response carries x-docmint-batch-ok and x-docmint-batch-failed headers so a workflow
    can branch without opening the archive. Only the rows that produced a document are
    charged. A synchronous batch is capped at 20 items when output is pdf, because that
    stage is the slow one.

    Queued — POST /v1/jobs. The same body, plus an optional webhook_url. It answers
    202 immediately:

    {
      "id": "job_…", "status": "queued", "kind": "batch", "count": 4, "output": "pdf",
      "statusurl": "https://docmint.app.mintapis.com/v1/jobs/job…",
      "credits": { "reserved": 8, "remaining": 22, "limit": 30 }
    }
    

    Poll status_url (or wait for the webhook) until status is succeeded. The finished
    job carries a per-item report and a single ZIP whose download URL lives for 24 hours.
    Credits are reserved when the job is queued and whatever it does not produce is given
    back. The webhook is signed HMAC-SHA256 over {timestamp}.{body} in the
    X-DocMint-Signature header, is retried three times, and must be publicly reachable —
    a localhost URL is refused by name, with privateaddressblocked, so an n8n Webhook
    node’s production URL is the one to use rather than its test URL.

    GET /v1/capabilities needs no key and reports the live limits of the deployment you are
    calling, including maxbatchitems and maxsyncbatchpdfitems.

    Choosing the template

    The Template field has three modes:

  • From List — a searchable dropdown of the templates on your account, each showing its
  • format and how many placeholders it has.

  • By Name — plain text, so an expression can pick the template at runtime:
  • {{ $json.docType }}.

  • Upload / Inline — the name of a binary field on the item holding the template file.
  • It is sent with the request and nothing is stored on your account. Useful when the
    template itself comes from Drive or an email attachment.

    In Upload / Inline mode the placeholders are only known once the workflow runs, so the
    Data section cannot list them in advance. It says so, and points you at **Send Raw JSON
    Instead or at the Inspect** operation.

    Office file, PDF, or both

    Output is a per-request choice, not a property of the template.

    | Output | You get |
    |—|—|
    | Office File (default) | The filled .docx, .xlsx or .pptx, in the data binary field |
    | PDF | The document converted to PDF, in the data binary field |
    | Both | Two binary fields on one item: the Office file in data and the PDF in pdf |

    With Both, the item’s JSON also carries binaryFields: { document: "data", pdf: "pdf" },
    so the next node does not have to guess where they went. Both field names are configurable
    under Options.

    A PDF costs one credit more than the Office file, because converting it costs DocMint
    about a hundred times the CPU of the fill itself: measured against production on
    2026-09-20, four invoices filled in 93 ms together and then took 12.9 s to convert — about
    2.6 s of LibreOffice each, against about 23 ms each to fill. Nothing else is metered —
    downloading a file you already generated is free.

    Template syntax

    Both delimiter styles work, in every format, always: {name} and {{name}}. A template
    written for docxtemplater and one written for a Handlebars-based tool both work unchanged.

    | Form | Meaning |
    |—|—|
    | {name} | a value |
    | {user.email}, {items.0.sku} | dotted path, numeric index |
    | {{[Customer Name]}} | a field name containing spaces |
    | {#items} … {/items} | section: once per array element; once for an object; once for a truthy scalar; never for an empty array |
    | {^items} … {/items} | inverted section: renders only when absent, empty or falsy |
    | {/} | closes the innermost open section |
    | {.} | the current item, for arrays of scalars |
    | {../x} | one scope outwards |
    | {$index} {$index1} {$first} {$last} {$length} | loop metadata |
    | {%logo} | an image, from a URL or base64 |
    | {@rawXml} | raw OOXML, unescaped |
    | {!note} | a comment, removed from the output |
    | {price|currency:EUR} | a formatter pipeline |

    Anything between braces that does not parse as a tag is left alone, so a document
    containing { "total": 12 } or .a { color: red } renders unchanged.

    Formatters — all 43, as /v1/capabilities reports them:

    Values and lists: add count default divide filter groupBy join limit
    multiply reject reverse round skip sort subtract sum sumProduct unique

    Text and numbers: currency lower number ordinal percent title trim upper
    yesno

    Comparisons: contains empty eq gt gte lt lte ne notEmpty

    Dates: after before date daysSince daysUntil future past

    The comparison and date groups were missing from this list until 2026-09-20, though the
    count was right. They are most useful inside a section, where they read as a condition:
    {#total|gt:1000}Free delivery{/total}, {#due|past}OVERDUE{/due}.

    {items|sum:amount} and {items|sumProduct:qty:price} are the ones that matter: a total
    in a document should be computed from the data, never typed in. Formatters work on
    sections too — {#items|filter:active|sort:due_date} — so the workflow does not have to
    pre-sort a list before it gets here.

    The live list for your deployment is at
    , which is generated from the running
    code rather than written by hand.

    Options

    Everything below is optional and lives under Options on the Fill operation.

    | Option | Default | Notes |
    |—|—|—|
    | Locale | en-US | BCP 47 tag deciding how numbers, dates and currencies are written |
    | Currency | USD | Three-letter ISO code used by currency when the template names none |
    | Time Zone | UTC | IANA zone the date formatter renders in |
    | On Missing Placeholder | Fail and name it | Or leave empty, or keep the tag visible for debugging |
    | Strict Scope | off | Fail when a placeholder inside a loop is not a field of the row |
    | Template Version | current | Render an older version of a stored template |
    | Put Office File in Field | data | Binary field name |
    | Put PDF in Field | pdf | Binary field name, used when Output is Both |

    Strict Scope, and the bug it catches

    Write {#items}{name}{/items} where name exists only at the top level of your data and
    most document generators will happily print the same value on every row and tell you
    nothing. DocMint allows it — {currency} from the invoice root should resolve inside a
    loop — but reports every occurrence back as a resolvedfromouter_scope warning naming
    the placeholder, the location, and the fields the row actually has. Turn Strict Scope
    on and it becomes an error instead.

    Errors

    The API answers every failure with a machine code, a sentence, a hint, and — for a
    placeholder that would not resolve — the field name and its location in the document. The
    node puts the sentence in the error title and everything else in the description.

    Turn on Settings → Continue On Fail to route bad items down the error branch instead.
    The failed item carries the API’s own fields, so an IF node can branch on
    $json.error.code, $json.error.field or $json.error.location:

    {
      "error": {
        "code": "placeholder_unresolved",
        "message": "The template uses {customervat} but the data has no "customervat".",
        "hint": "Did you mean "customervatid"? …",
        "field": "customer_vat",
        "location": "word/document.xml, paragraph 12",
        "available": ["invoiceno", "customervat_id", "total"],
        "docs": "https://docmint.app.mintapis.com/docs#errors",
        "request_id": "51e5390e4107f566",
        "httpCode": "422"
      },
      "errorMessage": "The template uses {customervat} but the data has no "customervat"."
    }
    

    ratelimited (429), pdfqueuefull (503) and pdftimeout (504) are all worth
    retrying — turn on Settings → Retry On Fail and n8n handles them.

    A credit taken for a render that then fails is refunded, so a run that errors on a missing
    field costs you nothing.

    Limits

    | | |
    |—|—|
    | Formats filled | .docx, .xlsx, .pptx |
    | Max template | 25 MB |
    | Max data | 8 MB |
    | Max request body | 36 MB |
    | Versions kept per template | 20, oldest pruned first, never below one |
    | PDF conversion timeout | 90 s |
    | Requests per minute, per account | 120, with a burst of 30 |
    | Items in one batch | 100, or 20 when the output is PDF |
    | Queued job result kept | 24 hours for the ZIP, 7 days for the job record |

    Template names are 1–64 characters of lower-case letters, digits, dot, dash and
    underscore. Macro-enabled files (.docm, .xlsm, .pptm) are filled, but the macros are
    not guaranteed to survive and the file comes back with the plain content type — the render
    response says so in its warnings.

    Pricing

    | Plan | Price | Documents / month |
    |—|—|—|
    | Free | $0 | 30 |
    | Starter | $9 | 2,000 |
    | Pro | $29 | 20,000 |
    | Scale | $99 | 100,000 |

    A document costs one credit; asking for a PDF costs one more. Nothing else is metered.

    For scale: Docupilot’s published Starter plan is $29/month for 100 documents, and it bills
    per delivery — “if you generate an invoice and send it via email to recipients while also
    uploading it to Google Drive, it will use 2 credits”, and “Downloading generated document
    also consumes 1 credit”. DocMint bills once per document produced.

    What DocMint does not do

    Stated plainly, because a README that only lists strengths is not much use:

  • No richtext, QR codes or maps. HTML or Markdown into a Word document is not
  • supported. Docupilot has all three.

  • LibreOffice only for the PDF step. No PDF/A, no watermarking, no PDF encryption, no
  • choice of converter. Carbone offers all of those.

  • A smaller formatter library than Carbone’s, which also has aggregates
  • (aggSum, cumSum), an i18n system, live currency conversion and block operators
    (drop/keep). DocMint has 43 formatters including value and date comparisons; Carbone
    has more.

  • One error per render. If two placeholders are wrong, DocMint names the first,
  • you fix it, and the next render names the second. docxtemplater reports them all at once.

  • The node renders one document per item — batching and queueing are API-only.
  • The node has four operations (Fill, Inspect, the Template set, Get Usage) and no batch
    or job operation, so 500 invoices are 500 executions of the Fill node. The API itself
    has since grown both: POST /v1/render/batch takes up to 100 datasets against one
    template in a single call, and POST /v1/jobs queues the same request and answers 202
    with a status_url, then delivers a ZIP and optionally an HMAC-signed webhook. Until
    the node exposes them, an HTTP Request node reaches them in one step. See
    Many documents in one call.

  • PDF conversion still runs one at a time. Measured against the production instance
  • on 2026-09-20: a queued batch of four PDFs took 13.4 s wall clock, of which the PDF
    stage was 12.9 s — about 2.6 s per conversion, serialised — while filling all four
    .docx files took 93 ms together. Queueing removes the round trips and the risk of
    losing your place in a loop; it does not make LibreOffice faster. If you need hundreds
    of PDFs at once that is still a real limit; ask for the Office file instead, which is
    not throttled and took about 23 ms per document in the same run.

  • No delivery integrations. DocMint returns the file and lets n8n do the emailing,
  • uploading and storing — which is the right split for a workflow tool, but it is a
    difference rather than a strict advantage.

  • Types are as good as the template is explicit. A field only DocMint’s formatters
  • describe gets described well: {total|currency:EUR} is a number, {issued|date:...} a
    date. A bare {qty} is text, even where the template later multiplies it, so the form
    offers a text box and the arithmetic is checked at render time instead.

  • Against a DocMint older than the typed field tree — an out-of-date self-hosted
  • instance — the node falls back to listing every placeholder as text at the top level,
    with no types and no row grouping. It does not guess to fill the gap.

    Links

  • Docs:
  • Dashboard:
  • Capabilities of the running build:
  • Issues:

Generating PDFs from HTML, Markdown or a URL instead? That is
n8n-nodes-pdfmint, by the same author.

Compatibility

Built and tested against n8n-workflow 2.35.3 on Node.js 20. The engines field asks for
Node 20.15 or newer, matching n8n’s own floor. The package has **zero runtime
dependencies** — it is a thin HTTP client, which is what makes it eligible for n8n
verification at all.

Licence

MIT

Verification video, submitted 2026-08-25 — review passed

Verified. n8n’s registry lists n8n-nodes-docmint as published since
2026-09-07 (isPublished: true, checked again on 2026-09-20), which is why the
Install section above says the node installs on Cloud. The heading below is kept
because the takes are worth reading, not because the outcome is still open.

Recorded against n8n’s own checklist, read off the portal rather than guessed:
install from npm (0.1.0, the submitted version), new workflow, credential **with
the connection test shown passing**, the common action, and the node used as an
AI-agent tool. 3.9 minutes, no cuts — Playwright records the browser context, so
the file is continuous by construction.

recorder/ holds the script. Nine takes; each failure is worth knowing:

| Take | What broke |
| — | — |
| 1 | the risks checkbox never ticked, so Install stayed disabled — the step now asserts the button became enabled instead of assuming the click landed |
| 2 | text= mixed into a CSS selector list; Playwright syntax cannot be |
| 3 | the action is “Fill document template”, not “Fill” |
| 4 | the credential button reads “Connect to DocMint” in this n8n build |
| 5–6 | the template dropdown items are not el-select-dropdown__item; matched on text instead |
| 7 | the chat-model handle was picked by DOM order, which includes the agent’s main output handle — the agent then errored “A Chat Model sub-node must be connected” |
| 8 | the agent asked a clarifying question instead of calling the tool: the prompt left the output format open |
| 9 | the expression editor auto-closes braces, so a typed }} produced }} }} |

One thing the video does not claim. $fromAI reached the tool but never
carried the field values, and DocMint correctly refused a fill with no data. The
tool’s data is therefore pinned, and the agent decides when to call it — which
is what n8n asks to see. The caption says exactly that and no more.