Document Generation API Reference

Programmatically fill PDF, Word, PowerPoint, and HTML templates with your data and receive the generated documents as a ZIP. Integrate document generation into your applications without using the Excel Add-in.

⚡ API Reference
This is the API reference for developers. If you're using the Excel Add-in, see the PDF Forms User Guide instead.

⚠️ API Access Requirement
API access is included on Pro and Enterprise plans by default. EFCustom includes API access only when it is explicitly enabled for your tenant. If your current plan does not include API access, you can also enable it with an API access add-on pack instead of changing the full subscription. That add-on is useful when you need programmatic access on a lower-tier or Free-plan account, while actual document generation still uses your normal document-generation capacity. View pricing or manage your API key.

Supported document types

Type Supported Notes
PDF One PDF per data row. AcroForm and XFA.
Word (.docx) One .docx per data row. Optional Statics add shared content (logo, chart, named-range text).
PowerPoint (.pptx) One .pptx per data row. Optional Statics add shared content.
HTML One filled document per data row, emitted as both a .html and a rendered .pdf. Conditional rules and token mappings are configured on the template (server-side) — no extra request payload.

A single template set can mix all four types. One generate call returns a ZIP containing every document the set produces.

Base URL

https://office.exceltoforms.com/api/v1

Authentication

All API requests require authentication using an API key passed in the X-API-Key header.

X-API-Key: exf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxx

List Template Sets

GET /api/v1/TemplateSets

Returns all template sets accessible to your API key.

Response

{
  "TemplateSets": [
    {
      "Id": 84,
      "Name": "Employee Onboarding",
      "Description": "New hire forms package",
      "FolderName": "HR",
      "PdfCount": 5,
      "CreatedAt": "2024-01-15T10:30:00"
    }
  ],
  "Count": 1
}

Get Template Details

GET /api/v1/TemplateSets/{id}

Returns details of a specific template set, including the required fields and the list of documents (Forms) the set produces.

Path Parameters

Parameter Type Description
id integer Template set ID

Response

{
  "Id": 103,
  "Name": "Employee Pack",
  "Description": "PDF + Word + PowerPoint",
  "ParentTable": {
    "Name": "WordParentTable",
    "Fields": ["ID", "EmpName", "Title", "EmployeeName", "EmployeePosition"]
  },
  "ChildTables": [],
  "Forms": [
    { "Id": 438, "FileName": "Employee Information.pdf", "Type": "pdf",  "IsXFA": false, "ParentTable": "Table3" },
    { "Id": 439, "FileName": "CorporateBio.docx",        "Type": "word", "IsXFA": false, "ParentTable": "WordParentTable" },
    { "Id": 440, "FileName": "Sample PPT.pptx",          "Type": "ppt",  "IsXFA": false, "ParentTable": "PPTParentTable" }
  ],
  "PdfForms": [
    { "Id": 438, "FileName": "Employee Information.pdf", "IsXFA": false }
  ]
}
Field Description
Forms All documents in the set, each with Type (pdf | word | ppt | html).
Forms[].ParentTable The Excel table this form iterates — one document per row of it. Empty when the form declares no parent table.
PdfForms PDF-only list, kept for backward compatibility with earlier integrations.

Tip — multiple parent tables
Each document in a set can be mapped to a different parent table (a PDF might use Table3, a Word doc WordParentTable, a deck PPTParentTable). ParentTable in this response is only the set's primary table; Forms[].ParentTable tells you exactly which table each form needs. Supply every table the set uses — the primary one in ParentTable, the other forms' parents in AdditionalTables (see Supplying multiple tables below) — or the forms whose tables are missing will not generate.


Generate Documents

POST /api/v1/TemplateSets/{id}/generate

Fill the template set with provided data and receive a ZIP file containing all generated documents (PDF, Word, and/or PowerPoint).

Data Format
The first row of every table must be column headers (field names). Data rows follow after. This is the same format as Excel/CSV data.

Request Body

