Skip to content

210 × 297 mm

Invoices

Hourly consulting invoice with timesheet

An A4 invoice for European consultancies that bill by the hour: EN 16931 invoice lines per project and role with budget used per project, a timesheet grouped by project with subtotals, 21% VAT, and the invoice embedded as ZUGFeRD / Factur-X EN 16931 XML.

A4 portraitLocale en-GBHTML + JinjaHeader & footer

Opens Dynamic Document API with this template preloaded in a free workspace.

Template language features

  • Loops
  • Conditionals
  • Macros
  • Filters
  • Grouping
  • Currency formatting
  • Date formatting
  • Number formatting
  • 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: 5 top-level fields, 84 paths in total. Send the same shape as data.

PathTypeExample
invoiceobject
invoice.numberstring"LV-26-0311"
invoice.issue_datedate"2026-09-15"
invoice.due_datedate"2026-09-30"
invoice.currencystring"EUR"
invoice.order_referencestring"SWO-PO-7719"
invoice.contract_referencestring"SOW-2026-03"
invoice.payment_termsstring"Net 15 days"
invoice.invoice_periodobject
invoice.invoice_period.startdate"2026-09-01"
invoice.invoice_period.enddate"2026-09-14"
invoice.deliveryobject
invoice.delivery.datedate"2026-09-14"
invoice.sellerobject
invoice.seller.namestring"Larkspur & Vane Consulting B.V."
invoice.seller.vat_idstring"NL123456789B01"
invoice.seller.legal_registrationobject
invoice.seller.legal_registration.idstring"12345678"
invoice.seller.legal_registration.schemestring"0106"
invoice.seller.electronic_addressobject
invoice.seller.electronic_address.idstring"billing@larkspurvane.example.com"
invoice.seller.electronic_address.schemestring"EM"
invoice.seller.addressobject
invoice.seller.address.line1string"Suite 4.02, Aldermeerkade 118"
invoice.seller.address.citystring"Rotterdam"
invoice.seller.address.postcodestring"3011 AB"
invoice.seller.address.countrystring"NL"
invoice.seller.contactobject
invoice.seller.contact.namestring"Billing"
invoice.seller.contact.emailstring"billing@larkspurvane.example.com"
invoice.buyerobject
invoice.buyer.namestring"Sablewood Outfitters B.V."
invoice.buyer.vat_idstring"NL987654321B01"
invoice.buyer.electronic_addressobject
invoice.buyer.electronic_address.idstring"ap@sablewood.example.com"
invoice.buyer.electronic_address.schemestring"EM"
invoice.buyer.addressobject
invoice.buyer.address.line1string"Graniethaven 23"
invoice.buyer.address.citystring"Tilburg"
invoice.buyer.address.postcodestring"5048 AB"
invoice.buyer.address.countrystring"NL"
invoice.buyer.contactobject
invoice.buyer.contact.namestring"Dana Verhoeven, Head of Operations"
invoice.paymentobject
invoice.payment.means_codestring"58"
invoice.payment.remittance_informationstring"LV-26-0311"
invoice.payment.credit_transfersarray<object> · 1
invoice.payment.credit_transfers[].account_idstring"NL74EXMP0123456789"
invoice.payment.credit_transfers[].account_namestring"Larkspur & Vane Consulting B.V."
invoice.payment.credit_transfers[].bicstring"EXMPNL2A"
invoice.linesarray<object> · 8
invoice.lines[].quantitystring"4.5"
invoice.lines[].unitstring"HUR"
invoice.lines[].priceobject
invoice.lines[].price.netstring"210.00"
invoice.lines[].taxobject
invoice.lines[].tax.categorystring"S"
invoice.lines[].tax.ratestring"21"
invoice.lines[].itemobject
invoice.lines[].item.namestring"Demand forecasting model – Principal"
invoice.lines[].item.attributesarray<object> · 2
invoice.lines[].item.attributes[].namestring"Project"
invoice.lines[].item.attributes[].valuestring"DFM"
invoice.notesarray<object> · 2
invoice.notes[].textstring"Time is recorded in 15-minute increments and b…"
taglinestring"Operations and analytics advisory"
websitestring"larkspurvane.example.com"
bank_namestring"Kademeer Bank"
timesheetobject
timesheet.engagementstring"Supply chain analytics programme"
timesheet.projectsarray<object> · 3
timesheet.projects[].codestring"DFM"
timesheet.projects[].namestring"Demand forecasting model"
timesheet.projects[].budget_hoursnumber120
timesheet.projects[].prior_hoursnumber64.5
timesheet.entriesarray<object> · 13
timesheet.entries[].datedate"2026-09-01"
timesheet.entries[].projectstring"DFM"
timesheet.entries[].consultantstring"Maren Okafor"
timesheet.entries[].rolestring"Principal"
timesheet.entries[].descriptionstring"Kick-off workshop with merchandising and finan…"
timesheet.entries[].hoursnumber3
timesheet.entries[].ratenumber210
timesheet.entries[].amountnumber630

