Skip to main content

Invoice Templates

An invoice template is a saved skeleton of an invoice โ€” the buyer, the line items, the payment terms, whatever you invoice repeatedly โ€” stored once under a name you choose. Later you apply the template and get that data back, ready to turn into a real invoice. It is the feature behind a "repeat last month's invoice" button.

Templates are private to your organisation. Creating one publishes nothing and sends nothing.

Applying a template does not create or send an invoice

POST /api/v1/templates/{template_id}/apply returns a block of JSON and nothing else. Nothing is validated, nothing is queued, and nothing reaches the Peppol network. To actually send the result, you post it yourself to the invoice endpoint โ€” see Send an Invoice.

An ordinary API key is enough

Every endpoint on this page accepts either your organisation's API key or a signed-in portal session, and requires no permission beyond that. Anyone holding a valid key for your organisation can list, create, change, apply and permanently delete templates. This is worth knowing because some of these calls delete data. It is the same arrangement as Saved Parties, and a looser one than the transaction endpoints, which require named permissions.

The endpointsโ€‹

All paths are relative to https://app.goroute.ai/peppol-api. {template_id} is the UUID of the template.

Method and pathPurpose
GET /api/v1/templatesList templates. Favourites first, then most used
POST /api/v1/templatesCreate a template. Returns 201
GET /api/v1/templates/searchSearch name and description. q is required
GET /api/v1/templates/favoritesActive templates flagged as favourites, in name order
POST /api/v1/templates/from-invoiceBuild a template from an invoice you already have. Returns 201
GET /api/v1/templates/{template_id}Fetch one template. 404 if it is not yours
PATCH /api/v1/templates/{template_id}Change name, description, type, data, favourite or active flag
DELETE /api/v1/templates/{template_id}Delete permanently. Returns 204 with no body
POST /api/v1/templates/{template_id}/favoriteFlip the favourite flag. No request body
POST /api/v1/templates/{template_id}/applyReturn the invoice data the template produces

Seven addresses, ten operations. Every one of them answers 401 without a valid credential, and every one that names a single template answers 404 when that template belongs to another organisation โ€” the same answer you get when it does not exist at all.

Create a templateโ€‹

POST /api/v1/templates โ€” returns 201 Created.

import requests

BASE_URL = "https://app.goroute.ai/peppol-api"
HEADERS = {"X-API-Key": "your_api_key", "Content-Type": "application/json"}

response = requests.post(
f"{BASE_URL}/api/v1/templates",
headers=HEADERS,
json={
"name": "Monthly retainer - Acme BV",
"description": "Standard monthly consulting retainer",
"template_type": "full",
"is_favorite": True,
"template_data": {
"buyer": {
"name": "Acme BV",
"peppol_id": {"scheme": "0106", "value": "12345678"},
},
"currency": "EUR",
"payment_terms": "Net 30",
"lines": [
{
"description": "Consulting retainer",
"quantity": 1,
"unit_price": 2500.00,
"tax_percent": 21,
}
],
},
},
)

template = response.json()
print(template["id"], template["use_count"])
FieldRequiredNotes
nameYes1 to 255 characters
template_dataYesThe saved invoice data. Any JSON object; see the note below
descriptionNoFree text
template_typeNoDefaults to full
is_favoriteNoDefaults to false
We store template_data, we do not check it

GoRoute does not validate template_data against the invoice format when you save it. It is stored exactly as you send it and returned exactly as you sent it. A template holding a field name that no invoice accepts is saved happily, and only fails later, when you send the applied result. Check the result of an apply before relying on a template in production โ€” see Validation.

Build a template from an invoiceโ€‹

POST /api/v1/templates/from-invoice โ€” returns 201 Created. Send an invoice you already have and GoRoute strips the parts that should not repeat.

response = requests.post(
f"{BASE_URL}/api/v1/templates/from-invoice",
headers=HEADERS,
json={
"name": "Repeat of INV-2026-0042",
"invoice": previous_invoice, # the whole invoice object
"exclude_fields": ["purchase_order_reference"],
},
)
Fields disappear, and nothing tells you which