{
  "ParentTable": {
    "Name": "Employees",
    "Rows": [
      ["FullName", "Email", "Department", "StartDate"],
      ["John Smith", "[email protected]", "Engineering", "2026-02-01"],
      ["Jane Doe", "[email protected]", "Marketing", "2026-02-15"]
    ]
  },
  "ChildTables": [],
  "Statics": null,
  "Images": null,
  "forms": null
}

Request Parameters

Parameter Type Required Description
ParentTable.Name string No Table name (defaults to the template's parent table name)
ParentTable.Rows array Yes First row = column headers, remaining rows = data values
ChildTables array No Related child tables (same format: first row = headers). A child carries several rows per parent record, joined by the key column.
AdditionalTables array No The other forms' parent tables, when the forms in a set iterate different tables (same format: first row = headers). Each carries one row per document, like ParentTable. See Supplying multiple tables.
Statics object No Optional shared content for Word/PPT (named-range text, repeating tables, charts/logos) applied to every generated document.
Images object No Per-record images for multi-file Word/PPT placeholders.
forms array No Allow-list of which documents to generate (doc-type names and/or form ids). Omitted/empty = generate everything. See Selecting which documents to generate.

Data Format Details

The Rows array follows a CSV-like structure:

  • Row 0: Column headers (must match your Excel column names / placeholder bindings)
  • Row 1+: Data values — for PDF and multi-file Word/PPT, one document per row

Include all columns referenced by your template's mappings, even if not every column is mapped.

Supplying multiple tables

A set does not necessarily have one parent table: each form iterates its own table (one document per row of it), and a mixed set can hold a PDF driven by Table3, a Word file driven by WordParentTable and a deck driven by PPTParentTable. GET /TemplateSets/{id} reports each form's table in Forms[].ParentTable.

Each file resolves its own parent table by name from the tables you send. Put the set's primary table in ParentTable and every other form's parent in AdditionalTables — they have parent shape (one row per document), not child shape:

{
  "ParentTable": {
    "Name": "WordParentTable",
    "Rows": [["ID","EmpName","Title"], ["1","Ada Lovelace","Engineer"]]
  },
  "AdditionalTables": [
    { "Name": "Table3",         "Rows": [["ID","EmpName","Address"], ["1","Ada Lovelace","12 Analytical Ave"]] },
    { "Name": "PPTParentTable", "Rows": [["ID","EmpName","Title"],   ["1","Ada Lovelace","Engineer"]] }
  ]
}

A form whose parent table is not present in the request is skipped (the rest still generate), and the response names it in the X-ExForms-Forms-Not-Generated header so a 200 with missing documents is detectable.

Backward compatibility — requests that pass extra parent tables inside ChildTables keep working exactly as before: the server only needs a table of that name to be present. AdditionalTables is the accurate home for them (they are per-document tables, not per-record child rows), so prefer it in new integrations.


Selecting which documents to generate

By default, a generate call produces every document in the set. To produce only some of them, send a forms allow-list. Each entry is either:

  • a doc-type name"pdf", "word", "ppt", or "html" — which includes every file of that type in the set (1 or many), or
  • a form id (integer) from the Forms[].Id list in GET /TemplateSets/{id} — which includes that one file.

Type names and ids can be mixed. Omitting forms (or sending []) generates everything (unchanged default). Only the selected documents are produced and counted toward your usage.

// Example set with forms: 434 word, 435 ppt, 438 pdf
{ "forms": ["word", "html"] }   // every Word + every HTML file
{ "forms": ["pdf"] }            // every PDF in the set (whether 1 or 10)
{ "forms": [434] }              // just that one file, by id
{ "forms": ["word", 438] }      // all Word files + the specific PDF 438

Full request — generate only the Word doc, skipping the set's PDF and PPT:

{
  "forms": ["word"],
  "ParentTable": { "Name": "WordParentTable", "Rows": [["ID","EmpName"], ["1","Ada Lovelace"]] },
  "ChildTables": [ { "Name": "DataTable", "Rows": [["ID","Year","Sales"], ["1","2024","2500"]] } ]
}

Notes:

  • You only need to send the tables the selected files use; omit data for excluded files.
  • Type names match by document type, so excluding a type (e.g. send ["word","html"] to skip PDF/PPT) works regardless of how many files of each type the set has.
  • Entries that match nothing in the set are ignored and listed in the X-ExForms-Forms-Ignored response header. If no entry matches, the call returns 400 validation_error.

Word & PowerPoint statics

Statics carries content that is the same in every generated document — Word/PPT named-range text, repeating tables, and shared images (logos, charts). It is optional: the per-row fields fill from your ParentTable/ChildTables (one document per row), and Statics add the shared bits on top.

{
  "ParentTable": { "Name": "Report", "Rows": [["ID"], ["1"]] },
  "Statics": {
    "Text":   { "CompanyName": "Acme Corp", "Quarter": "Q2 2026" },
    "Tables": { "SalesTable": [["Region","Total"], ["West","98,000"], ["East","72,500"]] },
    "Images": { "Logo": { "url": "https://cdn.example.com/logo.png" } }
  }
}
Field Type Description
Statics.Text object placeholder → text. Single-value content controls / placeholders.
Statics.Tables object placeholder → 2D array (first row = headers). Repeating tables.
Statics.Images object placeholder → ImageRef. Images shared across every document.

Word & PowerPoint images

Images supplies per-record images for multi-file Word/PPT picture placeholders.

{
  "Images": {
    "1": {
      "Logo":       { "url": "https://cdn.example.com/ada/logo.png" },
      "Background":  { "base64": "iVBORw0KGgoAAAANSUhEUgAA..." }
    },
    "2": {
      "Logo": { "url": "https://cdn.example.com/alan/logo.png" }
    }
  }
}
  • Outer key ("1", "2" …) = the record key — the value in the row's parent-child key column (or the ID column if no key is mapped). It is matched against the parent table's key column to attach images to the right record's document.
  • Inner key ("Logo", "Background") = the placeholder / content-control name.
  • The same map is matched against both Word and PowerPoint placeholders by name, so a mixed set can share one Images block. Names that don't match a placeholder are ignored.

ImageRef

An image is referenced as either a public URL or inline base64:

{ "url": "https://cdn.example.com/photo.png" }
{ "base64": "iVBORw0KGgo..." }
{ "base64": "data:image/png;base64,iVBORw0KGgo..." }
Field Description
url Publicly reachable http/https URL. Fetched server-side.
base64 Base64-encoded image bytes. A data: URI prefix is accepted and stripped.

Rules & limits:

  • Maximum 10 MB per image (URL response or decoded base64).
  • The payload must be a real image — PNG, JPEG, GIF, BMP, TIFF, or WEBP. Anything else (an HTML error page, a JSON body, a PDF) is rejected rather than embedded.
  • URLs must resolve to a public host. Requests to loopback, private/internal networks, link-local and cloud-metadata addresses are refused, and redirects are not followed — point url directly at the image.
  • At most 200 image URL fetches per request. Beyond that, further URLs are skipped and the response carries X-ExForms-Images-Fetch-Limit. Send images as base64 (which has no such cap) or split the batch if you need more.
  • Image resolution is fail-safe: an image that can't be fetched/decoded is skipped (left blank) and the rest of the batch still succeeds.
  • Skipped images are reported in the response headers X-ExForms-Images-Skipped (count) and X-ExForms-Images-Skipped-Detail (first 25 keys).
  • There is no Excel over the API, so Excel-embedded pictures / live chart snapshots are not available — provide them as a URL or base64 instead.

HTML output

HTML templates fill from the same ParentTable + ChildTables payload as PDF — no HTML-specific request fields. In a mixed set the HTML file usually iterates its own parent table (check Forms[].ParentTable); supply it via AdditionalTables when it is not the set's primary. Each parent row produces one filled document, emitted as both a .html file and a rendered .pdf:

HTML/1001/Invoice_1001.html
HTML/1001/Invoice_1001.pdf
HTML/1002/Invoice_1002.html
HTML/1002/Invoice_1002.pdf

Key points:

  • Tokens & child tables: the template's `` placeholders and repeating child-table rows are filled from the table columns you send. The token→column mapping and the parent↔child join key are stored on the template (configured in the add-in) — the API resolves them server-side by file.
  • Conditional rules: any show/hide/format rules saved on the template are applied automatically. There is no rules field in the request.
  • No images payload: HTML references images via normal <img src="…"> URLs inside the template/data, so the Images/Statics blocks (used for Word/PPT placeholders) do not apply to HTML.
  • External resources: images/fonts/CSS referenced by public http(s) URLs (or inline data: URIs) are loaded during PDF rendering. References to private/internal hosts (localhost, RFC-1918, link-local, cloud-metadata) are blocked for security — such a resource is simply omitted and the document still renders.
  • Counting: each HTML record counts as one form (the .html + .pdf pair is a single document), same per-row rate as PDF.
  • Mapping required: an HTML set must have been mapped (token/parent-child mapping saved) for tokens to fill. An unmapped set produces output with placeholders stripped rather than an error.

Example

{
  "parentTable": {
    "name": "Invoices",
    "rows": [
      ["ID", "invoice.number", "customer.name", "invoice.total"],
      ["1001", "INV-2026-0001", "Brightline Interiors Ltd", "2700"]
    ]
  },
  "childTables": [
    { "name": "LineItems",
      "rows": [
        ["ID", "description", "qty", "unit_price", "total"],
        ["1001", "BI requirements workshop", "1", "250", "250"],
        ["1001", "Power BI dataset modeling", "3", "180", "540"]
      ]
    }
  ]
}

Column names must match the template's mapped fields/tokens (or the token names themselves). Use GET /TemplateSets/{id} to see the parent table's expected fields.


Response

Returns a ZIP file (application/zip) containing the generated documents, organized by type and per-file naming:

PDF/1/Employee Information_Ada Lovelace_1.pdf
PDF/2/Employee Information_Alan Turing_2.pdf
Word/1/CorporateBio_Ada Lovelace_1.docx
Word/2/CorporateBio_Alan Turing_2.docx
PowerPoint/1/Sample PPT_Engineer_1.pptx
PowerPoint/2/Sample PPT_Scientist_2.pptx
HTML/1001/Invoice_1001.html
HTML/1001/Invoice_1001.pdf
Images/1/Intake Form_Ada Lovelace_1.jpg

Each file is named from its own naming pattern (configured per template file), not from the PDF's pattern.

Image-Sourced Templates

Template files uploaded as images (PNG, JPG, TIFF — scans and photos, made fillable with the field placement designer) work through this API exactly like any PDF form: same request shape, same field mapping, same naming.

Their generated documents are returned in the format they were uploaded in, in an Images/ folder:

  • PNG/JPG templates produce one image per page (name.jpg, name_p2.jpg, …).
  • Multi-page TIFF templates produce one multi-page TIFF file per record.
  • Set that file's Output option to PDF in the add-in (Edit Set → the file's Output dropdown) if you would rather receive PDFs; the file then appears under PDF/ as usual.

