210 × 297 mm
Quotes & estimates
Phased project estimate
An A4 estimate for agencies and studios that plan work in phases. Tasks sit inside each phase with days and day rates, optional extras are listed separately and left out of the total, and it adds a timeline, payment schedule, assumptions and a signature block.
Opens Dynamic Document API with this template preloaded in a free workspace.
Template language features
- Loops
- Conditionals
- Macros
- Filters
- Currency formatting
- Date formatting
- Number formatting
- Markdown
- Repeating table headers
- Header & footer
- Page numbers
Filters such as format_currency and qrcode are described in the template language reference.
Variables
Derived from the sample data: 7 top-level fields, 42 paths in total. Send the same shape as data.
| Path | Type | Example |
|---|---|---|
| studio | object | |
| studio.name | string | "Copperbeck Studio Ltd" |
| studio.short_name | string | "Copperbeck Studio" |
| studio.address | string | "Unit 5, Gasholder Yard ⏎ 41 Ropemaker Lane ⏎ L…" |
| studio.email | string | "hello@copperbeck.example.com" |
| studio.phone | string | "+44 20 7946 0321" |
| studio.website | string | "copperbeck.example.com" |
| studio.company_number | string | "12345678" |
| studio.vat_number | string | "GB 123 4567 89" |
| client | object | |
| client.name | string | "Hollin Vale Museum Trust" |
| client.contact | string | "Dr Amara Lindqvist, Director of Audiences" |
| client.address | string | "The Old Mill, Mill Lane ⏎ Hollin Vale ⏎ Hebden…" |
| quote | object | |
| quote.number | string | "CBS-E-2026-041" |
| quote.title | string | "Collections website and online booking" |
| quote.summary | string | "A new public website that brings the museum's …" |
| quote.issue_date | date | "2026-09-16" |
| quote.valid_for_days | number | 30 |
| quote.currency | string | "GBP" |
| quote.locale | string | "en-GB" |
| quote.vat_rate | number | 0.2 |
| quote.prepared_by | string | "Rhys Abernethy" |
| quote.prepared_by_role | string | "Client Partner" |
| phases | array<object> · 4 | |
| phases[].name | string | "Discovery" |
| phases[].summary | string | "Understand visitors, collections data and the …" |
| phases[].start_week | number | 1 |
| phases[].weeks | number | 2 |
| phases[].tasks | array<object> · 3 | |
| phases[].tasks[].name | string | "Stakeholder workshops" |
| phases[].tasks[].details | string | "Two half-day sessions with curators, learning …" |
| phases[].tasks[].days | number | 2 |
| phases[].tasks[].day_rate | number | 650 |
| phases[].tasks[].amount | number | 1300 |
| payment_schedule | array<object> · 3 | |
| payment_schedule[].milestone | string | "On acceptance" |
| payment_schedule[].share | number | 0.3 |
| assumptions | string | "- The museum provides a single point of contac…" |
| acceptance | object | |
| acceptance.studio_signatory | string | "Rhys Abernethy" |
| acceptance.studio_signatory_role | string | "Client Partner" |
Sample data
Fictional records used for previews. Replace them with your own data when you call the API.
{
"studio": {
"name": "Copperbeck Studio Ltd",
"short_name": "Copperbeck Studio",
"address": "Unit 5, Gasholder Yard\n41 Ropemaker Lane\nLondon N1 7XY",
"email": "hello@copperbeck.example.com",
"phone": "+44 20 7946 0321",
"website": "copperbeck.example.com",
"company_number": "12345678",
"vat_number": "GB 123 4567 89"
},
"client": {
"name": "Hollin Vale Museum Trust",
"contact": "Dr Amara Lindqvist, Director of Audiences",
"address": "The Old Mill, Mill Lane\nHollin Vale\nHebden Bridge HX7 8XY"
},
"quote": {
"number": "CBS-E-2026-041",
"title": "Collections website and online booking",
"summary": "A new public website that brings the museum's collection online, lets visitors book timed entry and events, and gives staff a content system they can run without developer help.",
"issue_date": "2026-09-16",
"valid_for_days": 30,
"currency": "GBP",
"locale": "en-GB",
"vat_rate": 0.2,
"prepared_by": "Rhys Abernethy",
"prepared_by_role": "Client Partner"
},
"phases": [
{
"name": "Discovery",
"summary": "Understand visitors, collections data and the current ticketing set-up.",
"start_week": 1,
"weeks": 2,
"tasks": [
{
"name": "Stakeholder workshops",
"details": "Two half-day sessions with curators, learning and visitor services teams",
"days": 2,
"day_rate": 650,
"amount": 1300
},
{
"name": "Visitor research",
"details": "Eight remote interviews and an on-site survey over two weekends",
"days": 3,
"day_rate": 650,
"amount": 1950
},
{
"name": "Technical audit",
"details": "Collections database export, ticketing system and analytics review",
"days": 1.5,
"day_rate": 650,
"amount": 975
}
]
},
{
"name": "Design",
"summary": "Shape the structure, look and feel, and test it with real visitors.",
"start_week": 3,
"weeks": 3,
"tasks": [
{
"name": "Information architecture",
"details": "Site map, navigation model and content types",
"days": 2,
"day_rate": 650,
"amount": 1300
},
{
"name": "Visual design system",
"details": "Typography, colour, components and accessibility annotations",
"days": 4,
"day_rate": 650,
"amount": 2600
},
{
"name": "Clickable prototype and usability testing",
"details": "Two rounds of testing with five participants each",
"days": 3,
"day_rate": 650,
"amount": 1950
},
{
"name": "Audio guide page designs",
"details": "Mobile-first object pages with transcripts for the gallery audio tour",
"days": 1.5,
"day_rate": 650,
"amount": 975,
"optional": true
}
]
},
{
"name": "Build",
"summary": "Develop, integrate and test the website on a staging environment.",
"start_week": 5,
"weeks": 6,
"tasks": [
{
"name": "Front-end templates and component library",
"details": "Responsive templates built from the approved design system",
"days": 8,
"day_rate": 600,
"amount": 4800
},
{
"name": "Collections search integration",
"details": "Nightly import from the collections database with faceted search",
"days": 6,
"day_rate": 600,
"amount": 3600
},
{
"name": "Booking flow and payment provider integration",
"details": "Timed entry, events and gift memberships",
"days": 5,
"day_rate": 600,
"amount": 3000
},
{
"name": "Accessibility testing and fixes",
"details": "WCAG 2.2 AA audit, including screen reader and keyboard checks",
"days": 3,
"day_rate": 600,
"amount": 1800
},
{
"name": "Recurring donations page",
"details": "Monthly giving form connected to the existing payment provider",
"days": 2.5,
"day_rate": 600,
"amount": 1500,
"optional": true
}
]
},
{
"name": "Launch and handover",
"summary": "Move content across, train the team and support the go-live.",
"start_week": 11,
"weeks": 3,
"tasks": [
{
"name": "Content migration support",
"details": "Import scripts and editorial checks for around 240 pages",
"days": 4,
"day_rate": 450,
"amount": 1800
},
{
"name": "Staff CMS training",
"details": "Two recorded sessions and a printed quick-start guide",
"days": 1,
"day_rate": 650,
"amount": 650
},
{
"name": "Launch and early-life support",
"details": "Go-live support and two weeks of priority fixes",
"days": 2,
"day_rate": 600,
"amount": 1200
},
{
"name": "Twelve-month support retainer",
"details": "Up to 1.5 days a month for updates, monitoring and small changes",
"days": 18,
"day_rate": 400,
"amount": 7200,
"optional": true
}
]
}
],
"payment_schedule": [
{ "milestone": "On acceptance", "share": 0.3 },
{ "milestone": "Design sign-off", "share": 0.4 },
{ "milestone": "Launch", "share": 0.3 }
],
"assumptions": "- The museum provides a single point of contact who can gather feedback and approve each phase within **five working days**.\n- Collections data can be exported nightly in CSV or JSON format; no changes to the collections database itself are included.\n- The booking flow uses the museum's existing payment provider account.\n- Content is supplied or rewritten by museum staff; we provide templates, guidance and migration scripts.\n- Hosting is quoted separately and is not part of this estimate.\n- Prices exclude VAT. Travel within Greater London is included; other travel is charged at cost with prior approval.",
"acceptance": {
"studio_signatory": "Rhys Abernethy",
"studio_signatory_role": "Client Partner"
}
}Source
The body markup with Jinja, the stylesheet placed in <head> and the header and footer settings.
{#- Phased project estimate. Tasks are nested inside phases; tasks flagged "optional": true are
listed separately and never counted in the phase subtotals or the total. Totals are derived by
mapping filters over the phases, so there is nothing to keep in sync by hand. -#}
{%- set locale = quote.locale | default("en-GB") -%}
{%- set currency = quote.currency -%}
{%- set task_lists = phases | map(attribute="tasks") | list -%}
{%- set subtotal = task_lists | map("rejectattr", "optional") | map("sum_by", "amount") | sum -%}
{%- set total_days = task_lists | map("rejectattr", "optional") | map("sum_by", "days") | sum -%}
{%- set optional_count = task_lists | map("selectattr", "optional") | map("list") | map("length") | sum -%}
{%- set optional_total = task_lists | map("selectattr", "optional") | map("sum_by", "amount") | sum -%}
{%- set vat = (subtotal * quote.vat_rate) | round(2) -%}
{%- set total = subtotal + vat -%}
{%- set valid_until = quote.issue_date | date_add(days=quote.valid_for_days) -%}
{%- set last_phase = phases | last -%}
{%- set total_weeks = last_phase.start_week + last_phase.weeks - 1 -%}
{%- macro money(value) -%}{{ value | format_currency(currency, locale) }}{%- endmacro -%}
{%- macro days(value) -%}{{ value | format_number(locale, 0 if value == value | int else 1) }}{%- endmacro -%}
<header class="letterhead">
<div class="brand">
<svg class="leaf" viewBox="0 0 40 40" aria-hidden="true">
<path d="M20 3C9 11 7 24 20 37 33 24 31 11 20 3z" fill="#b4532a"/>
<path d="M20 9v27M20 17l-5-4M20 23l6-5M20 29l-6-4" stroke="#f7e3d6" stroke-width="1.6" stroke-linecap="round" fill="none"/>
</svg>
<span class="brand-name">{{ studio.short_name }}</span>
</div>
<div class="contact">
{{ studio.website }} · {{ studio.email }} · {{ studio.phone }}
</div>
</header>
<section class="cover">
<p class="eyebrow">Project estimate · {{ quote.number }}</p>
<h1>{{ quote.title }}</h1>
<p class="lede">{{ quote.summary }}</p>
<div class="cover-grid">
<div>
<p class="label">Prepared for</p>
<p class="strong">{{ client.name }}</p>
<p>{{ client.contact }}</p>
<p class="muted">{{ client.address | nl2br }}</p>
</div>
<div>
<p class="label">Prepared by</p>
<p class="strong">{{ quote.prepared_by }}</p>
<p>{{ quote.prepared_by_role }}, {{ studio.name }}</p>
<p class="muted">{{ studio.address | nl2br }}</p>
</div>
<div>
<p class="label">Dates</p>
<p>Issued {{ quote.issue_date | format_date("long", locale) }}</p>
<p><strong>Valid until {{ valid_until | format_date("long", locale) }}</strong></p>
</div>
</div>
</section>
<section class="glance">
<div class="stat stat-main">
<span class="stat-k">Estimate excl. VAT</span>
<span class="stat-v">{{ money(subtotal) }}</span>
</div>
<div class="stat">
<span class="stat-k">Effort</span>
<span class="stat-v">{{ days(total_days) }} days</span>
</div>
<div class="stat">
<span class="stat-k">Timeline</span>
<span class="stat-v">{{ total_weeks }} weeks</span>
</div>
<div class="stat">
<span class="stat-k">Phases</span>
<span class="stat-v">{{ phases | length }}</span>
</div>
</section>
<section class="timeline">
<h2 class="section-title">Timeline</h2>
<div class="tl-row tl-scale">
<div class="tl-label">Week</div>
<div class="tl-track">
{% for week in range(1, total_weeks + 1) %}<span style="width: {{ (100 / total_weeks) | round(4) }}%">{{ week }}</span>{% endfor %}
</div>
</div>
{% for phase in phases %}
<div class="tl-row">
<div class="tl-label"><span class="tl-no">{{ "%02d" | format(loop.index) }}</span> {{ phase.name }}</div>
<div class="tl-track">
<span class="tl-bar" style="margin-left: {{ ((phase.start_week - 1) / total_weeks * 100) | round(4) }}%; width: {{ (phase.weeks / total_weeks * 100) | round(4) }}%"></span>
</div>
</div>
{% endfor %}
</section>
<h2 class="section-title">Scope and costs by phase</h2>
{% for phase in phases %}
{%- set phase_no = loop.index -%}
{%- set included = phase.tasks | rejectattr("optional") | list -%}
<article class="phase">
<header class="phase-head">
<span class="phase-no">{{ "%02d" | format(phase_no) }}</span>
<div class="phase-title">
<h3>{{ phase.name }}</h3>
<p>{{ phase.summary }}</p>
</div>
<div class="phase-meta">
Week{{ "s" if phase.weeks > 1 else "" }} {{ phase.start_week }}{% if phase.weeks > 1 %}–{{ phase.start_week + phase.weeks - 1 }}{% endif %}
<strong>{{ money(included | sum_by("amount")) }}</strong>
</div>
</header>
<table class="tasks">
<thead>
<tr>
<th class="ref" scope="col">Ref</th>
<th scope="col">Task</th>
<th class="num" scope="col">Days</th>
<th class="num" scope="col">Day rate</th>
<th class="num" scope="col">Amount</th>
</tr>
</thead>
<tbody>
{% for task in included %}
<tr>
<td class="ref">{{ phase_no }}.{{ loop.index }}</td>
<td>
<span class="task-name">{{ task.name }}</span>
{% if task.details %}<span class="task-details">{{ task.details }}</span>{% endif %}
</td>
<td class="num">{{ days(task.days) }}</td>
<td class="num">{{ money(task.day_rate) }}</td>
<td class="num">{{ money(task.amount) }}</td>
</tr>
{% endfor %}
<tr class="phase-total">
<td></td>
<td>{{ phase.name }} subtotal</td>
<td class="num">{{ days(included | sum_by("days")) }}</td>
<td></td>
<td class="num">{{ money(included | sum_by("amount")) }}</td>
</tr>
</tbody>
</table>
</article>
{% endfor %}
<section class="totals">
<table class="phase-summary">
<thead>
<tr><th scope="col">Phase</th><th class="num" scope="col">Days</th><th class="num" scope="col">Amount</th></tr>
</thead>
<tbody>
{% for phase in phases %}
{%- set included = phase.tasks | rejectattr("optional") | list -%}
<tr>
<td>{{ "%02d" | format(loop.index) }} · {{ phase.name }}</td>
<td class="num">{{ days(included | sum_by("days")) }}</td>
<td class="num">{{ money(included | sum_by("amount")) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<table class="grand">
<tr><th scope="row">Subtotal</th><td class="num">{{ money(subtotal) }}</td></tr>
<tr><th scope="row">VAT {{ quote.vat_rate | format_percent(locale) }}</th><td class="num">{{ money(vat) }}</td></tr>
<tr class="grand-total"><th scope="row">Total</th><td class="num">{{ money(total) }}</td></tr>
</table>
</section>
{% if optional_count > 0 %}
<section class="optional">
<h2 class="section-title">Optional extras</h2>
<p class="optional-intro">Not included in the total above. Any of these can be added when you accept the estimate.</p>
<table class="tasks">
<thead>
<tr>
<th class="tick" scope="col">Add</th>
<th scope="col">Extra</th>
<th scope="col">Phase</th>
<th class="num" scope="col">Days</th>
<th class="num" scope="col">Amount</th>
</tr>
</thead>
<tbody>
{% for phase in phases %}
{% for task in phase.tasks | selectattr("optional") %}
<tr>
<td class="tick"><span class="box" role="img" aria-label="Tick box"></span></td>
<td>
<span class="task-name">{{ task.name }}</span>
{% if task.details %}<span class="task-details">{{ task.details }}</span>{% endif %}
</td>
<td class="nowrap">{{ phase.name }}</td>
<td class="num">{{ days(task.days) }}</td>
<td class="num">{{ money(task.amount) }}</td>
</tr>
{% endfor %}
{% endfor %}
<tr class="phase-total">
<td></td>
<td colspan="3">All {{ optional_count }} extras, excl. VAT</td>
<td class="num">{{ money(optional_total) }}</td>
</tr>
</tbody>
</table>
</section>
{% endif %}
<section class="schedule">
<h2 class="section-title">Payment schedule</h2>
<table class="schedule-table">
<tbody>
{% for step in payment_schedule %}
<tr>
<td class="step-no">{{ loop.index }}</td>
<th scope="row">{{ step.milestone }}</th>
<td class="num">{{ step.share | format_percent(locale) }}</td>
<td class="num">{{ money((subtotal * step.share) | round(2)) }} <span class="muted">+ VAT</span></td>
</tr>
{% endfor %}
</tbody>
</table>
</section>
<section class="assumptions">
<h2 class="section-title">Assumptions</h2>
{{ assumptions | markdown }}
</section>
<section class="acceptance">
<h2 class="section-title">Acceptance</h2>
<p class="acceptance-intro">
To go ahead, sign below and return this page before {{ valid_until | format_date("long", locale) }}.
Signing accepts estimate {{ quote.number }} for {{ money(subtotal) }} excluding VAT, plus any optional extras you tick.
</p>
<div class="sign-grid">
<div class="sign-box">
<p class="label">For {{ studio.name }}</p>
<div class="field"><span>Signature</span></div>
<div class="field filled"><span>Name</span>{{ acceptance.studio_signatory }}</div>
<div class="field filled"><span>Role</span>{{ acceptance.studio_signatory_role }}</div>
<div class="field"><span>Date</span></div>
</div>
<div class="sign-box">
<p class="label">Accepted for {{ client.name }}</p>
<div class="field"><span>Signature</span></div>
<div class="field"><span>Name</span></div>
<div class="field"><span>Role</span></div>
<div class="field"><span>Date</span></div>
<div class="field"><span>PO number</span></div>
</div>
</div>
</section><title>Project estimate {{ quote.number }} – {{ quote.title }}</title>
<style>
:root {
--ink: #262220;
--muted: #776e68;
--rule: #e5dcd2;
--paper: #f8f3ed;
--accent: #b4532a;
--accent-deep: #8c3d1c;
--accent-soft: #f6e4d9;
}
* { box-sizing: border-box; }
html { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
body {
margin: 0;
color: var(--ink);
font-family: "Open Sans", "Liberation Sans", Arial, sans-serif;
font-size: 9pt;
line-height: 1.45;
}
p { margin: 0; }
h1, h2, h3 { font-family: "Montserrat", "Liberation Sans", Arial, sans-serif; }
.num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.nowrap { white-space: nowrap; }
.muted { color: var(--muted); }
.strong { font-weight: 700; }
.label { margin-bottom: 1mm; font-size: 7pt; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; color: var(--muted); }
.section-title {
margin: 9mm 0 3mm; font-size: 8pt; font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase; color: var(--accent);
break-after: avoid; page-break-after: avoid;
}
/* Letterhead and cover */
.letterhead { display: flex; justify-content: space-between; align-items: center; padding-bottom: 3mm; border-bottom: 0.3mm solid var(--rule); }
.brand { display: flex; align-items: center; gap: 2.5mm; }
.leaf { width: 8mm; height: 8mm; }
.brand-name { font-family: "Montserrat", "Liberation Sans", Arial, sans-serif; font-size: 11pt; font-weight: 700; letter-spacing: 0.02em; }
.contact { font-size: 7.5pt; color: var(--muted); }
.cover { position: relative; margin-top: 9mm; padding-left: 6mm; border-left: 1.6mm solid var(--accent); }
.eyebrow { font-size: 8pt; font-weight: 600; letter-spacing: 0.12em; text-transform: uppercase; color: var(--accent); }
.cover h1 { margin: 1.5mm 0 3mm; font-size: 24pt; font-weight: 700; line-height: 1.12; letter-spacing: -0.01em; }
.lede { max-width: 150mm; font-size: 10.5pt; line-height: 1.5; color: #4a433f; }
.cover-grid { display: grid; grid-template-columns: 1fr 1fr 0.8fr; gap: 7mm; margin-top: 7mm; font-size: 8.5pt; }
.cover-grid p + p { margin-top: 0.6mm; }
/* At a glance */
.glance { display: grid; grid-template-columns: 1.4fr 1fr 1fr 1fr; margin-top: 8mm; border-radius: 2mm; background: var(--paper); }
.stat { padding: 4mm 5mm; }
.stat + .stat { border-left: 0.3mm solid var(--rule); }
.stat-k { display: block; font-size: 7pt; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; color: var(--muted); }
.stat-v { display: block; margin-top: 0.8mm; font-family: "Montserrat", "Liberation Sans", Arial, sans-serif; font-size: 14pt; font-weight: 700; }
.stat-main { background: var(--accent); border-radius: 2mm 0 0 2mm; }
.stat-main .stat-k { color: #f6d9c8; }
.stat-main .stat-v { color: #ffffff; }
/* Timeline */
.timeline { break-inside: avoid; page-break-inside: avoid; }
.tl-row { display: flex; align-items: center; min-height: 6.5mm; }
.tl-label { flex: none; width: 46mm; font-size: 8.5pt; font-weight: 600; }
.tl-no { display: inline-block; width: 6mm; color: var(--accent); font-variant-numeric: tabular-nums; }
.tl-track { flex: 1; display: flex; position: relative; height: 6.5mm; align-items: center; border-bottom: 0.2mm dotted var(--rule); }
.tl-scale .tl-track { border-bottom: 0.3mm solid var(--rule); }
.tl-scale span { display: inline-block; text-align: center; font-size: 7pt; color: var(--muted); }
.tl-scale .tl-label { font-size: 7pt; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; color: var(--muted); }
.tl-bar { display: block; height: 3.2mm; border-radius: 1.6mm; background: var(--accent); }
/* Phases */
.phase { margin-bottom: 6mm; break-inside: avoid; page-break-inside: avoid; }
.phase-head { display: flex; align-items: flex-start; gap: 4mm; padding: 3.5mm 0 2.5mm; border-top: 0.6mm solid var(--ink); }
.phase-no {
flex: none; width: 10mm; height: 10mm; border-radius: 50%; background: var(--accent-soft); color: var(--accent-deep);
font-family: "Montserrat", "Liberation Sans", Arial, sans-serif; font-weight: 700; font-size: 10pt; line-height: 10mm; text-align: center;
}
.phase-title { flex: 1; }
.phase-title h3 { margin: 0; font-size: 12pt; font-weight: 700; }
.phase-title p { color: var(--muted); font-size: 8.5pt; }
.phase-meta { flex: none; text-align: right; font-size: 8pt; color: var(--muted); }
.phase-meta strong { display: block; font-family: "Montserrat", "Liberation Sans", Arial, sans-serif; font-size: 11pt; color: var(--ink); }
table { width: 100%; border-collapse: collapse; }
thead { display: table-header-group; }
th {
padding: 1.6mm 2mm; border-bottom: 0.3mm solid var(--rule); text-align: left;
font-size: 7pt; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted);
}
th.num { text-align: right; }
.tasks td { padding: 2.2mm 2mm; border-bottom: 0.2mm solid var(--rule); vertical-align: top; }
.tasks tr { break-inside: avoid; page-break-inside: avoid; }
.tasks .ref { width: 11mm; color: var(--accent); font-weight: 600; font-variant-numeric: tabular-nums; }
.task-name { font-weight: 600; }
.task-details { display: block; font-size: 8pt; color: var(--muted); }
.phase-total td { border-bottom: none; font-weight: 700; background: var(--paper); }
/* Totals */
.totals { display: grid; grid-template-columns: 1fr 70mm; gap: 10mm; align-items: end; margin-top: 8mm; break-inside: avoid; page-break-inside: avoid; }
.phase-summary td { padding: 1.8mm 2mm; border-bottom: 0.2mm solid var(--rule); }
.grand td, .grand th { padding: 1.8mm 0; }
.grand th { border-bottom: none; color: var(--muted); font-size: inherit; font-weight: inherit; letter-spacing: normal; text-transform: none; }
.grand-total td, .grand-total th {
padding-top: 3mm; border-top: 0.6mm solid var(--ink); font-family: "Montserrat", "Liberation Sans", Arial, sans-serif;
font-size: 14pt; font-weight: 700; color: var(--ink) !important;
}
/* Optional extras */
.optional { break-inside: avoid; page-break-inside: avoid; }
.optional-intro { margin: -1mm 0 2.5mm; color: var(--muted); }
.tasks .tick { width: 10mm; text-align: center; }
.box { display: inline-block; width: 3.6mm; height: 3.6mm; border: 0.35mm solid var(--accent); border-radius: 0.6mm; vertical-align: middle; }
/* Schedule, assumptions and acceptance */
.schedule { break-inside: avoid; page-break-inside: avoid; }
.schedule-table td, .schedule-table th { padding: 2mm; border-bottom: 0.2mm solid var(--rule); }
.schedule-table th { color: inherit; font-size: inherit; font-weight: inherit; letter-spacing: normal; text-transform: none; }
.step-no {
width: 9mm; font-family: "Montserrat", "Liberation Sans", Arial, sans-serif; font-weight: 700; color: var(--accent);
}
.assumptions ul { margin: 0; padding-left: 5mm; }
.assumptions li { margin-bottom: 1.2mm; padding-left: 1mm; }
.assumptions li::marker { color: var(--accent); }
.acceptance { margin-top: 4mm; break-inside: avoid; page-break-inside: avoid; }
.acceptance-intro { margin-bottom: 4mm; max-width: 160mm; }
.sign-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8mm; }
.sign-box { padding: 4mm 5mm 2mm; border: 0.3mm solid var(--rule); border-radius: 2mm; }
.sign-box .label { color: var(--accent); margin-bottom: 2mm; }
.field { display: flex; align-items: flex-end; min-height: 8.5mm; border-bottom: 0.3mm solid #bdb3aa; padding-bottom: 1mm; font-size: 8.5pt; }
.field span { flex: none; width: 21mm; font-size: 7pt; letter-spacing: 0.06em; text-transform: uppercase; color: var(--muted); }
.field:first-of-type { min-height: 13mm; }
</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": {
"studio": {
"name": "Copperbeck Studio Ltd",
"short_name": "Copperbeck Studio",
"address": "Unit 5, Gasholder Yard\n41 Ropemaker Lane\nLondon N1 7XY",
"email": "hello@copperbeck.example.com",
"phone": "+44 20 7946 0321",
"website": "copperbeck.example.com",
"company_number": "12345678",
"vat_number": "GB 123 4567 89"
},
"client": {
"name": "Hollin Vale Museum Trust",
"contact": "Dr Amara Lindqvist, Director of Audiences",
"address": "The Old Mill, Mill Lane\nHollin Vale\nHebden Bridge HX7 8XY"
},
"quote": {
"number": "CBS-E-2026-041",
"title": "Collections website and online booking",
"summary": "A new public website that brings the museum'\''s collection online, lets visitors book timed entry and events, and gives staff a content system they can run without developer help.",
"issue_date": "2026-09-16",
"valid_for_days": 30,
"currency": "GBP",
"locale": "en-GB",
"vat_rate": 0.2,
"prepared_by": "Rhys Abernethy",
"prepared_by_role": "Client Partner"
},
"phases": [
{
"name": "Discovery",
"summary": "Understand visitors, collections data and the current ticketing set-up.",
"start_week": 1,
"weeks": 2,
"tasks": [
{
"name": "Stakeholder workshops",
"details": "Two half-day sessions with curators, learning and visitor services teams",
"days": 2,
"day_rate": 650,
"amount": 1300
},
{
"name": "Visitor research",
"details": "Eight remote interviews and an on-site survey over two weekends",
"days": 3,
"day_rate": 650,
"amount": 1950
}
]
},
{
"name": "Design",
"summary": "Shape the structure, look and feel, and test it with real visitors.",
"start_week": 3,
"weeks": 3,
"tasks": [
{
"name": "Information architecture",
"details": "Site map, navigation model and content types",
"days": 2,
"day_rate": 650,
"amount": 1300
},
{
"name": "Visual design system",
"details": "Typography, colour, components and accessibility annotations",
"days": 4,
"day_rate": 650,
"amount": 2600
}
]
}
],
"payment_schedule": [
{
"milestone": "On acceptance",
"share": 0.3
},
{
"milestone": "Design sign-off",
"share": 0.4
}
],
"assumptions": "- The museum provides a single point of contact who can gather feedback and approve each phase within **five working days**.\n- Collections data can be exported nightly in CSV or JSON format; no changes to the collections database itself are included.\n- The booking flow uses the museum'\''s existing payment provider account.\n- Content is supplied or rewritten by museum staff; we provide templates, guidance and migration scripts.\n- Hosting is quoted separately and is not part of this estimate.\n- Prices exclude VAT. Travel within Greater London is included; other travel is charged at cost with prior approval.",
"acceptance": {
"studio_signatory": "Rhys Abernethy",
"studio_signatory_role": "Client Partner"
}
},
"output": {
"format": "pdf",
"filename": "quote-project-estimate.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": {
"studio": {
"name": "Copperbeck Studio Ltd",
"short_name": "Copperbeck Studio",
"address": "Unit 5, Gasholder Yard\n41 Ropemaker Lane\nLondon N1 7XY",
"email": "hello@copperbeck.example.com",
"phone": "+44 20 7946 0321",
"website": "copperbeck.example.com",
"company_number": "12345678",
"vat_number": "GB 123 4567 89"
},
"client": {
"name": "Hollin Vale Museum Trust",
"contact": "Dr Amara Lindqvist, Director of Audiences",
"address": "The Old Mill, Mill Lane\nHollin Vale\nHebden Bridge HX7 8XY"
},
"quote": {
"number": "CBS-E-2026-041",
"title": "Collections website and online booking",
"summary": "A new public website that brings the museum's collection online, lets visitors book timed entry and events, and gives staff a content system they can run without developer help.",
"issue_date": "2026-09-16",
"valid_for_days": 30,
"currency": "GBP",
"locale": "en-GB",
"vat_rate": 0.2,
"prepared_by": "Rhys Abernethy",
"prepared_by_role": "Client Partner"
},
"phases": [
{
"name": "Discovery",
"summary": "Understand visitors, collections data and the current ticketing set-up.",
"start_week": 1,
"weeks": 2,
"tasks": [
{
"name": "Stakeholder workshops",
"details": "Two half-day sessions with curators, learning and visitor services teams",
"days": 2,
"day_rate": 650,
"amount": 1300
},
{
"name": "Visitor research",
"details": "Eight remote interviews and an on-site survey over two weekends",
"days": 3,
"day_rate": 650,
"amount": 1950
}
]
},
{
"name": "Design",
"summary": "Shape the structure, look and feel, and test it with real visitors.",
"start_week": 3,
"weeks": 3,
"tasks": [
{
"name": "Information architecture",
"details": "Site map, navigation model and content types",
"days": 2,
"day_rate": 650,
"amount": 1300
},
{
"name": "Visual design system",
"details": "Typography, colour, components and accessibility annotations",
"days": 4,
"day_rate": 650,
"amount": 2600
}
]
}
],
"payment_schedule": [
{
"milestone": "On acceptance",
"share": 0.3
},
{
"milestone": "Design sign-off",
"share": 0.4
}
],
"assumptions": "- The museum provides a single point of contact who can gather feedback and approve each phase within **five working days**.\n- Collections data can be exported nightly in CSV or JSON format; no changes to the collections database itself are included.\n- The booking flow uses the museum's existing payment provider account.\n- Content is supplied or rewritten by museum staff; we provide templates, guidance and migration scripts.\n- Hosting is quoted separately and is not part of this estimate.\n- Prices exclude VAT. Travel within Greater London is included; other travel is charged at cost with prior approval.",
"acceptance": {
"studio_signatory": "Rhys Abernethy",
"studio_signatory_role": "Client Partner"
}
},
"output": {
"format": "pdf",
"filename": "quote-project-estimate.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": {
"studio": {
"name": "Copperbeck Studio Ltd",
"short_name": "Copperbeck Studio",
"address": "Unit 5, Gasholder Yard\n41 Ropemaker Lane\nLondon N1 7XY",
"email": "hello@copperbeck.example.com",
"phone": "+44 20 7946 0321",
"website": "copperbeck.example.com",
"company_number": "12345678",
"vat_number": "GB 123 4567 89",
},
"client": {
"name": "Hollin Vale Museum Trust",
"contact": "Dr Amara Lindqvist, Director of Audiences",
"address": "The Old Mill, Mill Lane\nHollin Vale\nHebden Bridge HX7 8XY",
},
"quote": {
"number": "CBS-E-2026-041",
"title": "Collections website and online booking",
"summary": "A new public website that brings the museum's collection online, lets visitors book timed entry and events, and gives staff a content system they can run without developer help.",
"issue_date": "2026-09-16",
"valid_for_days": 30,
"currency": "GBP",
"locale": "en-GB",
"vat_rate": 0.2,
"prepared_by": "Rhys Abernethy",
"prepared_by_role": "Client Partner",
},
"phases": [
{
"name": "Discovery",
"summary": "Understand visitors, collections data and the current ticketing set-up.",
"start_week": 1,
"weeks": 2,
"tasks": [
{
"name": "Stakeholder workshops",
"details": "Two half-day sessions with curators, learning and visitor services teams",
"days": 2,
"day_rate": 650,
"amount": 1300,
},
{
"name": "Visitor research",
"details": "Eight remote interviews and an on-site survey over two weekends",
"days": 3,
"day_rate": 650,
"amount": 1950,
},
],
},
{
"name": "Design",
"summary": "Shape the structure, look and feel, and test it with real visitors.",
"start_week": 3,
"weeks": 3,
"tasks": [
{
"name": "Information architecture",
"details": "Site map, navigation model and content types",
"days": 2,
"day_rate": 650,
"amount": 1300,
},
{
"name": "Visual design system",
"details": "Typography, colour, components and accessibility annotations",
"days": 4,
"day_rate": 650,
"amount": 2600,
},
],
},
],
"payment_schedule": [
{
"milestone": "On acceptance",
"share": 0.3,
},
{
"milestone": "Design sign-off",
"share": 0.4,
},
],
"assumptions": "- The museum provides a single point of contact who can gather feedback and approve each phase within **five working days**.\n- Collections data can be exported nightly in CSV or JSON format; no changes to the collections database itself are included.\n- The booking flow uses the museum's existing payment provider account.\n- Content is supplied or rewritten by museum staff; we provide templates, guidance and migration scripts.\n- Hosting is quoted separately and is not part of this estimate.\n- Prices exclude VAT. Travel within Greater London is included; other travel is charged at cost with prior approval.",
"acceptance": {
"studio_signatory": "Rhys Abernethy",
"studio_signatory_role": "Client Partner",
},
},
"output": {
"format": "pdf",
"filename": "quote-project-estimate.pdf",
},
},
timeout=90,
)
response.raise_for_status()
print(response.json())More quotes & estimates templates
- 8.5 × 11 in
Product bundle quote
LetterA US Letter sales quote for hardware bundles with SKUs, services and volume discount lines shown as negative amounts. Totals include a shipping estimate and optional tax exemption, and a QR code links to the online acceptance page.
- 210 × 297 mm
Project proposal
A4An A4 project proposal that is easy to edit visually: a letterhead header, key facts, a phase table that repeats per phase, an optional early-signature discount, terms, two signature lines and page numbers.