Five fields are always dropped, whether you ask for it or not: invoice_number, issue_date, due_date, tax_point_date and delivery.

Every field whose value is null is also dropped. A field you deliberately set to null on the original invoice is simply absent from the template.

There is no message listing what was removed. If you compare the template against the invoice it came from, expect it to be smaller.

exclude_fields adds to the list of five โ€” it does not replace it. There is no way to keep invoice_number in a template built this way.

This request has no is_favorite field. Create the template first, then flip the flag with POST /api/v1/templates/{template_id}/favorite.

Apply a templateโ€‹

POST /api/v1/templates/{template_id}/apply โ€” returns the resulting invoice data as a plain JSON object, not wrapped in an envelope. The request body is optional.

response = requests.post(
f"{BASE_URL}/api/v1/templates/{template_id}/apply",
headers=HEADERS,
json={"overrides": {"buyer": {"name": "Acme Netherlands BV"}}},
)

invoice_data = response.json()
# invoice_data is yours to post to the invoice endpoint; nothing has been sent

404 if the template does not exist or belongs to another organisation.

How overrides are mergedโ€‹

Overrides are merged into the template field by field, and the merge goes down into nested objects. In the example above, buyer.name is replaced while every other field inside buyer โ€” the Peppol identifier, the address โ€” survives.

The rule is narrower than it first looks:

  • If a key holds an object in both the template and the overrides, the two objects are merged, and the same rule is applied again inside them.
  • Anything else is replaced outright. That includes arrays. A lines override replaces the whole list of line items; it does not merge them item by item and it does not append to them. To change one line, send the complete list back with that line changed.

Applying a template changes itโ€‹

apply looks like a read. It is not. Every successful apply:

  • increases use_count by one, and
  • sets last_used_at to the time of the call.

Both values come back in later responses, and both affect the order templates are listed in โ€” so applying a template repeatedly pushes it up your own list. There is no look-without-touching mode. If you want to inspect a template without disturbing the count, fetch it with GET /api/v1/templates/{template_id} and read template_data instead.

List, search and favouritesโ€‹

GET /api/v1/templates returns {"templates": [...], "count": N}.

count is the size of this response

count is the number of templates in the response you are holding, not the total number you have stored. If you ask for limit=10 and have 60 templates, count is 10. There is no total and no page cursor; raise limit to see more, up to 100.

ParameterDefaultNotes
template_typenoneOptional exact-match filter
include_inactivefalseSet true to include templates with is_active set to false
limit50Between 1 and 100

Results come back favourites first, then highest use_count, then name in alphabetical order.

GET /api/v1/templates/search takes q (required, at least one character) and limit (between 1 and 50, default 10). q is a case-insensitive "contains" match against the name and the description, so retain finds Monthly retainer - Acme BV. Search covers active templates only and has no template_type filter.

GET /api/v1/templates/favorites takes no parameters and returns active favourites in name order. A favourite that has been made inactive does not appear here.

Change or remove a templateโ€‹

PATCH /api/v1/templates/{template_id} accepts name, description, template_type, template_data, is_favorite and is_active. Send only the fields you are changing; anything you leave out is untouched.

template_data is replaced, not merged

Unlike overrides on an apply, a template_data sent to PATCH overwrites the stored object completely. To change one field, read the template first, change that field, and send the whole object back.

POST /api/v1/templates/{template_id}/favorite toggles the flag rather than setting it, so calling it twice puts you back where you started. The name reads like "make favourite"; it is not. To set the flag to a known value, use PATCH with {"is_favorite": true} or {"is_favorite": false}.

Deleting and deactivating are not the same thingโ€‹

These two are one call apart and only one of them can be undone.

  • PATCH with {"is_active": false} hides the template from the list, the search and the favourites, but keeps it. You can bring it back, and include_inactive=true still finds it.
  • DELETE /api/v1/templates/{template_id} removes the record permanently. It returns 204 with no body. There is no undo and no recycle bin. 404 if it is not yours.

