210 × 297 mm
Payslips
Monthly payslip
An A4 monthly payslip with employer and employee details, earnings and deductions tables with this-period and year-to-date columns, employer contributions, a net pay panel with the amount in words, payment details and annual leave. Sample figures are illustrative.
Opens Dynamic Document API with this template preloaded in a free workspace.
Template language features
- Loops
- Conditionals
- Macros
- Filters
- Currency formatting
- Number formatting
- Date formatting
Filters such as format_currency and qrcode are described in the template language reference.
Variables
Derived from the sample data: 9 top-level fields, 44 paths in total. Send the same shape as data.
| Path | Type | Example |
|---|---|---|
| employer | object | |
| employer.name | string | "Fennick & Rowe Instruments Ltd" |
| employer.address | string | "Unit 2, Heathfield Science Park ⏎ Cambridge CB…" |
| employer.paye_reference | string | "123/AB45678" |
| employer.payroll_email | string | "payroll@fennickrowe.example.com" |
| employee | object | |
| employee.name | string | "Saoirse Pembridge" |
| employee.employee_number | string | "E-00417" |
| employee.job_title | string | "Senior Test Engineer" |
| employee.department | string | "Test Engineering" |
| employee.ni_number | string | "QQ 12 34 56 C" |
| employee.tax_code | string | "1257L" |
| employee.ni_category | string | "A" |
| employee.start_date | date | "2021-04-12" |
| period | object | |
| period.label | string | "September 2026" |
| period.tax_year | string | "2026/27" |
| period.tax_period | number | 6 |
| period.frequency | string | "Monthly" |
| period.pay_date | date | "2026-09-25" |
| period.currency | string | "GBP" |
| earnings | array<object> · 4 | |
| earnings[].description | string | "Basic salary" |
| earnings[].detail | string | "£46,800.00 a year" |
| earnings[].amount | number | 3900 |
| earnings[].ytd | number | 23400 |
| deductions | array<object> · 5 | |
| deductions[].description | string | "Income tax" |
| deductions[].detail | string | "Tax code 1257L" |
| deductions[].amount | number | 604.46 |
| deductions[].ytd | number | 3654.88 |
| employer_contributions | array<object> · 2 | |
| employer_contributions[].description | string | "Employer National Insurance" |
| employer_contributions[].detail | string | "Paid on your earnings" |
| employer_contributions[].amount | number | 580.1 |
| employer_contributions[].ytd | number | 3502.8 |
| payment | object | |
| payment.method | string | "Bank transfer" |
| payment.account_hint | string | "account ending 4821" |
| leave | object | |
| leave.entitlement_days | number | 27 |
| leave.taken_days | number | 12.5 |
| leave.remaining_days | number | 14.5 |
| message | string | "The pension scheme's annual statements are now…" |
Sample data
Fictional records used for previews. Replace them with your own data when you call the API.
{
"employer": {
"name": "Fennick & Rowe Instruments Ltd",
"address": "Unit 2, Heathfield Science Park\nCambridge CB4 0XX\nUnited Kingdom",
"paye_reference": "123/AB45678",
"payroll_email": "payroll@fennickrowe.example.com"
},
"employee": {
"name": "Saoirse Pembridge",
"employee_number": "E-00417",
"job_title": "Senior Test Engineer",
"department": "Test Engineering",
"ni_number": "QQ 12 34 56 C",
"tax_code": "1257L",
"ni_category": "A",
"start_date": "2021-04-12"
},
"period": {
"label": "September 2026",
"tax_year": "2026/27",
"tax_period": 6,
"frequency": "Monthly",
"pay_date": "2026-09-25",
"currency": "GBP"
},
"earnings": [
{ "description": "Basic salary", "detail": "£46,800.00 a year", "amount": 3900.0, "ytd": 23400.0 },
{ "description": "Overtime at time and a half", "detail": "6.5 hours × £36.00", "amount": 234.0, "ytd": 1152.0 },
{ "description": "On-call allowance", "detail": "3 weekends × £50.00", "amount": 150.0, "ytd": 700.0 },
{ "description": "Quarterly bonus", "detail": "Paid in June", "amount": 0.0, "ytd": 600.0 }
],
"deductions": [
{ "description": "Income tax", "detail": "Tax code 1257L", "amount": 604.46, "ytd": 3654.88 },
{ "description": "National Insurance", "detail": "Category A", "amount": 258.88, "ytd": 1565.12 },
{ "description": "Pension, employee contribution", "detail": "5% of gross pay", "amount": 214.2, "ytd": 1292.6 },
{ "description": "Student loan", "detail": "Plan 2", "amount": 172.0, "ytd": 1044.0 },
{ "description": "Cycle to work scheme", "detail": "Instalment 6 of 12", "amount": 41.5, "ytd": 249.0 }
],
"employer_contributions": [
{ "description": "Employer National Insurance", "detail": "Paid on your earnings", "amount": 580.1, "ytd": 3502.8 },
{ "description": "Employer pension contribution", "detail": "8% of gross pay", "amount": 342.72, "ytd": 2068.16 }
],
"payment": {
"method": "Bank transfer",
"account_hint": "account ending 4821"
},
"leave": {
"entitlement_days": 27,
"taken_days": 12.5,
"remaining_days": 14.5
},
"message": "The pension scheme's annual statements are now available in the employee portal. Contact payroll by 5 October if any details on this payslip look wrong."
}Source
The body markup with Jinja, the stylesheet placed in <head>.
{#- Monthly payslip. Totals and net pay are derived from the earnings and deduction lines; figures are illustrative. -#}
{%- set locale = "en-GB" -%}
{%- set currency = period.currency -%}
{%- set gross = earnings | sum_by("amount") -%}
{%- set gross_ytd = earnings | sum_by("ytd") -%}
{%- set total_deductions = deductions | sum_by("amount") -%}
{%- set total_deductions_ytd = deductions | sum_by("ytd") -%}
{%- set net_pay = gross - total_deductions -%}
{%- set net_pay_ytd = gross_ytd - total_deductions_ytd -%}
{%- set pence = ((net_pay * 100) | round | int) % 100 -%}
{%- macro money(value) -%}{{ value | format_currency(currency, locale) }}{%- endmacro -%}
{%- macro pay_table(title, rows, total_label, css_class, detail_column=true) -%}
<table class="pay {{ css_class }}">
<thead>
<tr>
<th class="desc" scope="col">{{ title }}</th>
{%- if detail_column %}
<th class="detail" scope="col">Details</th>
{%- endif %}
<th class="num" scope="col">This period</th>
<th class="num" scope="col">Year to date</th>
</tr>
</thead>
<tbody>
{%- for row in rows %}
<tr>
<th class="desc" scope="row">{{ row.description }}{% if not detail_column and row.detail %}<span class="sub">{{ row.detail }}</span>{% endif %}</th>
{%- if detail_column %}
<td class="detail">{{ row.detail }}</td>
{%- endif %}
<td class="num">{% if row.amount %}{{ money(row.amount) }}{% else %}<span class="nil">–</span>{% endif %}</td>
<td class="num ytd">{{ money(row.ytd) }}</td>
</tr>
{%- endfor %}
</tbody>
<tfoot>
<tr>
<th colspan="{{ 2 if detail_column else 1 }}" scope="row">{{ total_label }}</th>
<td class="num">{{ money(rows | sum_by("amount")) }}</td>
<td class="num ytd">{{ money(rows | sum_by("ytd")) }}</td>
</tr>
</tfoot>
</table>
{%- endmacro %}
<header class="masthead">
<div class="employer">
<svg class="logo" viewBox="0 0 40 40" aria-hidden="true">
<circle cx="20" cy="20" r="18" fill="#2f3a8f"/>
<path d="M11 25a9 9 0 0 1 18 0" fill="none" stroke="#c7ccf2" stroke-width="2.6" stroke-linecap="round"/>
<path d="M20 25l5-7" stroke="#ffffff" stroke-width="2.6" stroke-linecap="round"/>
<circle cx="20" cy="25" r="2.2" fill="#ffffff"/>
</svg>
<div>
<p class="employer-name">{{ employer.name }}</p>
<p class="employer-meta">{{ employer.address | replace("\n", ", ") }}</p>
<p class="employer-meta">Employer PAYE reference {{ employer.paye_reference }}</p>
</div>
</div>
<div class="doc">
<p class="confidential">Private and confidential</p>
<h1>Payslip</h1>
<p class="period">{{ period.label }}</p>
</div>
</header>
<section class="employee">
<div class="who">
<p class="label">Employee</p>
<p class="employee-name">{{ employee.name }}</p>
<p>{{ employee.job_title }} · {{ employee.department }}</p>
</div>
<dl>
<div><dt>Employee no.</dt><dd>{{ employee.employee_number }}</dd></div>
<div><dt>NI number</dt><dd>{{ employee.ni_number }}</dd></div>
<div><dt>Tax code</dt><dd>{{ employee.tax_code }}</dd></div>
<div><dt>NI category</dt><dd>{{ employee.ni_category }}</dd></div>
<div><dt>Pay date</dt><dd>{{ period.pay_date | format_date("long", locale) }}</dd></div>
<div><dt>Tax period</dt><dd>Month {{ period.tax_period }}, {{ period.tax_year }}</dd></div>
</dl>
</section>
{{ pay_table("Earnings", earnings, "Gross pay", "earnings") }}
{{ pay_table("Deductions", deductions, "Total deductions", "deductions") }}
<section class="net">
<div class="net-main">
<p class="label">Net pay</p>
<p class="net-amount">{{ money(net_pay) }}</p>
<p class="net-words">{{ (net_pay | int) | number_to_words("en") | capitalize }} pounds{% if pence %} and {{ pence }} pence{% endif %}</p>
</div>
<dl class="net-side">
<div><dt>Paid by</dt><dd>{{ payment.method }} to {{ payment.account_hint }}</dd></div>
<div><dt>Paid on</dt><dd>{{ period.pay_date | format_date("%A %d %B %Y", locale) }}</dd></div>
</dl>
</section>
<section class="summary">
<div><span>Gross pay</span><strong>{{ money(gross) }}</strong><em>{{ money(gross_ytd) }} year to date</em></div>
<div class="minus"><span>Deductions</span><strong>−{{ money(total_deductions) }}</strong><em>{{ money(total_deductions_ytd) }} year to date</em></div>
<div class="equals"><span>Net pay</span><strong>{{ money(net_pay) }}</strong><em>{{ money(net_pay_ytd) }} year to date</em></div>
</section>
<section class="lower">
<div class="employer-costs">
{{ pay_table("Paid by your employer", employer_contributions, "Employer contributions", "contributions", detail_column=false) }}
<p class="hint">These amounts are paid by {{ employer.name }} on top of your pay and are not deducted from it.</p>
</div>
<aside class="info">
<p class="label">Annual leave</p>
<div class="leave">
<span class="leave-bar"><span class="leave-fill" style="width: {{ (leave.taken_days / leave.entitlement_days * 100) | round(1) }}%"></span></span>
<p><strong>{{ leave.remaining_days }} days</strong> remaining of {{ leave.entitlement_days }} ({{ leave.taken_days }} taken)</p>
</div>
{%- if message %}
<p class="label">Message from payroll</p>
<p class="message">{{ message }}</p>
{%- endif %}
<p class="contact">Questions: {{ employer.payroll_email }}</p>
</aside>
</section><title>Payslip {{ period.label }} – {{ employee.name }}</title>
<style>
:root {
--ink: #1c2033;
--muted: #626782;
--rule: #e0e3f0;
--indigo: #2f3a8f;
--lavender: #eef0fb;
--emerald: #0f7b5f;
--emerald-soft: #e3f4ee;
}
* { box-sizing: border-box; }
html { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
body {
margin: 0;
color: var(--ink);
font-family: "Source Sans 3", "Liberation Sans", Arial, sans-serif;
font-size: 9.2pt;
line-height: 1.4;
}
p, h1, dl, dd { margin: 0; }
.num { text-align: right; white-space: nowrap; font-variant-numeric: tabular-nums; }
.label {
margin-bottom: 1mm;
font-size: 7pt;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--indigo);
}
/* Masthead */
.masthead { display: flex; justify-content: space-between; align-items: flex-start; }
.employer { display: flex; gap: 3mm; }
.logo { flex: none; width: 11mm; height: 11mm; }
.employer-name { font-size: 12pt; font-weight: 700; line-height: 1.25; }
.employer-meta { font-size: 8pt; color: var(--muted); }
.doc { text-align: right; }
.confidential { font-size: 7pt; font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase; color: var(--muted); }
.doc h1 { font-size: 24pt; font-weight: 300; line-height: 1.05; color: var(--indigo); }
.period { font-size: 11pt; font-weight: 600; }
/* Employee */
.employee {
display: grid;
grid-template-columns: 58mm 1fr;
gap: 6mm;
margin: 4mm 0;
padding: 3mm 4.5mm;
border-radius: 2mm;
background: var(--lavender);
}
.employee-name { font-size: 13pt; font-weight: 700; line-height: 1.2; }
.employee dl { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1.6mm 4mm; }
.employee dt { font-size: 7pt; color: var(--muted); }
.employee dd { font-weight: 600; font-variant-numeric: tabular-nums; }
/* Pay tables */
table.pay { width: 100%; margin-bottom: 4mm; border-collapse: collapse; }
table.pay thead { display: table-header-group; }
table.pay thead th {
padding: 1.6mm 2mm;
border-bottom: 0.4mm solid var(--indigo);
font-size: 7.2pt;
font-weight: 700;
letter-spacing: 0.06em;
text-align: left;
text-transform: uppercase;
color: var(--muted);
}
table.pay thead th.desc { color: var(--indigo); font-size: 8pt; }
table.pay thead th.num { text-align: right; }
table.pay td, table.pay tbody th, table.pay tfoot th { padding: 1.3mm 2mm; border-bottom: 0.2mm solid var(--rule); }
table.pay tbody th, table.pay tfoot th { font-weight: inherit; text-align: left; }
table.pay tr { break-inside: avoid; page-break-inside: avoid; }
table.pay .desc { width: 64mm; }
table.pay td.detail { color: var(--muted); }
table.pay .num { width: 30mm; }
table.pay .ytd { color: var(--muted); }
table.pay tfoot td, table.pay tfoot th { border-top: 0.3mm solid var(--ink); border-bottom: none; font-weight: 700; }
table.pay tfoot .ytd { color: var(--ink); }
.nil { color: #a3a8bf; }
/* Net pay */
.net {
display: grid;
grid-template-columns: 1fr 72mm;
gap: 6mm;
align-items: center;
margin: 4mm 0 3.5mm;
padding: 4mm 5mm;
border-radius: 2.5mm;
background: var(--emerald);
color: #ffffff;
break-inside: avoid;
page-break-inside: avoid;
}
.net .label { color: #bfe6d9; }
.net-amount { font-size: 27pt; font-weight: 700; line-height: 1.05; font-variant-numeric: tabular-nums; }
.net-words { margin-top: 1mm; font-size: 9pt; color: #dff3ec; }
.net-side { padding-left: 5mm; border-left: 0.3mm solid #3f9a80; }
.net-side div { margin-bottom: 1.4mm; }
.net-side dt { font-size: 7pt; letter-spacing: 0.06em; text-transform: uppercase; color: #bfe6d9; }
.net-side dd { font-weight: 600; }
.summary { display: grid; grid-template-columns: repeat(3, 1fr); gap: 3mm; margin-bottom: 5mm; break-inside: avoid; page-break-inside: avoid; }
.summary div { position: relative; padding: 2.4mm 3.5mm; border: 0.3mm solid var(--rule); border-radius: 1.5mm; }
.summary span { display: block; font-size: 7pt; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted); }
.summary strong { display: block; font-size: 12pt; font-variant-numeric: tabular-nums; }
.summary em { display: block; font-size: 7.6pt; font-style: normal; color: var(--muted); font-variant-numeric: tabular-nums; }
.summary .equals { border-color: var(--emerald); background: var(--emerald-soft); }
.summary .equals strong { color: var(--emerald); }
/* Employer contributions and other information */
.lower { display: grid; grid-template-columns: 1fr 58mm; gap: 7mm; break-inside: avoid; page-break-inside: avoid; }
.contributions { margin-bottom: 1.5mm; }
.contributions .desc { width: auto; }
.contributions .num { width: 24mm; }
.sub { display: block; font-size: 7.6pt; color: var(--muted); }
.hint { font-size: 7.6pt; color: var(--muted); }
.info { padding: 3mm 4mm; border-radius: 2mm; background: var(--lavender); }
.leave { margin-bottom: 3mm; }
.leave-bar { display: block; height: 2mm; margin: 0.6mm 0 1.2mm; border-radius: 1mm; background: #d5d9f2; overflow: hidden; }
.leave-fill { display: block; height: 100%; border-radius: 1mm; background: var(--indigo); }
.leave p { font-size: 8.4pt; }
.message { margin-bottom: 2.5mm; font-size: 8.4pt; }
.contact { font-size: 7.6pt; color: var(--muted); }
</style>Render it with the API
After adding the template to your workspace, render it by ID and pass your data. Arrays are shortened here.
curl -X POST https://api-eu.dynamicdocumentapi.com/v1/renders \
-H "Authorization: Bearer $DYNAMIC_DOCUMENT_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"input": {
"type": "template",
"template_id": "tpl_01J9ZK3M7Q8V5W2X4Y6Z8A0B1C"
},
"data": {
"employer": {
"name": "Fennick & Rowe Instruments Ltd",
"address": "Unit 2, Heathfield Science Park\nCambridge CB4 0XX\nUnited Kingdom",
"paye_reference": "123/AB45678",
"payroll_email": "payroll@fennickrowe.example.com"
},
"employee": {
"name": "Saoirse Pembridge",
"employee_number": "E-00417",
"job_title": "Senior Test Engineer",
"department": "Test Engineering",
"ni_number": "QQ 12 34 56 C",
"tax_code": "1257L",
"ni_category": "A",
"start_date": "2021-04-12"
},
"period": {
"label": "September 2026",
"tax_year": "2026/27",
"tax_period": 6,
"frequency": "Monthly",
"pay_date": "2026-09-25",
"currency": "GBP"
},
"earnings": [
{
"description": "Basic salary",
"detail": "£46,800.00 a year",
"amount": 3900,
"ytd": 23400
},
{
"description": "Overtime at time and a half",
"detail": "6.5 hours × £36.00",
"amount": 234,
"ytd": 1152
}
],
"deductions": [
{
"description": "Income tax",
"detail": "Tax code 1257L",
"amount": 604.46,
"ytd": 3654.88
},
{
"description": "National Insurance",
"detail": "Category A",
"amount": 258.88,
"ytd": 1565.12
}
],
"employer_contributions": [
{
"description": "Employer National Insurance",
"detail": "Paid on your earnings",
"amount": 580.1,
"ytd": 3502.8
},
{
"description": "Employer pension contribution",
"detail": "8% of gross pay",
"amount": 342.72,
"ytd": 2068.16
}
],
"payment": {
"method": "Bank transfer",
"account_hint": "account ending 4821"
},
"leave": {
"entitlement_days": 27,
"taken_days": 12.5,
"remaining_days": 14.5
},
"message": "The pension scheme'\''s annual statements are now available in the employee portal. Contact payroll by 5 October if any details on this payslip look wrong."
},
"output": {
"format": "pdf",
"filename": "payslip-monthly.pdf"
}
}'const response = await fetch("https://api-eu.dynamicdocumentapi.com/v1/renders", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.DYNAMIC_DOCUMENT_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({
"input": {
"type": "template",
"template_id": "tpl_01J9ZK3M7Q8V5W2X4Y6Z8A0B1C"
},
"data": {
"employer": {
"name": "Fennick & Rowe Instruments Ltd",
"address": "Unit 2, Heathfield Science Park\nCambridge CB4 0XX\nUnited Kingdom",
"paye_reference": "123/AB45678",
"payroll_email": "payroll@fennickrowe.example.com"
},
"employee": {
"name": "Saoirse Pembridge",
"employee_number": "E-00417",
"job_title": "Senior Test Engineer",
"department": "Test Engineering",
"ni_number": "QQ 12 34 56 C",
"tax_code": "1257L",
"ni_category": "A",
"start_date": "2021-04-12"
},
"period": {
"label": "September 2026",
"tax_year": "2026/27",
"tax_period": 6,
"frequency": "Monthly",
"pay_date": "2026-09-25",
"currency": "GBP"
},
"earnings": [
{
"description": "Basic salary",
"detail": "£46,800.00 a year",
"amount": 3900,
"ytd": 23400
},
{
"description": "Overtime at time and a half",
"detail": "6.5 hours × £36.00",
"amount": 234,
"ytd": 1152
}
],
"deductions": [
{
"description": "Income tax",
"detail": "Tax code 1257L",
"amount": 604.46,
"ytd": 3654.88
},
{
"description": "National Insurance",
"detail": "Category A",
"amount": 258.88,
"ytd": 1565.12
}
],
"employer_contributions": [
{
"description": "Employer National Insurance",
"detail": "Paid on your earnings",
"amount": 580.1,
"ytd": 3502.8
},
{
"description": "Employer pension contribution",
"detail": "8% of gross pay",
"amount": 342.72,
"ytd": 2068.16
}
],
"payment": {
"method": "Bank transfer",
"account_hint": "account ending 4821"
},
"leave": {
"entitlement_days": 27,
"taken_days": 12.5,
"remaining_days": 14.5
},
"message": "The pension scheme's annual statements are now available in the employee portal. Contact payroll by 5 October if any details on this payslip look wrong."
},
"output": {
"format": "pdf",
"filename": "payslip-monthly.pdf"
}
}),
});
if (!response.ok) throw new Error(`Request failed: ${response.status}`);
const result = await response.json();
console.log(result);import os
import uuid
import requests
response = requests.post(
"https://api-eu.dynamicdocumentapi.com/v1/renders",
headers={
"Authorization": f"Bearer {os.environ['DYNAMIC_DOCUMENT_API_KEY']}",
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"input": {
"type": "template",
"template_id": "tpl_01J9ZK3M7Q8V5W2X4Y6Z8A0B1C",
},
"data": {
"employer": {
"name": "Fennick & Rowe Instruments Ltd",
"address": "Unit 2, Heathfield Science Park\nCambridge CB4 0XX\nUnited Kingdom",
"paye_reference": "123/AB45678",
"payroll_email": "payroll@fennickrowe.example.com",
},
"employee": {
"name": "Saoirse Pembridge",
"employee_number": "E-00417",
"job_title": "Senior Test Engineer",
"department": "Test Engineering",
"ni_number": "QQ 12 34 56 C",
"tax_code": "1257L",
"ni_category": "A",
"start_date": "2021-04-12",
},
"period": {
"label": "September 2026",
"tax_year": "2026/27",
"tax_period": 6,
"frequency": "Monthly",
"pay_date": "2026-09-25",
"currency": "GBP",
},
"earnings": [
{
"description": "Basic salary",
"detail": "£46,800.00 a year",
"amount": 3900,
"ytd": 23400,
},
{
"description": "Overtime at time and a half",
"detail": "6.5 hours × £36.00",
"amount": 234,
"ytd": 1152,
},
],
"deductions": [
{
"description": "Income tax",
"detail": "Tax code 1257L",
"amount": 604.46,
"ytd": 3654.88,
},
{
"description": "National Insurance",
"detail": "Category A",
"amount": 258.88,
"ytd": 1565.12,
},
],
"employer_contributions": [
{
"description": "Employer National Insurance",
"detail": "Paid on your earnings",
"amount": 580.1,
"ytd": 3502.8,
},
{
"description": "Employer pension contribution",
"detail": "8% of gross pay",
"amount": 342.72,
"ytd": 2068.16,
},
],
"payment": {
"method": "Bank transfer",
"account_hint": "account ending 4821",
},
"leave": {
"entitlement_days": 27,
"taken_days": 12.5,
"remaining_days": 14.5,
},
"message": "The pension scheme's annual statements are now available in the employee portal. Contact payroll by 5 October if any details on this payslip look wrong.",
},
"output": {
"format": "pdf",
"filename": "payslip-monthly.pdf",
},
},
timeout=90,
)
response.raise_for_status()
print(response.json())