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 templateTemplate Generator template
DirectionDocument → dataData → document
You start withA PDF you uploadA JSON or XML sample
You defineRegions drawn on the pageA field contract
You get backExtracted valuesA rendered document
Used forReading invoices you receiveProducing invoices you send
No source document is needed here. If you already have a payload — from Dataverse, SQL, an ERP export, a Power Automate flow — that is all it takes to produce a template.

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.

Flow diagram: data connection leg into DocButterfly compose and fill, then out to the delivery leg 1 · DATA CONNECTION 2 · COMPOSE / FILL 3 · DELIVERY SSIS KingswaySoft source components or a Script Task pull the rows Power Automate / Logic Apps the flow triggers on the event that needs the document Plain HTTPS any language, any scheduler — cURL to production code Author once, in the portal Analyze → Generate → refine → save POST /api/ComposeFill templateId + JSON or XML data → PDF or HTML (base64) 1 token per document POST /api/SendEmail finished PDF attached — 1 token per email SharePoint / OneDrive your flow writes the decoded base64 as a file POST /api/RequestSignature DocuSign envelope — 50 tokens (1 with your own account) templateId

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": []
}
Why 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}}.
Repeating markers are wrapped in HTML comments on purpose. A bare {{#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"
  }'
Include at least two rows of any repeating element. XML gives no schema, so <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)
Always send the raw code. A numeric option-set value matches whether it arrives as 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:

KindBehaviorWho 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"
  }
}
FieldAccepted values
imageA data: image URI or an https: URL. Anything else is rejected.
colorHex, rgb()/rgba(), hsl()/hsla(), or a CSS color name.
opacity01, clamped. Applies to the background only.
fitcover (default), contain, tile, stretch.
showThroughtrue 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 body background, 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.
Background images are embedded in the template, so rendering needs no outbound network call. That is what makes them work in headless rendering, where an external image URL would silently come out blank.

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:

DatatypeYou sendDocument shows
currency1240.5$1,240.50
date"2026-07-28"Jul 28, 2026
booleanfalseNo
number15001,500
A value that isn't genuinely numeric — "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
StatusCodeMeaning
400UNSUPPORTED_SAVED_KINDYou passed kind as something other than html for a generated template.
400NO_TEMPLATE_BODYThe template record exists but has no stored body — re-save it from the designer.
404NOT_FOUNDNo 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:

  1. 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.
  2. Render — shape each record into the template's contract and POST it to ComposeFill, from an HTTP task or a small Script Task.
  3. Deliver — POST the returned base64 to SendEmail, or write it wherever the document lives (next section).
Check the HTTP status code explicitly after every call. SSIS HTTP tasks — third-party ones included — generally treat any response as success: a 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.

Fill and deliver in one POST. Publish an Orchestrator pipeline that chains 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'])}.

Flat dotted keys work too. Power Automate often produces flattened objects, so { "invoice.number": "INV-1002" } resolves exactly like the nested form. You don't have to reshape your payload.

What It Costs

CallTokensHow often
AnalyzeTemplateData1While designing. No AI call.
GenerateTemplateFromData5Once per template you author.
ComposeFill1Per document, forever after.
SendEmail1Per email delivered, document attached.
RequestSignature50Per envelope, on our DocuSign account.
RequestSignatureBYOK1Per 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.