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 |
|---|---|---|
| ✅ | 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
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
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 useTable3, a Word docWordParentTable, a deckPPTParentTable).ParentTablein this response is only the set's primary table;Forms[].ParentTabletells you exactly which table each form needs. Supply every table the set uses — the primary one inParentTable, the other forms' parents inAdditionalTables(see Supplying multiple tables below) — or the forms whose tables are missing will not generate.
Generate Documents
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
ChildTableskeep working exactly as before: the server only needs a table of that name to be present.AdditionalTablesis 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[].Idlist inGET /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-Ignoredresponse header. If no entry matches, the call returns400 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 theIDcolumn 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
Imagesblock. 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
urldirectly 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 asbase64(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) andX-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 theImages/Staticsblocks (used for Word/PPT placeholders) do not apply to HTML. - External resources: images/fonts/CSS referenced by public
http(s)URLs (or inlinedata: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+.pdfpair 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
PDFin the add-in (Edit Set → the file's Output dropdown) if you would rather receive PDFs; the file then appears underPDF/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:
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.
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
{ "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:
Returns the ZIP, exactly as the synchronous call would.
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.
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/async → jobs/{jobKey}/files |
| Long-running batch, or a client that times out | generate/async → poll → result or files |
Account Info
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:
FormsUsedThisMonthandFormsLimitare 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]