Flat PDFs that were made fillable with the placement designer are ordinary PDF forms here — they return PDFs under PDF/.

Response Headers

Header When Description
X-ExForms-Usage-Warning Near limit Human-readable usage warning.
X-ExForms-Usage-Percent Near limit Projected usage percentage.
X-ExForms-Images-Skipped Image(s) failed Count of images that could not be resolved.
X-ExForms-Images-Skipped-Detail Image(s) failed First 25 skipped keys (e.g. record 2:Logo).
X-ExForms-Images-Fetch-Limit >200 image URLs The per-request image URL fetch ceiling was reached; later URLs were skipped.
X-ExForms-Html-Render-Failed HTML render error Count of HTML records whose PDF render failed (the .html is still included; the .pdf is missing).
X-ExForms-Forms-Ignored forms filter Allow-list entries that matched no document in the set (the rest still ran).
X-ExForms-Forms-Not-Generated Missing parent table Forms that produced no documents because the table they iterate was not in the request, with the table name and the tables that were sent. Check Forms[].ParentTable in GET /TemplateSets/{id} and supply the missing table via AdditionalTables.

Test API Call (mixed PDF + Word + PowerPoint)

Send all three parent tables so every document generates (one per data row):

{
  "ParentTable": {
    "Name": "WordParentTable",
    "Rows": [
      ["ID","EmpName","Title","EmployeeName","EmployeePosition","Intro","Metrics"],
      ["1","Ada Lovelace","Engineer","Ada Lovelace","Principal Engineer","Welcome Ada","98%"],
      ["2","Alan Turing","Scientist","Alan Turing","Distinguished Scientist","Welcome Alan","99%"]
    ]
  },
  "AdditionalTables": [
    { "Name": "Table3",         "Rows": [["ID","EmpName","Address"], ["1","Ada Lovelace","12 Analytical Ave"], ["2","Alan Turing","1 Enigma Rd"]] },
    { "Name": "PPTParentTable", "Rows": [["ID","Title","Metrics"],   ["1","Engineer","98%"], ["2","Scientist","99%"]] }
  ],
  "Images": {
    "1": { "Logo": { "url": "https://cdn.example.com/ada/logo.png" } },
    "2": { "Logo": { "url": "https://cdn.example.com/alan/logo.png" } }
  }
}