Sample data

Fictional records used for previews. Replace them with your own data when you call the API.

data.json
{
  "invoice": {
    "number": "LV-26-0311",
    "issue_date": "2026-09-15",
    "due_date": "2026-09-30",
    "currency": "EUR",
    "order_reference": "SWO-PO-7719",
    "contract_reference": "SOW-2026-03",
    "payment_terms": "Net 15 days",
    "invoice_period": {
      "start": "2026-09-01",
      "end": "2026-09-14"
    },
    "delivery": {
      "date": "2026-09-14"
    },
    "seller": {
      "name": "Larkspur & Vane Consulting B.V.",
      "vat_id": "NL123456789B01",
      "legal_registration": {
        "id": "12345678",
        "scheme": "0106"
      },
      "electronic_address": {
        "id": "billing@larkspurvane.example.com",
        "scheme": "EM"
      },
      "address": {
        "line1": "Suite 4.02, Aldermeerkade 118",
        "city": "Rotterdam",
        "postcode": "3011 AB",
        "country": "NL"
      },
      "contact": {
        "name": "Billing",
        "email": "billing@larkspurvane.example.com"
      }
    },
    "buyer": {
      "name": "Sablewood Outfitters B.V.",
      "vat_id": "NL987654321B01",
      "electronic_address": {
        "id": "ap@sablewood.example.com",
        "scheme": "EM"
      },
      "address": {
        "line1": "Graniethaven 23",
        "city": "Tilburg",
        "postcode": "5048 AB",
        "country": "NL"
      },
      "contact": {
        "name": "Dana Verhoeven, Head of Operations"
      }
    },
    "payment": {
      "means_code": "58",
      "remittance_information": "LV-26-0311",
      "credit_transfers": [
        {
          "account_id": "NL74EXMP0123456789",
          "account_name": "Larkspur & Vane Consulting B.V.",
          "bic": "EXMPNL2A"
        }
      ]
    },
    "lines": [
      {
        "quantity": "4.5",
        "unit": "HUR",
        "price": {
          "net": "210.00"
        },
        "tax": {
          "category": "S",
          "rate": "21"
        },
        "item": {
          "name": "Demand forecasting model – Principal",
          "attributes": [
            {
              "name": "Project",
              "value": "DFM"
            },
            {
              "name": "Role",
              "value": "Principal"
            }
          ]
        }
      },
      {
        "quantity": "10",
        "unit": "HUR",
        "price": {
          "net": "165.00"
        },
        "tax": {
          "category": "S",
          "rate": "21"
        },
        "item": {
          "name": "Demand forecasting model – Senior consultant",
          "attributes": [
            {
              "name": "Project",
              "value": "DFM"
            },
            {
              "name": "Role",
              "value": "Senior consultant"
            }
          ]
        }
      },
      {
        "quantity": "6.5",
        "unit": "HUR",
        "price": {
          "net": "120.00"
        },
        "tax": {
          "category": "S",
          "rate": "21"
        },
        "item": {
          "name": "Demand forecasting model – Data analyst",
          "attributes": [
            {
              "name": "Project",
              "value": "DFM"
            },
            {
              "name": "Role",
              "value": "Data analyst"
            }
          ]
        }
      },
      {
        "quantity": "6.25",
        "unit": "HUR",
        "price": {
          "net": "165.00"
        },
        "tax": {
          "category": "S",
          "rate": "21"
        },
        "item": {
          "name": "Point-of-sale data feed – Senior consultant",
          "attributes": [
            {
              "name": "Project",
              "value": "POS"
            },
            {
              "name": "Role",
              "value": "Senior consultant"
            }
          ]
        }
      },
      {
        "quantity": "9.5",
        "unit": "HUR",
        "price": {
          "net": "120.00"
        },
        "tax": {
          "category": "S",
          "rate": "21"
        },
        "item": {
          "name": "Point-of-sale data feed – Data analyst",
          "attributes": [
            {
              "name": "Project",
              "value": "POS"
            },
            {
              "name": "Role",
              "value": "Data analyst"
            }
          ]
        }
      },
      {
        "quantity": "6.5",
        "unit": "HUR",
        "price": {
          "net": "210.00"
        },
        "tax": {
          "category": "S",
          "rate": "21"
        },
        "item": {
          "name": "Warehouse slotting review – Principal",
          "attributes": [
            {
              "name": "Project",
              "value": "WSR"
            },
            {
              "name": "Role",
              "value": "Principal"
            }
          ]
        }
      },
      {
        "quantity": "3.75",
        "unit": "HUR",
        "price": {
          "net": "165.00"
        },
        "tax": {
          "category": "S",
          "rate": "21"
        },
        "item": {
          "name": "Warehouse slotting review – Senior consultant",
          "attributes": [
            {
              "name": "Project",
              "value": "WSR"
            },
            {
              "name": "Role",
              "value": "Senior consultant"
            }
          ]
        }
      },
      {
        "quantity": "6",
        "unit": "HUR",
        "price": {
          "net": "120.00"
        },
        "tax": {
          "category": "S",
          "rate": "21"
        },
        "item": {
          "name": "Warehouse slotting review – Data analyst",
          "attributes": [
            {
              "name": "Project",
              "value": "WSR"
            },
            {
              "name": "Role",
              "value": "Data analyst"
            }
          ]
        }
      }
    ],
    "notes": [
      {
        "text": "Time is recorded in 15-minute increments and billed at the rates agreed in SOW-2026-03. Travel to the Tilburg distribution centre was pre-approved and will be invoiced separately at cost."
      },
      {
        "text": "Questions about an entry? Email billing@larkspurvane.example.com."
      }
    ]
  },
  "tagline": "Operations and analytics advisory",
  "website": "larkspurvane.example.com",
  "bank_name": "Kademeer Bank",
  "timesheet": {
    "engagement": "Supply chain analytics programme",
    "projects": [
      {
        "code": "DFM",
        "name": "Demand forecasting model",
        "budget_hours": 120,
        "prior_hours": 64.5
      },
      {
        "code": "POS",
        "name": "Point-of-sale data feed",
        "budget_hours": 60,
        "prior_hours": 22.25
      },
      {
        "code": "WSR",
        "name": "Warehouse slotting review",
        "budget_hours": 40,
        "prior_hours": 0
      }
    ],
    "entries": [
      {
        "date": "2026-09-01",
        "project": "DFM",
        "consultant": "Maren Okafor",
        "role": "Principal",
        "description": "Kick-off workshop with merchandising and finance; agree forecast horizons and accuracy targets",
        "hours": 3.0,
        "rate": 210.0,
        "amount": 630.0
      },
      {
        "date": "2026-09-02",
        "project": "DFM",
        "consultant": "Ines Castellano",
        "role": "Data analyst",
        "description": "Profile three years of weekly sales history; flag gaps, outliers and stock-outs",
        "hours": 6.5,
        "rate": 120.0,
        "amount": 780.0
      },
      {
        "date": "2026-09-03",
        "project": "DFM",
        "consultant": "Tobias Linde",
        "role": "Senior consultant",
        "description": "Build baseline seasonal model and holdout test set",
        "hours": 5.25,
        "rate": 165.0,
        "amount": 866.25
      },
      {
        "date": "2026-09-08",
        "project": "DFM",
        "consultant": "Tobias Linde",
        "role": "Senior consultant",
        "description": "Add promotion and weather features; compare forecast error by region",
        "hours": 4.75,
        "rate": 165.0,
        "amount": 783.75
      },
      {
        "date": "2026-09-10",
        "project": "DFM",
        "consultant": "Maren Okafor",
        "role": "Principal",
        "description": "Review interim results with the finance lead and agree the next iteration",
        "hours": 1.5,
        "rate": 210.0,
        "amount": 315.0
      },
      {
        "date": "2026-09-02",
        "project": "POS",
        "consultant": "Tobias Linde",
        "role": "Senior consultant",
        "description": "Map store transaction export fields to the warehouse schema",
        "hours": 3.5,
        "rate": 165.0,
        "amount": 577.5
      },
      {
        "date": "2026-09-04",
        "project": "POS",
        "consultant": "Ines Castellano",
        "role": "Data analyst",
        "description": "Write validation checks for nightly file loads",
        "hours": 4.0,
        "rate": 120.0,
        "amount": 480.0
      },
      {
        "date": "2026-09-09",
        "project": "POS",
        "consultant": "Tobias Linde",
        "role": "Senior consultant",
        "description": "Fix duplicate-receipt handling in the load job; add regression tests",
        "hours": 2.75,
        "rate": 165.0,
        "amount": 453.75
      },
      {
        "date": "2026-09-11",
        "project": "POS",
        "consultant": "Ines Castellano",
        "role": "Data analyst",
        "description": "Backfill August data and reconcile totals with store reports",
        "hours": 5.5,
        "rate": 120.0,
        "amount": 660.0
      },
      {
        "date": "2026-09-08",
        "project": "WSR",
        "consultant": "Maren Okafor",
        "role": "Principal",
        "description": "Site walk-through at the Tilburg distribution centre with shift leads",
        "hours": 4.0,
        "rate": 210.0,
        "amount": 840.0
      },
      {
        "date": "2026-09-09",
        "project": "WSR",
        "consultant": "Ines Castellano",
        "role": "Data analyst",
        "description": "Pick-frequency analysis by SKU velocity band",
        "hours": 6.0,
        "rate": 120.0,
        "amount": 720.0
      },
      {
        "date": "2026-09-11",
        "project": "WSR",
        "consultant": "Maren Okafor",
        "role": "Principal",
        "description": "Draft slotting recommendations and labour savings estimate",
        "hours": 2.5,
        "rate": 210.0,
        "amount": 525.0
      },
      {
        "date": "2026-09-14",
        "project": "WSR",
        "consultant": "Tobias Linde",
        "role": "Senior consultant",
        "description": "Travel-path simulation for the proposed pick-face layout",
        "hours": 3.75,
        "rate": 165.0,
        "amount": 618.75
      }
    ]
  }
}

