Template Generator from JSON or XML
Paste a sample of the data your workflow already produces. Get back a document template that renders it — designed by AI, refined by you, then filled on every run with a single API call.
Two Directions, Two Kinds of Template
DocButterfly has templates that run in opposite directions. It's worth being clear which one you want:
| Document AI template | Template Generator template | |
|---|---|---|
| Direction | Document → data | Data → document |
| You start with | A PDF you upload | A JSON or XML sample |
| You define | Regions drawn on the page | A field contract |
| You get back | Extracted values | A rendered document |
| Used for | Reading invoices you receive | Producing invoices you send |
The End-to-End Pipeline
A production document run has three legs: connect to the data, fill the template, deliver the result. The middle leg is what the walkthrough below covers in detail; the other two are covered in Where the Data Comes From and Where the Document Goes.
Authoring happens once, interactively. After that every leg of this diagram is a single HTTPS call — the pipeline runs unattended, one document per ComposeFill.
Step 1 — Analyze Your Sample Data
AnalyzeTemplateData flattens your payload into a field contract: every value gets a dot-path, an inferred datatype, and a sample. Arrays become repeating tables. This step makes no AI call and costs 1 token.
curl -X POST https://your-app.azurewebsites.net/api/AnalyzeTemplateData \
-H "X-API-Key: df_your_key" \
-H "Content-Type: application/json" \
-d '{
"data": {
"invoice": { "number": "INV-1001", "issuedOn": "2026-07-28", "total": 1240.50, "paid": false },
"customer": { "name": "Acme Ltd", "email": "ap@acme.com" },
"lines": [
{ "description": "Consulting", "qty": 10, "amount": 100.00 },
{ "description": "License", "qty": 1, "amount": 240.50 }
]
}
}'
The response tells you what was understood:
{
"success": true,
"source": "json",
"fields": [
{ "name": "invoice.number", "label": "Number", "dataType": "string" },
{ "name": "invoice.issuedOn", "label": "Issued On", "dataType": "date", "format": { "dateStyle": "medium" } },
{ "name": "invoice.total", "label": "Total", "dataType": "currency", "format": { "currency": "USD", "decimals": 2 } },
{ "name": "invoice.paid", "label": "Paid", "dataType": "boolean" },
{ "name": "customer.email", "label": "Email", "dataType": "email" }
],
"tables": [
{ "name": "lines", "label": "Lines", "sampleRowCount": 2,
"columns": [
{ "name": "description", "dataType": "string" },
{ "name": "qty", "dataType": "number" },
{ "name": "amount", "dataType": "currency" }
] }
],
"warnings": []
}
total became currency and qty did not. Datatypes come from the value and the key name. A number under a key like total, amount, price, or balance is treated as money; anything else stays a plain number. You can override any of it in the designer.
Step 2 — Generate the Template
GenerateTemplateFromData takes the same payload and returns a complete HTML template. Costs 5 tokens, once, per template you author.
curl -X POST https://your-app.azurewebsites.net/api/GenerateTemplateFromData \
-H "X-API-Key: df_your_key" \
-H "Content-Type: application/json" \
-d '{
"data": { "...same payload as above..." },
"documentType": "invoice",
"title": "Invoice",
"pageSize": "Letter"
}'
The generated template uses two placeholder forms:
{{invoice.number}}— a single value, HTML-escaped and formatted per its datatype.<!--{{#lines}}--> … <!--{{/lines}}-->— a repeating section. Inside it, columns are referenced by bare name:{{description}}.
{{#lines}} placed directly inside a <tbody> is text in a table context, and every HTML parser relocates it outside the table — which silently drops every line item. Comments are preserved in place. Keep the comment wrappers if you hand-edit the HTML.
Generated markup is sanitized before it is returned or stored: scripts, event handlers, external stylesheets, remote images, and unsafe URL schemes are stripped. Images may only be data: URIs or https: URLs — headless rendering has no network access, so a remote reference would render as a broken image in your production PDF.
The Same Thing in XML
Send dataXml instead of data. Everything downstream is identical.
curl -X POST https://your-app.azurewebsites.net/api/GenerateTemplateFromData \
-H "X-API-Key: df_your_key" \
-H "Content-Type: application/json" \
-d '{
"dataXml": "<invoice><number>INV-1001</number><issuedOn>2026-07-28</issuedOn><total>1240.50</total><lines><line><description>Consulting</description><qty>10</qty><amount>100.00</amount></line><line><description>License</description><qty>1</qty><amount>240.50</amount></line></lines></invoice>",
"documentType": "invoice"
}'
<lines><line/></lines> with a single <line> is indistinguishable from a plain object. With one row it becomes a group of fields; with two or more it is correctly detected as a repeating table. If your sample only has one, add a second — or mark it as a table in the designer.
Coded Values — Showing the Label, Not the Key
Business systems store a choice as a key and display it as a label. A Dynamics 365 or Dataverse
choice column holds 100000001; a Field Service inspection answer holds
needs_replacement. Send that to a template as-is and the finished PDF says
needs_replacement in front of your customer.
Do not pre-format the code in your flow or package. That fixes exactly one caller.
Tell DocButterfly what the codes mean instead, and the label is applied by the fill engine —
so Power Automate, Logic Apps, SSIS, Data Factory and a plain curl posting the
same JSON all produce the same document.
Option 1 — paste the form definition when you build the template
Add dataDefinition to AnalyzeTemplateData or
GenerateTemplateFromData. Any question carrying a choices list is
recognized, and the lists are stored on the saved template, so later fills need nothing extra.
{
"data": { "unit_type": "rooftop_unit", "filter_condition": "needs_replacement" },
"dataDefinition": {
"pages": [{ "elements": [
{ "name": "unit_type", "choices": [
{ "value": "rooftop_unit", "text": "Rooftop unit (RTU)" },
{ "value": "split_system", "text": "Split system" } ] } ] }]
}
}
Option 2 — let the lists travel with the payload
Put them under the reserved __choices key at the top level of your data. This is the
one that works everywhere: it is read when the template is built and on
ComposeFill, including an inline template that has no saved contract to read from.
{
"unit_type": "rooftop_unit",
"filter_condition": "needs_replacement",
"__choices": {
"unit_type": { "rooftop_unit": "Rooftop unit (RTU)", "split_system": "Split system" },
"filter_condition": { "needs_replacement": "Needs replacement (part on order)" }
}
}
The list accepts any of these shapes — use whichever your source system exports:
[{ "value": "rooftop_unit", "label": "Rooftop unit (RTU)" }][{ "value": "rooftop_unit", "text": "Rooftop unit (RTU)" }](SurveyJS / Field Service){ "rooftop_unit": "Rooftop unit (RTU)" }(compact map)
100000001 or "100000001". __choices is metadata: it never
becomes a field, never appears in the palette, and is never rendered. A code with no entry is
de-slugged rather than printed raw — needs_replacement becomes
Needs Replacement — so adding an option upstream degrades readably instead of breaking a
running pipeline. Nothing is ever guessed to be a coded value: without a list, a field like a part
number is left exactly as you sent it.
Adding Fields That Aren't in Your Sample
You are not limited to what the sample contained. Two kinds of field can be added:
| Kind | Behavior | Who supplies the value |
|---|---|---|
| Static | Baked into the template. Renders identically on every document. | The template. Caller data of the same name is ignored. |
| Dynamic | A name you choose plus a datatype. Becomes part of the template's declared input contract. | You, in your JSON or XML payload. |
Supported datatypes: string, multiline, number, currency, date, datetime, boolean, email, phone, url, image, richtext, table.
{
"data": { "...your sample..." },
"extraFields": [
{ "name": "invoice.dueDate", "dataType": "date" },
{ "name": "company.tagline", "mode": "static", "value": "Precision document automation" },
{ "name": "invoice.poNumber", "dataType": "string" }
]
}
This is the round trip that matters: invoice.dueDate did not exist in your sample data. You added it in the designer, so it now appears in the template's input contract — and from the next fill onward you include it in your payload:
{
"templateId": "8f3c...",
"data": {
"invoice": { "number": "INV-1002", "issuedOn": "2026-08-01", "dueDate": "2026-08-31", "total": 500.00 }
}
}
The template drives the data shape, not only the other way round. Ask any template what it expects at any time:
GET /api/manage/templates/{clientId}/{templateId}/contract
Backgrounds, Branding and Opacity
A template can carry a page background — an image, a color, or both — with adjustable opacity. Typical uses are a letterhead, a watermark, or a faint brand pattern behind the content.
{
"data": { "...your sample..." },
"background": {
"image": "data:image/png;base64,iVBORw0KGgo...",
"color": "#f7f9fc",
"opacity": 0.15,
"fit": "cover"
}
}
| Field | Accepted values |
|---|---|
image | A data: image URI or an https: URL. Anything else is rejected. |
color | Hex, rgb()/rgba(), hsl()/hsla(), or a CSS color name. |
opacity | 0–1, clamped. Applies to the background only. |
fit | cover (default), contain, tile, stretch. |
showThrough | true makes content elements' own background colors transparent so the page background shows through them — an AI-drafted layout often paints table headers opaque, which punches a white hole in the background. |
Partial updates are safe: when a template's HTML already carries a background, any key you omit keeps its designed value — re-sending {"image": "...", "fit": "contain"} changes the fit without resetting a 15% opacity back to full strength. Send an explicit value to change a key, or an empty "background": {} to remove the background entirely.
Three details worth knowing, because each one is a silent failure if you build this yourself:
- Opacity fades the background, not your text. The background is a separate layer behind the content rather than a
bodybackground, so a 15% watermark leaves the text fully opaque and readable. - It repeats on every page. The layer is fixed-position, which Chromium re-paints on each printed page — a five-page statement keeps its letterhead throughout.
- Backgrounds print. The layer sets
print-color-adjust: exact; without it Chromium discards background colors and images when producing a PDF, and you get a blank white page with no error.
Use the Template
Save the designed template, then fill it with ComposeFill by templateId — 1 token per document, no AI cost. This is the call your workflow makes on every run.
curl -X POST https://your-app.azurewebsites.net/api/ComposeFill \
-H "X-API-Key: df_your_key" \
-H "Content-Type: application/json" \
-d '{
"templateId": "8f3c1e42-...",
"outputFormat": "pdf",
"filename": "invoice-1002",
"data": {
"invoice": { "number": "INV-1002", "issuedOn": "2026-08-01", "dueDate": "2026-08-31", "total": 500.00, "paid": true },
"customer": { "name": "Beta Co", "email": "ap@beta.co" },
"lines": [ { "description": "Support", "qty": 2, "amount": 250.00 } ]
}
}'
{
"success": true,
"kind": "pdf",
"file": "JVBERi0xLjcKJ...",
"contentType": "application/pdf",
"filename": "invoice-1002.pdf",
"tokensReplaced": 14,
"dataHash": "<sha256>"
}
Omit outputFormat to get filled HTML back instead of a PDF. Identical repeat calls are served from the fill cache.
Formatting is applied for you
Values are rendered according to the datatype declared in the contract, so you send raw data and the document shows presentation:
| Datatype | You send | Document shows |
|---|---|---|
currency | 1240.5 | $1,240.50 |
date | "2026-07-28" | Jul 28, 2026 |
boolean | false | No |
number | 1500 | 1,500 |
"n/a", "TBD" — is passed through unchanged rather than being coerced to $0.00. A wrong number on an invoice reads as authoritative, so we never invent one.
Errors you should expect
| Status | Code | Meaning |
|---|---|---|
| 400 | UNSUPPORTED_SAVED_KIND | You passed kind as something other than html for a generated template. |
| 400 | NO_TEMPLATE_BODY | The template record exists but has no stored body — re-save it from the designer. |
| 404 | NOT_FOUND | No template with that id for this client. |
Where the Data Comes From — the Connection Leg
ComposeFill neither knows nor cares where the payload originated. The connection leg belongs to whatever automation host your organization already runs — anything that can make an HTTPS POST completes it. Three hosts cover nearly every workflow:
Power Automate / Logic Apps
Trigger on the event that needs the document — a Dataverse row change, a new SharePoint item, an approval completing — and pass the trigger's fields straight through as the fill payload. The complete recipe is below, including the flattened-key behavior that saves you reshaping the payload.
SSIS — KingswaySoft components or a Script Task
For batch runs out of SQL Server, Dataverse, Dynamics 365, or Salesforce, the proven pattern is a three-step control flow — it is exactly how our own SSIS demo turns a Dynamics 365 Field Service work order into a branded, emailed PDF:
- Extract — a source component reads the rows for the run. KingswaySoft's SSIS Integration Toolkit is the established way to get data out of Dataverse, Dynamics 365, Salesforce, and SharePoint; plain OLE DB or ADO.NET sources cover SQL Server. KingswaySoft moves the data; DocButterfly turns it into the document.
- Render — shape each record into the template's contract and POST it to
ComposeFill, from an HTTP task or a small Script Task. - Deliver — POST the returned base64 to
SendEmail, or write it wherever the document lives (next section).
401 or 402 body comes back as an ordinary string and the package stays green. In a nightly job that means documents quietly stop being produced while every run reports success. Fail the package on any non-2xx status.
Plain HTTPS from code
Every example on this page is already the plain-HTTP path — cURL, PowerShell, Python, a scheduled function, anything with an HTTP client. Ask the template what shape to send before you wire it up:
GET /api/manage/templates/{clientId}/{templateId}/contract
The contract lists every field the template expects, with datatypes — none are mandatory; a field you leave out renders blank. Map your source columns onto those dot-paths and the connection leg is done.
Where the Document Goes — the Delivery Leg
ComposeFill returns the finished document as base64 in file. Every delivery option consumes that value directly — no temporary storage, no separate download step.
Email it — SendEmail
Attach the document and send in one call. The content below is the file value from the ComposeFill response, passed through verbatim. 1 token per email.
curl -X POST https://your-app.azurewebsites.net/api/SendEmail \
-H "X-API-Key: df_your_key" \
-H "Content-Type: application/json" \
-d '{
"to": "ap@beta.co",
"subject": "Invoice INV-1002",
"body": "<p>Hi — your July invoice is attached.</p>",
"bodyType": "HTML",
"attachments": [{
"name": "invoice-1002.pdf",
"contentType": "application/pdf",
"content": "JVBERi0xLjcKJ..."
}]
}'
File it — SharePoint, OneDrive, blob storage
In Power Automate, add Create file with content base64ToBinary(...) — exactly as in the recipe below. From code, decode the base64 and write the bytes wherever documents live: a Microsoft Graph PUT …/content for SharePoint or OneDrive, a blob upload, a network share. There is no DocButterfly charge for this leg — the fill call already paid for the document.
Get it signed — RequestSignature
Send the document straight into a DocuSign envelope. Again, document.content is ComposeFill's file value, untouched:
curl -X POST https://your-app.azurewebsites.net/api/RequestSignature \
-H "X-API-Key: df_your_key" \
-H "Content-Type: application/json" \
-d '{
"document": {
"name": "invoice-1002.pdf",
"content": "JVBERi0xLjcKJ...",
"contentType": "application/pdf"
},
"signers": [
{ "email": "ap@beta.co", "name": "Pat Lee", "routingOrder": 1 }
],
"emailSubject": "Please sign: Invoice INV-1002",
"status": "sent"
}'
RequestSignature uses our DocuSign account at 50 tokens per envelope — the envelope itself is what costs. RequestSignatureBYOK is the same call against your own DocuSign account for 1 token; pass your credentials in byok.
ComposeFill into SendEmail and you get a single webhook URL: the caller sends data, the recipient gets the document, and your host system makes exactly one HTTP call. See Webhooks & Integrations.
Power Automate / Logic Apps
Authoring happens once in the portal. Your flow only ever makes the fill call, so there is a single HTTP action to configure:
{
"method": "POST",
"uri": "https://your-app.azurewebsites.net/api/ComposeFill",
"headers": {
"X-API-Key": "df_your_key",
"Content-Type": "application/json"
},
"body": {
"templateId": "8f3c1e42-...",
"outputFormat": "pdf",
"filename": "invoice-@{triggerBody()?['invoiceNumber']}",
"data": {
"invoice": {
"number": "@{triggerBody()?['invoiceNumber']}",
"issuedOn": "@{triggerBody()?['issuedOn']}",
"total": @{triggerBody()?['total']}
},
"lines": "@{triggerBody()?['lineItems']}"
}
}
}
Then add Create file (SharePoint or OneDrive) with content @{base64ToBinary(body('HTTP')?['file'])}.
{ "invoice.number": "INV-1002" } resolves exactly like the nested form. You don't have to reshape your payload.
What It Costs
| Call | Tokens | How often |
|---|---|---|
AnalyzeTemplateData | 1 | While designing. No AI call. |
GenerateTemplateFromData | 5 | Once per template you author. |
ComposeFill | 1 | Per document, forever after. |
SendEmail | 1 | Per email delivered, document attached. |
RequestSignature | 50 | Per envelope, on our DocuSign account. |
RequestSignatureBYOK | 1 | Per envelope, on your own DocuSign account. |
The AI cost is a one-time authoring cost, not a per-document cost. A template generated once for 5 tokens then produces documents at 1 token each — and emailing one adds 1 more. Filing the document in SharePoint or blob storage is your host's action and costs nothing here.