DOCX templating API
Use this guide to generate DOCX documents from Word templates with Nutrient DWS Processor API. For API overview details such as signup, pricing, and examples, refer to the DOCX templating API page.
Basic DOCX templating
The DOCX templating API takes a Word template and a JSON model, and then returns a generated DOCX document. Developers often use this flow for contracts, proposals, and letters where the layout stays fixed and only data changes.
Step 1: Creating a template
Create a Word document named template.docx and add placeholders:
Hello {name}Company: {company}Date: {date}The API uses single braces as default delimiters. For example, {name} maps to {"name":"Alex"}.
Step 2: Sending the template and model
Run the request from the same folder as template.docx.
curl -X POST https://api.nutrient.io/process_office_template \ -H "Authorization: Bearer your_api_key_here" \ -o result.docx \ --fail \ -F 'document=@template.docx;type=application/vnd.openxmlformats-officedocument.wordprocessingml.document' \ -F 'model={"name":"Alex","company":"Acme","date":"2026-04-10"}'curl -X POST https://api.nutrient.io/process_office_template ^ -H "Authorization: Bearer your_api_key_here" ^ -o result.docx ^ --fail ^ -F "document=@template.docx;type=application/vnd.openxmlformats-officedocument.wordprocessingml.document" ^ -F "model={\"name\":\"Alex\",\"company\":\"Acme\",\"date\":\"2026-04-10\"}"package com.example.pspdfkit;
import java.io.File;import java.io.IOException;import java.nio.file.FileSystems;import java.nio.file.Files;import java.nio.file.StandardCopyOption;
import org.json.JSONObject;
import okhttp3.MediaType;import okhttp3.MultipartBody;import okhttp3.OkHttpClient;import okhttp3.Request;import okhttp3.RequestBody;import okhttp3.Response;
public final class PspdfkitApiExample { public static void main(final String[] args) throws IOException { final MediaType mediaType = MediaType.parse("application/vnd.openxmlformats-officedocument.wordprocessingml.document"); final RequestBody body = new MultipartBody.Builder() .setType(MultipartBody.FORM) .addFormDataPart( "document", "template.docx", RequestBody.create( mediaType, new File("template.docx") ) ) .addFormDataPart( "model", new JSONObject() .put("name", "Alex") .put("company", "Acme") .put("date", "2026-04-10").toString() ) .build();
final Request request = new Request.Builder() .url("https://api.nutrient.io/process_office_template") .method("POST", body) .addHeader("Authorization", "Bearer your_api_key_here") .build();
final OkHttpClient client = new OkHttpClient() .newBuilder() .build();
final Response response = client.newCall(request).execute();
if (response.isSuccessful()) { Files.copy( response.body().byteStream(), FileSystems.getDefault().getPath("result.docx"), StandardCopyOption.REPLACE_EXISTING ); } else { throw new IOException(response.body().string()); } }}using System;using System.IO;using System.Net;using RestSharp;
namespace PspdfkitApiDemo{ class Program { static void Main(string[] args) { var client = new RestClient("https://api.nutrient.io/process_office_template");
var request = new RestRequest(Method.POST) .AddHeader("Authorization", "Bearer your_api_key_here") .AddFile("document", "template.docx") .AddParameter("model", new JsonObject { ["name"] = "Alex", ["company"] = "Acme", ["date"] = "2026-04-10" }.ToString());
request.AdvancedResponseWriter = (responseStream, response) => { if (response.StatusCode == HttpStatusCode.OK) { using (responseStream) { using var outputFileWriter = File.OpenWrite("result.docx"); responseStream.CopyTo(outputFileWriter); } } else { var responseStreamReader = new StreamReader(responseStream); Console.Write(responseStreamReader.ReadToEnd()); } };
client.Execute(request); } }}// This code requires Node.js. Do not run this code directly in a web browser.
const axios = require('axios')const FormData = require('form-data')const fs = require('fs')
const formData = new FormData()formData.append('document', fs.createReadStream('template.docx'), { contentType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', filename: 'template.docx'})formData.append('model', JSON.stringify({ name: 'Alex', company: 'Acme', date: '2026-04-10'}))
;(async () => { try { const response = await axios.post('https://api.nutrient.io/process_office_template', formData, { headers: formData.getHeaders({ 'Authorization': 'Bearer your_api_key_here' }), responseType: 'stream' })
response.data.pipe(fs.createWriteStream('result.docx')) } catch (e) { const errorString = await streamToString(e.response.data) console.log(errorString) }})()
function streamToString(stream) { const chunks = [] return new Promise((resolve, reject) => { stream.on('data', (chunk) => chunks.push(Buffer.from(chunk))) stream.on('error', (err) => reject(err)) stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf8'))) })}import jsonimport requests
response = requests.request( 'POST', 'https://api.nutrient.io/process_office_template', headers = { 'Authorization': 'Bearer your_api_key_here' }, files = { 'document': ('template.docx', open('template.docx', 'rb'), 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') }, data = { 'model': json.dumps({ 'name': 'Alex', 'company': 'Acme', 'date': '2026-04-10' }) }, stream = True)
if response.ok: with open('result.docx', 'wb') as fd: for chunk in response.iter_content(chunk_size=8096): fd.write(chunk)else: print(response.text) exit()<?php
$FileHandle = fopen('result.docx', 'w+');
$curl = curl_init();
curl_setopt_array($curl, array( CURLOPT_URL => 'https://api.nutrient.io/process_office_template', CURLOPT_CUSTOMREQUEST => 'POST', CURLOPT_RETURNTRANSFER => true, CURLOPT_ENCODING => '', CURLOPT_POSTFIELDS => array( 'document' => new CURLFILE('template.docx', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'template.docx'), 'model' => '{ "name": "Alex", "company": "Acme", "date": "2026-04-10" }' ), CURLOPT_HTTPHEADER => array( 'Authorization: Bearer your_api_key_here' ), CURLOPT_FILE => $FileHandle,));
$response = curl_exec($curl);
curl_close($curl);
fclose($FileHandle);POST https://api.nutrient.io/process_office_template HTTP/1.1Content-Type: multipart/form-data; boundary=--customboundaryAuthorization: Bearer your_api_key_here
--customboundaryContent-Disposition: form-data; name="document"; filename="template.docx"Content-Type: application/vnd.openxmlformats-officedocument.wordprocessingml.document
(template.docx data)--customboundaryContent-Disposition: form-data; name="model"
{"name":"Alex","company":"Acme","date":"2026-04-10"}--customboundary--Step 3: Checking the output
The API returns result.docx with placeholders replaced by model values.
Supported templating features
Beyond basic text replacement, the DOCX templating API also supports more advanced template logic. For example:
- Repeated sections with loops
- Nested loops for hierarchical data
- Conditional blocks
- Dynamic table row generation
- Automatic content reflow as generated content grows
As content expands or contracts, the generated DOCX reflows naturally across lines, tables, and pages while preserving the formatting defined in the Word template.
Loops and nested loops
Use loops to repeat content for arrays in your JSON model — for example, invoice items, employees, or grouped sections in a report.
With double-brace delimiters, loop syntax looks like this:
{{#items}}- {{name}}: {{price}}{{/items}}You can also nest loops for hierarchical data:
{{#sections}}{{title}}{{#items}}- {{title}}{{/items}}{{/sections}}For bulleted or numbered lists, place the opening loop marker immediately before the list and the closing marker immediately after it so Word keeps the intended list formatting.
Conditional blocks
Use conditionals to show or hide content based on Boolean values in your model:
{{#isBlue}}Blue{{/isBlue}}{{^isBlue}}Red{{/isBlue}}In this example, isBlue is the condition name:
{{#isBlue}}Blue{{/isBlue}}rendersBluewhenisBlueistrue{{^isBlue}}Red{{/isBlue}}rendersRedwhenisBlueisfalse{{/isBlue}}closes the conditional block
Conditionals can also be used inside loops, including nested loops.
Dynamic table loops
You can place loop markers inside Word tables to generate rows dynamically from array data. This is useful for invoices, order summaries, and reports where the number of rows varies per document.
For example, a table row can contain placeholders like:
{{#lineItems}}{{product}} | {{quantity}} | {{price}}{{/lineItems}}When the template is processed, the row repeats once for each entry in lineItems.
End-to-end example with loops, nested data, conditionals, and table rows
The example below shows a single template using several supported features together:
- Placeholder replacement
- Conditional sections
- Nested loops
- Dynamic table rows
Example Word template
Create a template named proposal-template.docx with content like this:
Proposal for {{clientName}}Prepared by {{salesRep}}
{{#hasDiscount}}Discount approved: {{discountLabel}}{{/hasDiscount}}{{^hasDiscount}}Standard pricing applies.{{/hasDiscount}}
Project phases{{#phases}}{{name}}{{#tasks}}- {{title}}{{#isOptional}}(Optional){{/isOptional}}{{/tasks}}{{/phases}}
Line items{{#lineItems}}{{description}} | {{quantity}} | {{unitPrice}} | {{total}}{{/lineItems}}
Grand total: {{grandTotal}}In Word, the lineItems loop would normally be placed inside a table row so the row is repeated for each item.
Example JSON model
Send the following model together with your template:
{ "config": { "delimiter": { "start": "{{", "end": "}}" } }, "model": { "clientName": "Bluebird Health", "salesRep": "Alex Smith", "hasDiscount": true, "discountLabel": "10% annual agreement discount", "phases": [ { "name": "Discovery", "tasks": [ { "title": "Requirements workshop", "isOptional": false }, { "title": "Architecture review", "isOptional": true } ] }, { "name": "Implementation", "tasks": [ { "title": "Template integration", "isOptional": false }, { "title": "User training", "isOptional": false } ] } ], "lineItems": [ { "description": "Platform setup", "quantity": 1, "unitPrice": "$1,200.00", "total": "$1,200.00" }, { "description": "Template implementation", "quantity": 3, "unitPrice": "$400.00", "total": "$1,200.00" } ], "grandTotal": "$2,400.00" }}How the template is evaluated
{{clientName}},{{salesRep}}, and{{grandTotal}}are replaced with scalar values.{{#hasDiscount}}...{{/hasDiscount}}renders only whenhasDiscountistrue.{{^hasDiscount}}...{{/hasDiscount}}renders whenhasDiscountisfalse.{{#phases}}...{{/phases}}repeats once for each project phase.{{#tasks}}...{{/tasks}}runs inside each phase, demonstrating nested loops.{{#lineItems}}...{{/lineItems}}repeats table content for each billing item.
Result
When processed, the API generates a DOCX document with all placeholders resolved, repeated sections expanded, optional blocks shown or hidden, and table rows added as needed. If the generated content becomes longer than the original layout, Word content reflows across lines and pages automatically.
Using custom delimiters
If your template uses double braces such as {{name}}, send config.delimiter with your model.
Step 1: Updating placeholders in the template
Update template.docx to use double braces:
Hello {{name}}Company: {{company}}Date: {{date}}Step 2: Sending an advanced request
In this request shape:
- Put your data under
model. - Set delimiters to
{{and}}. - The processor then reads double-brace placeholders instead of single-brace placeholders.
curl -X POST https://api.nutrient.io/process_office_template \ -H "Authorization: Bearer your_api_key_here" \ -o result.docx \ --fail \ -F 'document=@template.docx;type=application/vnd.openxmlformats-officedocument.wordprocessingml.document' \ -F 'model={"config":{"delimiter":{"start":"{{","end":"}}"}},"model":{"name":"Alex","company":"Acme","date":"2026-04-10"}}'curl -X POST https://api.nutrient.io/process_office_template ^ -H "Authorization: Bearer your_api_key_here" ^ -o result.docx ^ --fail ^ -F "document=@template.docx;type=application/vnd.openxmlformats-officedocument.wordprocessingml.document" ^ -F "model={\"config\":{\"delimiter\":{\"start\":\"{{\",\"end\":\"}}\"}},\"model\":{\"name\":\"Alex\",\"company\":\"Acme\",\"date\":\"2026-04-10\"}}"package com.example.pspdfkit;
import java.io.File;import java.io.IOException;import java.nio.file.FileSystems;import java.nio.file.Files;import java.nio.file.StandardCopyOption;
import org.json.JSONObject;
import okhttp3.MediaType;import okhttp3.MultipartBody;import okhttp3.OkHttpClient;import okhttp3.Request;import okhttp3.RequestBody;import okhttp3.Response;
public final class PspdfkitApiExample { public static void main(final String[] args) throws IOException { final MediaType mediaType = MediaType.parse("application/vnd.openxmlformats-officedocument.wordprocessingml.document"); final JSONObject model = new JSONObject() .put("config", new JSONObject() .put("delimiter", new JSONObject() .put("start", "{{") .put("end", "}}") ) ) .put("model", new JSONObject() .put("name", "Alex") .put("company", "Acme") .put("date", "2026-04-10") );
final RequestBody body = new MultipartBody.Builder() .setType(MultipartBody.FORM) .addFormDataPart( "document", "template.docx", RequestBody.create( mediaType, new File("template.docx") ) ) .addFormDataPart("model", model.toString()) .build();
final Request request = new Request.Builder() .url("https://api.nutrient.io/process_office_template") .method("POST", body) .addHeader("Authorization", "Bearer your_api_key_here") .build();
final OkHttpClient client = new OkHttpClient() .newBuilder() .build();
final Response response = client.newCall(request).execute();
if (response.isSuccessful()) { Files.copy( response.body().byteStream(), FileSystems.getDefault().getPath("result.docx"), StandardCopyOption.REPLACE_EXISTING ); } else { throw new IOException(response.body().string()); } }}using System;using System.IO;using System.Net;using RestSharp;
namespace PspdfkitApiDemo{ class Program { static void Main(string[] args) { var client = new RestClient("https://api.nutrient.io/process_office_template");
var request = new RestRequest(Method.POST) .AddHeader("Authorization", "Bearer your_api_key_here") .AddFile("document", "template.docx") .AddParameter("model", new JsonObject { ["config"] = new JsonObject { ["delimiter"] = new JsonObject { ["start"] = "{{", ["end"] = "}}" } }, ["model"] = new JsonObject { ["name"] = "Alex", ["company"] = "Acme", ["date"] = "2026-04-10" } }.ToString());
request.AdvancedResponseWriter = (responseStream, response) => { if (response.StatusCode == HttpStatusCode.OK) { using (responseStream) { using var outputFileWriter = File.OpenWrite("result.docx"); responseStream.CopyTo(outputFileWriter); } } else { var responseStreamReader = new StreamReader(responseStream); Console.Write(responseStreamReader.ReadToEnd()); } };
client.Execute(request); } }}// This code requires Node.js. Do not run this code directly in a web browser.
const axios = require('axios')const FormData = require('form-data')const fs = require('fs')
const formData = new FormData()formData.append('document', fs.createReadStream('template.docx'), { contentType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', filename: 'template.docx'})formData.append('model', JSON.stringify({ config: { delimiter: { start: '{{', end: '}}' } }, model: { name: 'Alex', company: 'Acme', date: '2026-04-10' }}))
;(async () => { try { const response = await axios.post('https://api.nutrient.io/process_office_template', formData, { headers: formData.getHeaders({ 'Authorization': 'Bearer your_api_key_here' }), responseType: 'stream' })
response.data.pipe(fs.createWriteStream('result.docx')) } catch (e) { const errorString = await streamToString(e.response.data) console.log(errorString) }})()
function streamToString(stream) { const chunks = [] return new Promise((resolve, reject) => { stream.on('data', (chunk) => chunks.push(Buffer.from(chunk))) stream.on('error', (err) => reject(err)) stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf8'))) })}import jsonimport requests
response = requests.request( 'POST', 'https://api.nutrient.io/process_office_template', headers = { 'Authorization': 'Bearer your_api_key_here' }, files = { 'document': ('template.docx', open('template.docx', 'rb'), 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') }, data = { 'model': json.dumps({ 'config': { 'delimiter': { 'start': '{{', 'end': '}}' } }, 'model': { 'name': 'Alex', 'company': 'Acme', 'date': '2026-04-10' } }) }, stream = True)
if response.ok: with open('result.docx', 'wb') as fd: for chunk in response.iter_content(chunk_size=8096): fd.write(chunk)else: print(response.text) exit()<?php
$FileHandle = fopen('result.docx', 'w+');
$curl = curl_init();
curl_setopt_array($curl, array( CURLOPT_URL => 'https://api.nutrient.io/process_office_template', CURLOPT_CUSTOMREQUEST => 'POST', CURLOPT_RETURNTRANSFER => true, CURLOPT_ENCODING => '', CURLOPT_POSTFIELDS => array( 'document' => new CURLFILE('template.docx', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'template.docx'), 'model' => '{ "config": { "delimiter": { "start": "{{", "end": "}}" } }, "model": { "name": "Alex", "company": "Acme", "date": "2026-04-10" } }' ), CURLOPT_HTTPHEADER => array( 'Authorization: Bearer your_api_key_here' ), CURLOPT_FILE => $FileHandle,));
$response = curl_exec($curl);
curl_close($curl);
fclose($FileHandle);POST https://api.nutrient.io/process_office_template HTTP/1.1Content-Type: multipart/form-data; boundary=--customboundaryAuthorization: Bearer your_api_key_here
--customboundaryContent-Disposition: form-data; name="document"; filename="template.docx"Content-Type: application/vnd.openxmlformats-officedocument.wordprocessingml.document
(template.docx data)--customboundaryContent-Disposition: form-data; name="model"
{"config":{"delimiter":{"start":"{{","end":"}}"}},"model":{"name":"Alex","company":"Acme","date":"2026-04-10"}}--customboundary--Step 3: Verifying the generated document
Open result.docx in Word, Pages, or another DOCX-compatible editor. Confirm that:
- Placeholders are removed.
- Expected values appear in the correct locations.
- The document opens normally and keeps original formatting.
After this flow works, extend your templates with repeated sections, nested data, and image placeholders.