Expected result: the response ZIP contains 2 PDFs + 2 Word docs + 2 PowerPoint decks (one of each per data row), with per-record images embedded.


Getting files instead of a ZIP

A ZIP is convenient for a person and awkward for a program: you have to write it to disk or unpack it in memory before you can do anything with a single document. If you need the documents individually — to attach one to an email, push each to storage, or hand one back to a user — ask for a files array instead.

Add ?response=files to the generate call:

POST /api/v1/TemplateSets/{id}/generate?response=files

The request body is unchanged. The response is JSON, and each document arrives complete — the bytes are always included, base64 encoded, in content:

{
  "count": 2,
  "files": [
    {
      "index": 0,
      "name": "Invoice_1001.pdf",
      "path": "PDF/1/Invoice_1001.pdf",
      "contentType": "application/pdf",
      "sizeBytes": 48213,
      "content": "JVBERi0xLjcKJeLjz9MKMSAwIG9iago8PC9UeXBl..."
    },
    {
      "index": 1,
      "name": "Invoice_1002.pdf",
      "path": "PDF/2/Invoice_1002.pdf",
      "contentType": "application/pdf",
      "sizeBytes": 47980,
      "content": "JVBERi0xLjcKJeLjz9MKMSAwIG9iago8PC9UeXBl..."
    }
  ]
}

