CSV and Excel Import
Upload a spreadsheet of invoices and GoRoute turns each row into a Peppol document and sends it. This is the path for teams that cannot integrate with the API directly: export from your accounting system, check the file, upload it.
The import accepts both CSV files and native Excel workbooks (.xlsx). An
uploaded workbook is converted to CSV internally, so both formats run through
exactly the same parsing, validation and identity checks.
This page covers file import under /api/v1/invoices/import/csv, where you
upload a spreadsheet. Batch Sending
covers a different mechanism under /api/v1/batches, where you build a batch
from documents you already hold. They are not interchangeable.
The endpointsโ
| Method | Path | Purpose |
|---|---|---|
GET | /api/v1/invoices/import/csv/template | Download a sample file to fill in |
GET | /api/v1/invoices/import/csv/columns | List every column name and alias the importer accepts |
POST | /api/v1/invoices/import/csv/preview | Parse and fully validate without sending |
POST | /api/v1/invoices/import/csv/validation-report | Turn preview results into a downloadable error report |
POST | /api/v1/invoices/import/csv | Import and send, asynchronously |
Preview and import require the csv_import feature to be enabled for your
organisation, and the invoices:send permission. The template and column
endpoints require invoices:read.
The recommended sequenceโ
Preview first. The preview endpoint validates every invoice in the file against the same compliance stack the send path uses, so anything that would be rejected after sending is visible before you commit to it.
- Download the template so your column headers match.
- Preview the file. Fix what comes back.
- Import the file. This returns immediately with a job ID.
- Poll the job until it finishes, then fetch the result.
1. Download a templateโ
curl -X GET "https://app.goroute.ai/peppol-api/api/v1/invoices/import/csv/template" \
-H "X-API-Key: YOUR_API_KEY" \
-o invoice_template_simple.csv
Two query parameters change what you get:
formatโsimple(default) puts one invoice on one row.multiproduces a multi-line layout, where several rows sharing aninvoice_numberbecome one invoice with several lines.filetypeโcsv(default), orxlsxfor a native Excel workbook.
The template is tenant-aware. Organisations registered in Oman are given the
additional Fawtara classification columns (document_type, om_invoice_kind,
seller_identifier and the import, export and item-classification columns);
other organisations are given a leaner template without them.
2. Check which columns are acceptedโ
curl -X GET "https://app.goroute.ai/peppol-api/api/v1/invoices/import/csv/columns" \
-H "X-API-Key: YOUR_API_KEY"
This returns every field the importer understands, the aliases accepted for it, whether it is required, and a short description. It is the authoritative list โ it is generated from the parser itself, so it cannot drift from what the importer actually does.
Required columnsโ
Four columns must be present and populated:
| Column | Meaning |
|---|---|
invoice_number | Unique invoice identifier, for example INV-2026-001 |
buyer_peppol_id | Customer's Peppol participant ID, as scheme:value |
buyer_name | Customer's company name |
line_description | Description of the product or service |
Commonly used optional columnsโ
Everything else is optional. Blank means "a standard invoice".
| Column | Meaning |
|---|---|
issue_date | Date the invoice was issued; YYYY-MM-DD is preferred |
due_date | Payment due date |
currency | Three-letter currency code |
buyer_reference | The customer's purchase order number or reference |
notes | Additional notes to appear on the invoice |
seller_peppol_id, seller_name, seller_vat, seller_country | Your own details |
buyer_vat, buyer_country, buyer_street, buyer_city | More customer details |
line_quantity | Quantity; defaults to 1 |
line_unit | Unit of measure, for example EA, HUR, DAY |
line_unit_price | Price per unit |
line_tax_rate | Tax rate as a percentage, for example 25 for 25% |
line_tax_category | Tax category for the line โ see Zero-rated and exempt lines |
total_payable | Total amount due; calculated if you leave it out |
payment_iban, payment_bic, payment_reference | Payment details |
If your seller details are missing from the file, they are filled in from your organisation's company settings.
Column names are flexibleโ
Most columns accept several spellings, so an export from your accounting system
often works without renaming anything. invoice_number also accepts
invoice_no, inv_num, number, invoice # and document_number.
line_quantity also accepts quantity and qty. buyer_name also accepts
customer_name, customer, recipient and to_name. Call the columns
endpoint for the full mapping rather than guessing.
Zero-rated and exempt linesโ
A tax rate of zero does not say enough. Zero-rated and exempt are two different tax categories with two different reason-code lists, and a line sent under the wrong one is rejected. Three columns let you state which you mean.
| Column | Also accepted as | What it is for |
|---|---|---|
line_tax_category | tax_category, vat_category | Says outright which category the line is |
line_tax_exemption_reason_code | vat_exemption_reason_code, tax_exemption_reason_code, exemption_code | The official code that justifies the zero rate or the exemption |
line_tax_exemption_reason | vat_exemption_reason, tax_exemption_reason, exemption_reason | The same justification in words |
line_tax_category accepts standard, zero, zero_rated, exempt,
export, outside_scope and out_of_scope. Capitalisation and surrounding
spaces are ignored.
How the importer decidesโ
For each line, in this order:
- If
line_tax_categoryhas a value, that value wins โ whatever the rate says. - Otherwise, if the rate is zero or missing, the row's own reason code decides:
a code beginning
VATEXmakes the line exempt, anything else makes it zero-rated. - Otherwise the line is standard.
So a spreadsheet that already carries reason codes gets the right answer without
a new column. Add line_tax_category when you want to be explicit, or when a
line is exempt but carries no code beginning VATEX.
The platform recognises seven tax categories. This column reaches five of them. Reduced rate and reverse charge have no spelling here and cannot be selected through a CSV or Excel import at all.
A value the importer does not recognise is worse than a rejection: it records an
error against the row and falls back to standard, and the row is still
imported and sent. Read the preview before you import, and treat a tax-category
error as a stop.
Oman: a reason code is mandatoryโ
The Oman rulebook GoRoute ships and runs states it as a fatal rule:
[IBR-069-OM] - A VAT breakdown (IBG-23) with VAT Category code (IBT-118) "E" and/or "Z" must have a VAT exemption reason code (IBT-121).
In plain terms: every exempt or zero-rated line on an Omani invoice must carry a
code in line_tax_exemption_reason_code, or the invoice fails validation. The
two lists are separate โ zero-rating codes and exemption codes are not
interchangeable. The
Oman country guide
lists both, and is the one place to look them up.
This quotation is taken from the Schematron rule file inside the product, not from a page on the Oman Tax Authority's website.
3. Preview before sendingโ
import requests
API_KEY = "YOUR_API_KEY"
BASE_URL = "https://app.goroute.ai/peppol-api"
with open("invoices.csv", "rb") as fh:
response = requests.post(
f"{BASE_URL}/api/v1/invoices/import/csv/preview",
headers={"X-API-Key": API_KEY},
files={"file": ("invoices.csv", fh, "text/csv")},
)
preview = response.json()
print(preview["total_rows"], "rows")
print(preview["valid_invoices"], "valid,", preview["invalid_invoices"], "invalid")
print(preview["skipped_rows"], "rows skipped at parse time")
The response reports:
total_rows,valid_invoices,invalid_invoices,total_errorsandtotal_warnings.skipped_rowsโ data rows dropped during parsing, for example a row with no invoice number. This is reported separately so that seventeen rows becoming four invoices is explained rather than looking like lost data.column_mappingandunmapped_columnsโ which of your headers were recognised, and which were ignored.invoice_validationsโ the full validation result for every invoice, not just the first few.
Every invoice is validated against the complete stack: UBL 2.1 structure, calculation and business rules, eDEC code lists, CEN-EN16931 and Peppol BIS 3.0 Schematron rules, and any national extension that applies to the receiver's country.
4. Download an error reportโ
For a file with many problems, turn the preview result into a report someone can work through in a spreadsheet.
response = requests.post(
f"{BASE_URL}/api/v1/invoices/import/csv/validation-report",
headers={"X-API-Key": API_KEY},
json={"invoice_validations": preview["invoice_validations"], "format": "csv"},
)
with open("validation_report.csv", "wb") as fh:
fh.write(response.content)
format accepts csv for a flat spreadsheet with one row per error, or json
for the full structured report. The CSV columns are invoice number, status,
layer, rule ID, severity, message, XPath and field.
5. Import and sendโ
with open("invoices.csv", "rb") as fh:
response = requests.post(
f"{BASE_URL}/api/v1/invoices/import/csv",
headers={"X-API-Key": API_KEY},
files={"file": ("invoices.csv", fh, "text/csv")},
)
job = response.json() # HTTP 202
job_id = job["job_id"]
print(job_id, job["total_rows"], job["status_url"])
This returns 202 Accepted, not a completed result. The file is parsed immediately โ headers checked, rows counted, your default seller resolved โ and then an import job is created. The heavy work per row (rendering the Peppol XML, signing it where the jurisdiction requires it, running Schematron validation and queueing for delivery) happens in the background, so the request returns within seconds even for tens of thousands of rows.
The maximum upload size is 100 MB.
Poll the jobโ
The 202 response carries status_url and result_url. Poll the status until it
reaches a terminal value โ validated, partially_validated or failed โ then
fetch the result.
import time
while True:
status = requests.get(
f"{BASE_URL}/api/v1/import-jobs/{job_id}",
headers={"X-API-Key": API_KEY},
).json()
if status["status"] in ("validated", "partially_validated", "failed"):
break
time.sleep(2)
result = requests.get(
f"{BASE_URL}/api/v1/import-jobs/{job_id}/result.json",
headers={"X-API-Key": API_KEY},
).json()
partially_validated means some rows were accepted and others rejected. The
result payload lists the transactions created and the errors for the rows that
did not make it.
Multi-line invoicesโ
In the multi template layout, several rows that share the same
invoice_number are grouped into a single invoice with several lines. This
grouping is applied on both the preview and the import path, so what you see in
the preview is what is sent.
Next Stepsโ
- Saved Parties โ store customers you invoice repeatedly instead of retyping them in every file
- Batch Sending โ the other bulk mechanism
- Validation โ what the validation layers check
- Tracking โ following documents after they are sent
- Error Handling