Source

The body markup with Jinja, the stylesheet placed in <head> and the header and footer settings.

{#- Hourly consulting invoice rendered from the EN 16931 invoice model (`invoice`, proto/einvoice/invoice.schema.json).
    Invoice lines are billed per project and consultant role (hours × the role's rate), tagged with the item
    attributes "Project" (code) and "Role"; the detailed time entries and project budgets stay outside the model in
    `timesheet` and back up those lines (their hours per project and role add up to the line quantities).
    With `output.einvoice.invoice_path = "invoice"` (this template's default) the renderer embeds the invoice as
    Factur-X / ZUGFeRD XML; the platform computes the amounts as `render.invoice` for every render of it (with or
    without the XML, proto/README.md §20.7), so the page prints exactly the amounts of the XML, and
    `render.einvoice` only tells whether the XML is embedded. Without either (e.g. an HTML render of the file) the
    template applies the same rules itself. -#}
{%- set inv = invoice -%}
{%- set cur = inv.currency -%}
{%- set loc = "en-GB" -%}
{%- set embedded = render.einvoice if (render is defined and render.einvoice is defined and render.einvoice) else none -%}
{%- set computed = render.invoice if (render is defined and render.invoice is defined and render.invoice) else embedded -%}
{%- set countries = {"NL": "Netherlands", "BE": "Belgium", "DE": "Germany", "FR": "France", "LU": "Luxembourg", "AT": "Austria", "IE": "Ireland", "ES": "Spain", "IT": "Italy", "DK": "Denmark"} -%}
{%- set profiles = {"MINIMUM": "Factur-X MINIMUM", "BASIC_WL": "Factur-X BASIC WL", "BASIC": "ZUGFeRD / Factur-X BASIC", "EN16931": "ZUGFeRD / Factur-X EN 16931", "EXTENDED": "ZUGFeRD / Factur-X EXTENDED", "XRECHNUNG": "XRechnung"} -%}
{%- set exemptions = {"Z": "zero rated", "E": "exempt", "AE": "reverse charge", "K": "intra-community supply", "G": "export", "O": "outside the scope of VAT"} -%}


{#- Line net amounts and VAT groups (EN 16931: line net = quantity × price ÷ base quantity − allowances + charges) -#}
{%- set ns = namespace(rows=[], nets=[], line_total=0.0, allowance_total=0.0, charge_total=0.0) -%}
{%- for line in inv.lines -%}
  {%- set base = (line.price.base_quantity | default(1)) | float -%}
  {%- set lns = namespace(net=((line.quantity | float) * (line.price.net | float) / base) | round(2)) -%}
  {%- for a in line.allowances | default([]) -%}{%- set lns.net = lns.net - (a.amount | float) -%}{%- endfor -%}
  {%- for c in line.charges | default([]) -%}{%- set lns.net = lns.net + (c.amount | float) -%}{%- endfor -%}
  {%- set net = (computed.lines[loop.index0].net_amount | float) if computed else (lns.net | round(2)) -%}
  {%- set rate = (line.tax.rate | default(0)) | float -%}
  {%- set ns.nets = ns.nets + [net] -%}
  {%- set ns.rows = ns.rows + [{"key": line.tax.category ~ "|" ~ rate, "category": line.tax.category, "rate": rate, "net": net}] -%}
  {%- set ns.line_total = ns.line_total + net -%}
{%- endfor -%}
{%- for a in inv.allowances | default([]) -%}
  {%- set rate = (a.tax_rate | default(0)) | float -%}
  {%- set ns.rows = ns.rows + [{"key": a.tax_category ~ "|" ~ rate, "category": a.tax_category, "rate": rate, "net": 0 - (a.amount | float)}] -%}
  {%- set ns.allowance_total = ns.allowance_total + (a.amount | float) -%}
{%- endfor -%}
{%- for c in inv.charges | default([]) -%}
  {%- set rate = (c.tax_rate | default(0)) | float -%}
  {%- set ns.rows = ns.rows + [{"key": c.tax_category ~ "|" ~ rate, "category": c.tax_category, "rate": rate, "net": c.amount | float}] -%}
  {%- set ns.charge_total = ns.charge_total + (c.amount | float) -%}
{%- endfor -%}
{%- if computed -%}
  {%- set breakdown = computed.vat_breakdown -%}
  {%- set totals = computed.totals -%}
{%- else -%}
  {%- set bns = namespace(items=[], tax=0.0) -%}
  {%- for group in ns.rows | groupby("key") -%}
    {%- set first = group.list | first -%}
    {%- set basis = (group.list | sum_by("net")) | round(2) -%}
    {%- set tax = (basis * first.rate / 100) | round(2) -%}
    {%- set bns.items = bns.items + [{"category": first.category, "rate": first.rate, "basis": basis, "tax": tax}] -%}
    {%- set bns.tax = bns.tax + tax -%}
  {%- endfor -%}
  {%- set tax_basis = (ns.line_total - ns.allowance_total + ns.charge_total) | round(2) -%}
  {%- set grand = (tax_basis + bns.tax) | round(2) -%}
  {%- set prepaid = (inv.prepaid_amount | default(0)) | float -%}
  {%- set rounding = (inv.rounding_amount | default(0)) | float -%}
  {%- set breakdown = bns.items -%}
  {%- set totals = {"line_total": ns.line_total | round(2), "allowance_total": ns.allowance_total | round(2), "charge_total": ns.charge_total | round(2), "tax_basis_total": tax_basis, "tax_total": bns.tax | round(2), "grand_total": grand, "prepaid": prepaid, "rounding": rounding, "due_payable": (grand - prepaid + rounding) | round(2)} -%}
{%- endif -%}

{#- One row per invoice line with its project code and role (item attributes), for grouping by project -#}
{%- set pns = namespace(rows=[]) -%}
{%- for line in inv.lines -%}
  {%- set attrs = line.item.attributes | default([]) -%}
  {%- set project_attr = attrs | selectattr("name", "equalto", "Project") | first -%}
  {%- set role_attr = attrs | selectattr("name", "equalto", "Role") | first -%}
  {%- set pns.rows = pns.rows + [{"project": project_attr.value if project_attr else line.item.name, "role": role_attr.value if role_attr else line.item.name, "line": line, "hours": (line.quantity | float) if line.unit == "HUR" else 0, "net": ns.nets[loop.index0]}] -%}
{%- endfor -%}
{%- set line_groups = pns.rows | group_by("project") -%}
{%- set total_hours = pns.rows | sum_by("hours") -%}
{%- set entry_groups = timesheet.entries | group_by("project") if (timesheet is defined and timesheet.entries) else [] -%}

{%- macro hours(value) -%}{{ value | format_number(loc, 2) }}{%- endmacro -%}
{%- macro money(value) -%}{{ value | format_currency(cur, loc) }}{%- endmacro -%}
{%- macro day(value) -%}{{ value | format_date("long", loc) }}{%- endmacro -%}
{%- macro project_of(code) -%}{%- set p = timesheet.projects | selectattr("code", "equalto", code) | first if timesheet is defined else none -%}{{ p.name if p else code }}{%- endmacro -%}
{%- macro grouped(value) -%}
  {%- for i in range(0, value | length, 4) -%}{{ value[i:i + 4] }}{{ " " if not loop.last }}{%- endfor -%}
{%- endmacro -%}
{%- macro address(party) -%}
  {% if party.address.line1 %}{{ party.address.line1 }}<br>{% endif %}
  {% if party.address.line2 %}{{ party.address.line2 }}<br>{% endif %}
  {{ party.address.postcode }} {{ party.address.city }}<br>
  {{ countries[party.address.country] or party.address.country }}
{%- endmacro -%}

<header class="masthead">
  <div class="brand">
    <svg class="mark" viewBox="0 0 40 40" aria-hidden="true">
      <rect width="40" height="40" rx="10" fill="#3b39a6"/>
      <path d="M10 11v18h8" fill="none" stroke="#ffffff" stroke-width="3.2" stroke-linecap="round" stroke-linejoin="round"/>
      <path d="M21 11l5.5 18L32 11" fill="none" stroke="#c6c4f1" stroke-width="3.2" stroke-linecap="round" stroke-linejoin="round"/>
    </svg>
    <div>
      <div class="brand-name">{{ inv.seller.trading_name or inv.seller.name }}</div>
      {% if tagline is defined and tagline %}<div class="brand-tagline">{{ tagline }}</div>{% endif %}
    </div>
  </div>
  <div class="doc-id">
    <h1 class="kicker">{{ "Credit note" if inv.type_code in ["381", "261"] else "Invoice" }}</h1>
    <div class="number">{{ inv.number }}</div>
  </div>
</header>

<section class="overview">
  <div class="due-card">
    <div class="due-label">Amount due</div>
    <div class="due-amount">{{ money(totals.due_payable) }}</div>
    {% if inv.due_date %}<div class="due-date">Due {{ day(inv.due_date) }}</div>{% endif %}
    {% if inv.payment_terms %}<div class="due-terms">{{ inv.payment_terms }}</div>{% endif %}
  </div>
  <dl class="facts">
    {% if inv.invoice_period %}
    <div>
      <dt>Billing period</dt>
      <dd>{{ inv.invoice_period.start | format_date("medium", loc) }} – {{ inv.invoice_period.end | format_date("medium", loc) }}</dd>
    </div>
    {% endif %}
    <div>
      <dt>Issued</dt>
      <dd>{{ day(inv.issue_date) }}</dd>
    </div>
    <div>
      <dt>Hours billed</dt>
      <dd>{{ hours(total_hours) }} hours · {{ line_groups | length }} project{{ "s" if line_groups | length != 1 else "" }}</dd>
    </div>
    {% if inv.order_reference %}
    <div>
      <dt>Purchase order</dt>
      <dd>{{ inv.order_reference }}</dd>
    </div>
    {% endif %}
  </dl>
</section>

<section class="parties">
  <div>
    <p class="label">From</p>
    <p class="strong">{{ inv.seller.name }}</p>
    <p>{{ address(inv.seller) }}</p>
    {% if inv.seller.contact %}<p class="muted">{{ inv.seller.contact.email }}{% if inv.seller.contact.phone %}<br>{{ inv.seller.contact.phone }}{% endif %}</p>{% endif %}
    <p class="muted">{% if inv.seller.vat_id %}VAT {{ inv.seller.vat_id }}{% endif %}{% if inv.seller.legal_registration %}<br>{{ "KvK" if inv.seller.legal_registration.scheme == "0106" else "Reg. no." }} {{ inv.seller.legal_registration.id }}{% endif %}</p>
  </div>
  <div>
    <p class="label">Bill to</p>
    <p class="strong">{{ inv.buyer.name }}</p>
    {% if inv.buyer.contact and inv.buyer.contact.name %}<p>{{ inv.buyer.contact.name }}</p>{% endif %}
    <p>{{ address(inv.buyer) }}</p>
    {% if inv.buyer.vat_id %}<p class="muted">VAT {{ inv.buyer.vat_id }}</p>{% endif %}
  </div>
  <div>
    <p class="label">Engagement</p>
    {% if timesheet is defined and timesheet.engagement %}<p class="strong">{{ timesheet.engagement }}</p>{% endif %}
    {% if inv.contract_reference %}<p>Statement of work {{ inv.contract_reference }}</p>{% endif %}
    {% if inv.project_reference %}<p>Project {{ inv.project_reference }}</p>{% endif %}
    <p class="muted">Rates as agreed per consultant role; time billed in quarter hours.</p>
  </div>
</section>

<section class="charges">
  <h2 class="section-title">Charges by project</h2>
  <table class="lines">
    <thead>
      <tr>
        <th scope="col">Service</th>
        <th class="num col-hours" scope="col">Hours</th>
        <th class="num col-rate" scope="col">Rate</th>
        <th class="num col-vat" scope="col">VAT</th>
        <th class="num col-amount" scope="col">Net amount</th>
      </tr>
    </thead>
    {% for group in line_groups %}
    {%- set project = (timesheet.projects | selectattr("code", "equalto", group.grouper) | first) if timesheet is defined else none -%}
    {%- set group_hours = group.list | sum_by("hours") -%}
    <tbody class="project">
      <tr class="project-head">
        <td colspan="5">
          <div class="project-row">
            <div>
              <span class="code">{{ group.grouper }}</span>
              <span class="project-name">{{ project.name if project else group.grouper }}</span>
            </div>
            {% if project and project.budget_hours %}
            {%- set used_hours = project.prior_hours + group_hours -%}
            {%- set prior_pct = [project.prior_hours / project.budget_hours * 100, 100] | min -%}
            {%- set period_pct = [group_hours / project.budget_hours * 100, 100 - prior_pct] | min -%}
            <div class="budget">
              <div class="bar"><span class="bar-prior" style="width: {{ prior_pct | round(2) }}%"></span><span class="bar-period" style="width: {{ period_pct | round(2) }}%"></span></div>
              <div class="bar-caption">Budget {{ hours(used_hours) }} of {{ project.budget_hours }} h used · {{ (used_hours / project.budget_hours) | format_percent(loc) }}</div>
            </div>
            {% endif %}
          </div>
        </td>
      </tr>
      {% for row in group.list %}
      <tr class="line">
        <td>{{ row.role }}{% if row.line.item.description %}<span class="role">{{ row.line.item.description }}</span>{% endif %}</td>
        <td class="num">{{ hours(row.line.quantity) }}</td>
        <td class="num">{{ money(row.line.price.net) }}</td>
        <td class="num">{% if row.line.tax.category in exemptions %}{{ exemptions[row.line.tax.category] }}{% else %}{{ (row.line.tax.rate | float) | format_number(loc) }}%{% endif %}</td>
        <td class="num">{{ money(row.net) }}</td>
      </tr>
      {% endfor %}
    </tbody>
    {% endfor %}
  </table>
  <div class="charges-foot">
    <p class="legend"><span class="swatch swatch-prior"></span>Previous invoices <span class="swatch swatch-period"></span>This invoice</p>
    <table class="totals">
      <tr><th scope="row">Net total</th><td class="num">{{ money(totals.tax_basis_total) }}</td></tr>
      {% for b in breakdown %}
      <tr><th scope="row">{% if b.category in exemptions %}VAT · {{ exemptions[b.category] }}{% else %}VAT {{ (b.rate | float) | format_number(loc) }}%{% endif %}{% if breakdown | length > 1 %} on {{ money(b.basis) }}{% endif %}</th><td class="num">{{ money(b.tax) }}</td></tr>
      {% endfor %}
      {% if (totals.prepaid | float) != 0 %}
      <tr><th scope="row">Total</th><td class="num">{{ money(totals.grand_total) }}</td></tr>
      <tr><th scope="row">Paid</th><td class="num">−{{ money(totals.prepaid) }}</td></tr>
      {% endif %}
      <tr class="grand-total"><th scope="row">Amount due</th><td class="num">{{ money(totals.due_payable) }}</td></tr>
    </table>
  </div>
</section>

{% if entry_groups %}
<h2 class="section-title">Time detail</h2>
<table class="entries">
  <thead>
    <tr>
      <th class="col-date" scope="col">Date</th>
      <th class="col-who" scope="col">Consultant</th>
      <th scope="col">Work performed</th>
      <th class="num col-hours" scope="col">Hours</th>
      <th class="num col-rate" scope="col">Rate</th>
      <th class="num col-amount" scope="col">Net amount</th>
    </tr>
  </thead>
  {% for group in entry_groups %}
  <tbody class="project">
    <tr class="project-head">
      <td colspan="6">
        <span class="project-name">{{ project_of(group.grouper) }}</span>
        <span class="project-count">{{ group.list | length }} entr{{ "ies" if group.list | length != 1 else "y" }}</span>
      </td>
    </tr>
    {% for entry in group.list %}
    <tr class="entry{% if loop.last %} last{% endif %}">
      <td class="col-date">{{ entry.date | format_date("%a %d %b", loc) }}</td>
      <td class="col-who">
        {{ entry.consultant }}
        {% if entry.role %}<span class="role">{{ entry.role }}</span>{% endif %}
      </td>
      <td>{{ entry.description }}</td>
      <td class="num">{{ hours(entry.hours) }}</td>
      <td class="num">{{ money(entry.rate) }}</td>
      <td class="num">{{ money(entry.amount) }}</td>
    </tr>
    {% endfor %}
    <tr class="subtotal">
      <td colspan="3">Subtotal · {{ project_of(group.grouper) }}</td>
      <td class="num">{{ hours(group.list | sum_by("hours")) }}</td>
      <td class="blank"></td>
      <td class="num">{{ money(group.list | sum_by("amount")) }}</td>
    </tr>
  </tbody>
  {% endfor %}
  <tbody class="grand">
    <tr>
      <td colspan="3">Total{% if inv.invoice_period %} for {{ inv.invoice_period.start | format_date("medium", loc) }} – {{ inv.invoice_period.end | format_date("medium", loc) }}{% endif %} (excl. VAT)</td>
      <td class="num">{{ hours(timesheet.entries | sum_by("hours")) }}</td>
      <td class="blank"></td>
      <td class="num">{{ money(timesheet.entries | sum_by("amount")) }}</td>
    </tr>
  </tbody>
</table>
{% endif %}

<section class="closing">
  {% if inv.payment %}
  <div class="remit">
    <h2>How to pay</h2>
    <dl>
      {% for account in inv.payment.credit_transfers | default([]) %}
      {% if loop.first and bank_name is defined and bank_name %}<dt>Bank</dt><dd>{{ bank_name }}</dd>{% endif %}
      <dt>Account name</dt><dd>{{ account.account_name or inv.seller.name }}</dd>
      <dt>{{ "IBAN" if (account.account_id | length) >= 15 else "Account" }}</dt><dd class="mono">{{ grouped(account.account_id) }}</dd>
      {% if account.bic %}<dt>BIC</dt><dd class="mono">{{ account.bic }}</dd>{% endif %}
      {% endfor %}
      <dt>Reference</dt><dd class="mono">{{ inv.payment.remittance_information or inv.number }}</dd>
      {% if inv.due_date %}<dt>Due by</dt><dd>{{ day(inv.due_date) }}</dd>{% endif %}
    </dl>
  </div>
  {% endif %}
  <div class="notes">
    <h2>Notes</h2>
    {% for note in inv.notes | default([]) %}<p>{{ note.text }}</p>{% endfor %}
    {% if embedded %}<p class="einvoice-hint">This PDF carries an embedded e-invoice ({{ profiles[embedded.profile] or embedded.profile }} XML).</p>{% endif %}
  </div>
</section>

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": {
    "invoice": {
      "number": "LV-26-0311",
      "issue_date": "2026-09-15",
      "due_date": "2026-09-30",
      "currency": "EUR",
      "order_reference": "SWO-PO-7719",
      "contract_reference": "SOW-2026-03",
      "payment_terms": "Net 15 days",
      "invoice_period": {
        "start": "2026-09-01",
        "end": "2026-09-14"
      },
      "delivery": {
        "date": "2026-09-14"
      },
      "seller": {
        "name": "Larkspur & Vane Consulting B.V.",
        "vat_id": "NL123456789B01",
        "legal_registration": {
          "id": "12345678",
          "scheme": "0106"
        },
        "electronic_address": {
          "id": "billing@larkspurvane.example.com",
          "scheme": "EM"
        },
        "address": {
          "line1": "Suite 4.02, Aldermeerkade 118",
          "city": "Rotterdam",
          "postcode": "3011 AB",
          "country": "NL"
        },
        "contact": {
          "name": "Billing",
          "email": "billing@larkspurvane.example.com"
        }
      },
      "buyer": {
        "name": "Sablewood Outfitters B.V.",
        "vat_id": "NL987654321B01",
        "electronic_address": {
          "id": "ap@sablewood.example.com",
          "scheme": "EM"
        },
        "address": {
          "line1": "Graniethaven 23",
          "city": "Tilburg",
          "postcode": "5048 AB",
          "country": "NL"
        },
        "contact": {
          "name": "Dana Verhoeven, Head of Operations"
        }
      },
      "payment": {
        "means_code": "58",
        "remittance_information": "LV-26-0311",
        "credit_transfers": [
          {
            "account_id": "NL74EXMP0123456789",
            "account_name": "Larkspur & Vane Consulting B.V.",
            "bic": "EXMPNL2A"
          }
        ]
      },
      "lines": [
        {
          "quantity": "4.5",
          "unit": "HUR",
          "price": {
            "net": "210.00"
          },
          "tax": {
            "category": "S",
            "rate": "21"
          },
          "item": {
            "name": "Demand forecasting model – Principal",
            "attributes": [
              {
                "name": "Project",
                "value": "DFM"
              },
              {
                "name": "Role",
                "value": "Principal"
              }
            ]
          }
        },
        {
          "quantity": "10",
          "unit": "HUR",
          "price": {
            "net": "165.00"
          },
          "tax": {
            "category": "S",
            "rate": "21"
          },
          "item": {
            "name": "Demand forecasting model – Senior consultant",
            "attributes": [
              {
                "name": "Project",
                "value": "DFM"
              },
              {
                "name": "Role",
                "value": "Senior consultant"
              }
            ]
          }
        }
      ],
      "notes": [
        {
          "text": "Time is recorded in 15-minute increments and billed at the rates agreed in SOW-2026-03. Travel to the Tilburg distribution centre was pre-approved and will be invoiced separately at cost."
        },
        {
          "text": "Questions about an entry? Email billing@larkspurvane.example.com."
        }
      ]
    },
    "tagline": "Operations and analytics advisory",
    "website": "larkspurvane.example.com",
    "bank_name": "Kademeer Bank",
    "timesheet": {
      "engagement": "Supply chain analytics programme",
      "projects": [
        {
          "code": "DFM",
          "name": "Demand forecasting model",
          "budget_hours": 120,
          "prior_hours": 64.5
        },
        {
          "code": "POS",
          "name": "Point-of-sale data feed",
          "budget_hours": 60,
          "prior_hours": 22.25
        }
      ],
      "entries": [
        {
          "date": "2026-09-01",
          "project": "DFM",
          "consultant": "Maren Okafor",
          "role": "Principal",
          "description": "Kick-off workshop with merchandising and finance; agree forecast horizons and accuracy targets",
          "hours": 3,
          "rate": 210,
          "amount": 630
        },
        {
          "date": "2026-09-02",
          "project": "DFM",
          "consultant": "Ines Castellano",
          "role": "Data analyst",
          "description": "Profile three years of weekly sales history; flag gaps, outliers and stock-outs",
          "hours": 6.5,
          "rate": 120,
          "amount": 780
        }
      ]
    }
  },
  "output": {
    "format": "pdf",
    "filename": "invoice-hourly-timesheet.pdf"
  }
}'