path is the same folder layout you would have found inside the ZIP, so anything that already understands the ZIP structure keeps working.

There is no downloadUrl on this route — a synchronous request keeps nothing on the server, so the response is the only copy. Use the asynchronous route below if you would rather fetch documents one at a time.

Two limits, both refused up front

response=files returns everything in a single response, so it is bounded twice:

Limit Value
Documents per request 500
Total inline size 20 MB

Exceeding either is refused with a clear message rather than a truncated list, and nothing is charged — the document-count check runs before anything is generated. The error tells you how many documents would fit and points at the asynchronous route, which has neither limit.


Large batches: asynchronous generation

A synchronous request has to finish inside your client's timeout — and inside any proxy's. Power Automate gives up at about 120 seconds, Zapier at 30. For batches that take longer, submit the job and collect the result afterwards.

POST /api/v1/TemplateSets/{id}/generate/async

Same request body as generate. Returns 202 Accepted immediately, with a Location header and the URLs you need:

{
  "jobKey": "9f2c1a8e4b7d4c05a1e6f3b9d2c47e10",
  "status": "queued",
  "existing": false,
  "statusUrl": "/api/v1/jobs/9f2c1a8e4b7d4c05a1e6f3b9d2c47e10",
  "resultUrl": "/api/v1/jobs/9f2c1a8e4b7d4c05a1e6f3b9d2c47e10/result",
  "retryAfterSeconds": 5
}

existing is true when an Idempotency-Key matched an earlier request and this is that original job rather than a new one — see Idempotency.

Poll for completion

