Product engineers spend hours writing boilerplate to generate invoices, contracts, and reports. I was one of them. So I built an API that handles the entire pipeline — upload template, fill with data, return PDF — and exposed it as MCP tools so AI agents can call it directly.
The Problem: docxtpl Breaks With Real Templates
You have a Word template for invoices. It has a table with line items. You need to fill it with data from your API.
You reach for docxtpl. You throw in some Jinja2 syntax. And… it breaks.
from docxtpl import DocxTemplate
template = DocxTemplate("invoice.docx")
context = {
"client": "Acme Corp",
"items": [
{"name": "Design", "qty": 10, "rate": 150},
{"name": "Development", "qty": 20, "rate": 175},
{"name": "Review", "qty": 5, "rate": 120},
]
}
template.render(context) # Table collapses. XML is garbage.
template.save("invoice-filled.docx")
Enter fullscreen mode Exit fullscreen mode
The table collapses. The PDF is garbage. You Google “docx to pdf python” and find 47 Stack Overflow answers that all say “just use HTML templates.”
Here’s why it breaks: Word’s XML isn’t HTML. A table row isn’t a <tr> tag. It’s <w:tr> wrapped in <w:tbl>, with properties, cell definitions, and spans all entangled. When docxtpl expands a {% for %} loop across rows, it doesn’t understand the XML boundaries. It copies text. The row structure breaks. LibreOffice throws an error.
Most devs give up here. They either manually duplicate rows, switch to HTML templates and forget about Word, or accept that DOCX templates only work for flat documents without loops.
I refused to accept any of those options.
The Fix: Upload, Fill, Download
I built a pipeline that handles the entire lifecycle:
.docx upload → validate ZIP → extract schema → expand loops → render → convert to PDF → store
Enter fullscreen mode Exit fullscreen mode
But here’s the thing: you don’t need to understand any of that. You just need three API calls:
# 1. Upload your template
curl -X POST https://api.docuqueue.com/templates/upload \
-H "Authorization: Bearer dq_..." \
-F [email protected]
# Returns: { "template_id": "tpl_abc123", "fields": ["client", "items"] }
# 2. Fill it with data
curl -X POST https://api.docuqueue.com/templates/tpl_abc123/fill \
-H "Authorization: Bearer dq_..." \
-H "Content-Type: application/json" \
-d '{
"client": "Acme Corp",
"items": [
{"name": "Design", "qty": 10, "rate": 150},
{"name": "Development", "qty": 20, "rate": 175},
{"name": "Review", "qty": 5, "rate": 120}
]
}'
# Returns: { "job_id": "job_xyz789", "status": "processing" }
# 3. Download the PDF
curl -X GET https://api.docuqueue.com/jobs/job_xyz789/pdf \
-H "Authorization: Bearer dq_..." \
-o invoice-filled.pdf
Enter fullscreen mode Exit fullscreen mode
That’s it. The XML manipulation, the loop expansion, the LibreOffice conversion — all handled. You get a PDF.
The MCP Angle: 12 Tools for AI Agents
I exposed this same pipeline as MCP tools. Twelve total, but here are the ones product engineers actually use:
Tool What It Does When You’d Use Itupload_template
Upload a .docx or .html template
Once, when you have a new template
get_schema
See what fields the template expects
Before filling, to validate data
fill_template
Fill template with data, get PDF
Every time you generate a document
preview
Quick preview without full render
When iterating on template design
list_templates
See all your uploaded templates
When you have many templates
get_status
Check if a job is done
For async generation
download_pdf
Get the finished PDF
After fill completes
delete_template
Remove a template
When cleaning up
The workflow for an AI agent looks like this:
Agent: "Generate an invoice for Acme Corp with 3 line items"
→ upload_template(invoice.docx)
→ get_schema(template_id)
→ fill_template(template_id, {client: "Acme Corp", items: [...]})
→ download_pdf(job_id)
→ return PDF to user
Enter fullscreen mode Exit fullscreen mode
The agent doesn’t need to know about XML, LibreOffice, or any pipeline internals. It just uploads a template and gets a PDF back.
Authentication: OAuth 2.0 with PKCE
Every tool call goes through OAuth 2.0 with PKCE. No API keys stored in code. Agents authenticate once, get a scoped token, and use it for all operations.
This matters because you don’t want an AI agent generating arbitrary documents from arbitrary templates without guardrails. Each token is scoped to the agent’s permissions. If you only want it to fill invoices, that’s all it can do.
What’s Under the Hood
The pipeline handles edge cases you’d hit in production:
- ZIP validation. Users upload corrupted files, renamed .doc files, or PDFs with .docx extensions. I check the ZIP structure before touching anything.
- Loop expansion. The custom step that makes docxtpl work. Walks the XML tree, finds loop constructs in table rows, clones them to match your data.
- Dual storage. Recent previews go into Redis with a 1-hour TTL (instant re-preview). Permanent copies go to Cloudflare R2.
- Credit refund on failure. If generation fails, the credit goes back atomically with an audit log. No stale balances.
Try It
If you’re building something that needs to generate PDFs from templates, check out DocuQueue. There’s a free tier that covers most small use cases.
The MCP tools are available if you want AI agents to interact with the pipeline directly. Upload a template, call the fill tool, get a PDF. That’s the whole API.
The DOCX problem isn’t solved perfectly. No one has solved it perfectly. But the row expansion trick makes it work reliably for the vast majority of real-world templates. And that’s enough to build on.
Found this useful? I’m @arun on here. The code behind DocuQueue is a constant source of “why does this work this way” blog posts. More coming soon.