If you are retiring a template rather than destroying it, deactivate it.

What a template response containsโ€‹

FieldMeaning
idThe template's UUID
name, descriptionAs stored
template_typeAs stored โ€” see the note below
template_dataThe saved invoice data, exactly as you sent it
use_countHow many times the template has been applied. Starts at 0
last_used_atWhen it was last applied, or null if it never has been
is_favoriteWhether it is pinned to the top of listings
is_activefalse hides it from listings without deleting it
created_at, updated_atTimestamps
template_type is a label, not a checked value

The product describes three types โ€” full for a complete invoice, lines for line items only, and buyer for a buyer plus line items. Nothing enforces those three names. The value is stored as free text in a 20-character field and is used only as an exact-match filter on the list endpoint. It does not change how a template is applied or what it may contain. Sending template_type: "quarterly" is accepted and behaves like any other label.

Organisation invoice defaults โ€” stored, but never appliedโ€‹

Separately from templates, your organisation has a small block of settings called invoice defaults: a currency, payment terms, a payment means code, a note, a tax rate and a tax category. They look like the answer to "set these once and every invoice gets them". They are not.

GoRoute does not apply these to anything

Nothing in the product reads these values when you create, validate, prefill or send an invoice. They are written by PUT, read back by GET, and that is the whole of their effect.

The clearest example is the currency. Auto-fix has a repair called apply_default_currency that fills in a missing currency โ€” and it writes a fixed EUR of its own, without ever looking at the currency you stored here. Whatever you set, an invoice with no currency behaves exactly as if these settings were empty.

Treat them as a scratchpad your own integration can read and apply itself. If you want every invoice to carry the same currency and payment terms without repeating them, use an invoice template โ€” the feature documented on the rest of this page โ€” and apply it. That does prefill an invoice; this does not.

The endpointsโ€‹

All paths are relative to https://app.goroute.ai/peppol-api.

Method and pathPurposePermission
GET /api/v1/settings/company/invoice-defaultsRead the stored defaultsA valid credential for the organisation; no named permission
PUT /api/v1/settings/company/invoice-defaultsStore defaultssettings:manage

Both answer 401 without a valid credential. The permissions above are the ones each route declares in the product; this page states what the code requires, not a measurement of what a server enforces.

These defaults are also readable and writable alongside your seller details and your branding in a single call โ€” GET and PATCH /api/v1/settings/company. See reading and writing all your company settings in one call.

The fieldsโ€‹

Every field is optional on the PUT.

FieldTypeNotes
currencystringAn ISO 4217 code. Not checked against the list
payment_terms_daysintegerBetween 0 and 365. This one is range-checked
payment_means_codestringA UNCL 4461 code โ€” 30 is credit transfer. Not checked against the list
notestringFree text
tax_ratestringA percentage written as a string, for example "25.00" โ€” not a number
tax_categorystringA tax category code such as S, Z or E. Not checked against the list

Two things that will catch you outโ€‹

PUT behaves like a PATCH. The stored block is merged with what you send, not replaced by it. A field you leave out keeps its current value, and so does a field you send as null โ€” nulls are discarded before the merge. There is no way to clear a single field back to nothing through this call; send the empty value you want instead, such as "" for note.

The starting values are Northern European, and nobody chose them for you. Before you have ever called PUT, a GET returns a built-in block rather than an empty one:

{
"currency": "EUR",
"payment_terms_days": 30,
"payment_means_code": "30",
"note": "",
"tax_rate": "25.00",
"tax_category": "S"
}

That is a euro, a 30-day term and a 25% standard rate, regardless of where your organisation is. It is harmless today only because nothing applies it. If you build your own prefill on top of these values, set them deliberately first โ€” an Omani or Nigerian seller who inherits EUR and 25.00 from this block has been given the wrong invoice by their own integration.

Next Stepsโ€‹