GET /api/v1/jobs/{jobKey}
{ "jobKey": "9f2c1a8e...", "status": "running", "progress": 40 }

status is queued, running, succeeded, failed or expired. While the job is queued or running the response carries a Retry-After: 5 header — it is still a 200, so poll on that interval rather than treating it as an error.

When a job fails, error is an object:

{ "jobKey": "9f2c1a8e...", "status": "failed",
  "error": { "code": "generation_error", "message": "..." } }

A job key belonging to another account returns 404, not 403 — keys cannot be probed.

Collect the result

Once the job reports succeeded, take it either way:

GET /api/v1/jobs/{jobKey}/result

Returns the ZIP, exactly as the synchronous call would.

GET /api/v1/jobs/{jobKey}/files

Returns the same file entries, with no 500-document limit — this is the route for large batches. It differs from the synchronous route in one way: entries carry a downloadUrl and no content unless you ask for it.

# metadata only — fast, however many documents there are
curl "https://office.exceltoforms.com/api/v1/jobs/{jobKey}/files" -H "X-API-Key: YOUR_API_KEY"

# with the bytes inline, base64, capped at 20 MB per response
curl "https://office.exceltoforms.com/api/v1/jobs/{jobKey}/files?include=content" -H "X-API-Key: YOUR_API_KEY"

Past the 20 MB cap the remaining entries keep their downloadUrl and lose content, and the response says so rather than going quiet:

{
  "jobKey": "9f2c1a8e...",
  "count": 120,
  "files": [ "..." ],
  "contentOmitted": true,
  "contentOmittedReason": "Inline content is capped at 20 MB per response. Files past the cap have a downloadUrl instead."
}

You never receive a silently truncated list: if anything was left out, contentOmitted is present. Fetch those from their downloadUrl.

GET /api/v1/jobs/{jobKey}/files/{index}

Downloads one document by its index from the files list, as its own file with the correct content type. This is what downloadUrl points at.

Retry-After

Asking for the result before the job is finished returns 409 with a Retry-After: 5 header, rather than an error. Treat it as "not yet" and poll again.

Retention

Results are kept for 24 hours after generation. After that the job reports expired and the documents are gone — collect them within the day.

Idempotency

Send an Idempotency-Key header with the async request and a repeat of the same key returns the original job instead of starting a second one. Worth using on any workflow that might retry: without it, a retry generates the documents again and counts against your usage twice.

curl -X POST "https://office.exceltoforms.com/api/v1/TemplateSets/84/generate/async" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Idempotency-Key: invoice-run-2026-08-03" \
  -H "Content-Type: application/json" \
  -d @request.json

Which route to use

Situation Route
A handful of documents, want them individually generate?response=files
A handful of documents, ZIP is fine generate
More than 500 documents generate/asyncjobs/{jobKey}/files
Long-running batch, or a client that times out generate/async → poll → result or files

Account Info

GET /api/v1/account

Returns account information and usage statistics for the current API key.

Response

{
  "CompanyId": 59,
  "ApiKeyName": "Production Key",
  "ApiKeyCreatedAt": "2026-01-12T09:50:34",
  "ApiKeyLastUsedAt": "2026-01-12T12:00:00",
  "RateLimitPerMinute": 60,
  "CurrentMonth": "2026-01",
  "FormsUsedThisMonth": 0,
  "FormsLimit": 5000
}

Note: FormsUsedThisMonth and FormsLimit are populated from current-month usage and plan limits.


Error Handling

All errors return a JSON object with Error and Message fields.

Status Error Code Description
400 validation_error Invalid request format or missing required fields
401 api_key_missing API key header is missing
401 api_key_empty API key header is empty
401 api_key_invalid API key is invalid
401 api_key_revoked API key is revoked
401 api_key_expired API key is expired
403 subscription_required Active API-enabled subscription required
403 usage_limit_reached Monthly forms limit would be exceeded
404 template_not_found Template set not found or not accessible
500 internal_error Server error, contact support
500 generation_error Error occurred during document generation

Error Response Example

{
  "Error": "validation_error",
  "Message": "ParentTable with at least one row is required."
}

Rate Limits

RateLimitPerMinute is stored on the API key and returned by /api/v1/account. Enforcement is handled in API authentication (429 when exceeded).


Forms Limits

Important: One API call can use many forms.

Forms used =  (PDF files)          × (data rows)
            + (Word files)         × (data rows)
            + (PowerPoint files)   × (data rows)
            + (HTML files)         × (data rows)

(Each file produces one document per data row. An HTML record's .html + rendered .pdf pair counts as one form.)

Example: a set with 1 PDF + 1 Word + 1 PowerPoint, sent with 50 data rows = (1 + 1 + 1) × 50 = 150 forms counted.

Form usage is logged server-side. Current usage counters are available via /api/v1/account.


Code Examples

cURL

# List template sets
curl -X GET "https://office.exceltoforms.com/api/v1/TemplateSets" \
  -H "X-API-Key: YOUR_API_KEY"

# Generate documents (first row = headers, remaining rows = data)
curl -X POST "https://office.exceltoforms.com/api/v1/TemplateSets/84/generate" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"ParentTable":{"Name":"Employees","Rows":[["FullName","Email"],["John Smith","[email protected]"]]}}' \
  --output documents.zip

C# / .NET

using var client = new HttpClient();
client.DefaultRequestHeaders.Add("X-API-Key", "YOUR_API_KEY");

// Generate documents - first row is headers, rest is data
var request = new {
    ParentTable = new {
        Name = "Employees",
        Rows = new[] {
            new[] { "FullName", "Email", "Department" },  // Headers
            new[] { "John Smith", "[email protected]", "Engineering" },
            new[] { "Jane Doe", "[email protected]", "Marketing" }
        }
    },
    // Optional: per-record images for Word/PPT placeholders (keyed by record id, then placeholder)
    Images = new Dictionary<string, Dictionary<string, object>> {
        ["1"] = new() { ["Logo"] = new { url = "https://cdn.example.com/logo.png" } }
    }
};

var response = await client.PostAsJsonAsync(
    "https://office.exceltoforms.com/api/v1/TemplateSets/84/generate", request);
var zipBytes = await response.Content.ReadAsByteArrayAsync();
await File.WriteAllBytesAsync("documents.zip", zipBytes);

Python

import requests

headers = {
    "X-API-Key": "YOUR_API_KEY",
    "Content-Type": "application/json"
}

payload = {
    "ParentTable": {
        "Name": "Employees",
        "Rows": [
            ["FullName", "Email", "Department"],  # Headers (row 0)
            ["John Smith", "[email protected]", "Engineering"],
            ["Jane Doe", "[email protected]", "Marketing"]
        ]
    },
    # Optional: static Word/PPT content + per-record images
    "Statics": {"Text": {"CompanyName": "Acme Corp"}},
    "Images": {"1": {"Logo": {"url": "https://cdn.example.com/logo.png"}}}
}

response = requests.post(
    "https://office.exceltoforms.com/api/v1/TemplateSets/84/generate",
    headers=headers,
    json=payload
)

if response.headers.get("X-ExForms-Images-Skipped"):
    print("Skipped images:", response.headers["X-ExForms-Images-Skipped-Detail"])

with open("documents.zip", "wb") as f:
    f.write(response.content)

JavaScript / Node.js

const headers = {
    "X-API-Key": "YOUR_API_KEY",
    "Content-Type": "application/json"
};

const response = await fetch(
    "https://office.exceltoforms.com/api/v1/TemplateSets/84/generate",
    {
        method: "POST",
        headers,
        body: JSON.stringify({
            ParentTable: {
                Name: "Employees",
                Rows: [
                    ["FullName", "Email", "Department"],  // Headers
                    ["John Smith", "[email protected]", "Engineering"],
                    ["Jane Doe", "[email protected]", "Marketing"]
                ]
            },
            Images: { "1": { Logo: { url: "https://cdn.example.com/logo.png" } } }
        })
    }
);

const blob = await response.blob();
// Save blob as documents.zip

Need help? Contact [email protected]