Skip to main content

Connectors

A connector links one organization to one accounting system, so invoices reach the Peppol network without anyone re-keying them. An organization can run several โ€” one row per organization and connector type.

Most customers set a connector up in the portal rather than through the API. The endpoints below exist for platforms that onboard customers programmatically, and are documented in full under API Reference โ†’ Connectors.

Two kinds of connectorโ€‹

The distinction decides who initiates the traffic, and it is the first thing to get right when you plan an integration.

Pull connectorsโ€‹

GoRoute holds credentials for your accounting system and polls it. You give us access once; new invoices are picked up, converted and sent without further action.

Systemconnector_type
Odooodoo
SAP S/4HANAsap
Zoho Bookszoho
bol.combol โ€” see bol.com: shipped business orders become Peppol e-invoices, and the PDF goes back onto the bol shipment
Amazonamazon โ€” see Amazon: one-click authorisation; business shipments on the European stores become e-invoices from Amazon's own VAT figures, and the PDF goes back to Amazon

Pull connectors are the ones that have a connect/disconnect lifecycle, because there are credentials to hold.

Push connectorsโ€‹

Your system authenticates to GoRoute with an API key and pushes documents to us. There is nothing for us to poll and no credentials of yours for us to store.

Accepted values: xero, quickbooks, zuora, oracle_ebs, tally, api, custom.

Microsoft Dynamics 365 Business Central is a push connector packaged as a Business Central app on Microsoft's E-Document framework โ€” see Business Central: posting a sales invoice or credit memo sends it, and the status comes back onto the posted document.

note

Calling /connect on a push connector returns 400 โ€” "it authenticates to GoRoute with an API key rather than being polled. Nothing to connect." That is the API telling you the integration runs the other way round, not an error in your request.

Lifecycle of a pull connectorโ€‹

POST /api/v1/connectors                      register the connector type
POST /api/v1/connectors/{id}/connect hand over credentials
POST /api/v1/connectors/{id}/test confirm we can reach it
POST /api/v1/connectors/{id}/sync pull now, instead of waiting
POST /api/v1/connectors/{id}/disconnect forget the credentials
DELETE /api/v1/connectors/{id} remove it entirely

GET /api/v1/connectors lists what the organization has configured and the status of each.

Odooโ€‹

Odoo connects over XML-RPC. You supply the address the customer opens to use Odoo, the database, a username and an Odoo API key (scope RPC).

POST /api/v1/connectors/discover probes a server before you commit credentials โ€” use it to confirm the address is an Odoo instance and to read back the database name.

On a multi-company database, set company_id to the company you are sending for. Both the poller and inbound delivery are scoped to it; leave it unset on single-company databases.

Before you connect Odoo: what the first sync sendsโ€‹

The first sync sends the customer's OLDEST invoices, not their newest

Tell the customer this before you connect them.

A new connector has no bookmark yet, so the opening sweep takes the 50 oldest posted customer invoices in the database and works forward from there โ€” 50 more on each polling cycle โ€” sending each one over the Peppol network to the real buyer named on it. On a ledger carrying years of history, the first documents that leave are the oldest documents in the business, not today's.

There is no start-date setting. Nothing in the connector API lets you tell the sweep to begin at a date or skip what came before it, so the only way to keep history from going out is to agree what should be sent before the connection is made.

Three more things about that first sweep surprise people, so they are worth knowing in advance.

What qualifies, and what is ignored. Only posted customer invoices and customer credit notes are swept. Drafts, supplier bills and other journal entries are never picked up, whatever their state. On a database holding several companies, only the company the connector is scoped to with company_id is swept โ€” and if you leave company_id unset on a multi-company database, every company's invoices qualify.

Nothing leaves at all until the sending company is registered on the network. If the company's VAT number is not yet registered for e-invoicing, every invoice is held rather than sent, and the connector says so in plain words, naming the company and telling you what to do โ€” add the VAT number in Odoo, or email GoRoute to finish the registration. Invoices wait; syncing resumes on its own once the registration is in place. This is a setup step that was missed, not a fault in the connector.

Disconnecting and reconnecting starts from the beginning again. Disconnecting clears the bookmark, and so does changing which company the connector sends for. The next connection sweeps from the oldest invoice once more, exactly as on a first connect. In Oman that overlap is safe by design: an Omani invoice's identifier is derived from the invoice's own contents, so the same invoice sent twice carries the same identifier rather than arriving as a second, different invoice. We do not make that claim for other countries โ€” treat a reconnection on a long ledger as something to plan, not something to do casually.

An invoice the sweep reaches again that GoRoute has already sent is not parked, even when a rule added since it went out would now refuse it (for example the Oman export rules of 24 September 2026): GoRoute looks the invoice up by its number, its date and your company's VAT number first, and an invoice already sent has nothing left to correct. A parked invoice's reason says "GoRoute has not sent it" and means this invoice. The Zoho Books connector behaves the same way.

This is how the Odoo connector behaves on GoRoute's production service as well as on the test service.

Getting an Oman reason code onto the invoice (Odoo)โ€‹

Oman requires a reason code on every zero-rated and every exempt line โ€” see Zero-rating and exemption reason codes for the full lists and what each code means. Odoo has nowhere native to keep one, so GoRoute reads it out of the tax itself.

Put the code in the tax's name or its description, and every line using that tax carries it:

VAT 0% Zero-rated (VATZR-OM-09)

How the tax is read:

What is on the lineWhat GoRoute sends
A tax with a rate above 0%Standard-rated. No reason code is needed.
A 0% tax whose name or description contains a VATZR-OM-nn codeZero-rated, with that code.
A 0% tax marked exempt โ€” the word "exempt" in the name or description, or a VATEX-OM-nn codeExempt, with that code.
A 0% tax with no codeNothing. The invoice is parked and the tax is named in the error.
No tax at all on the lineOutside the scope of VAT โ€” not standard-rated.

Three things catch people out:

  • Only the first tax on a line is read. A line with several taxes is judged on the first one.
  • The families must match. A zero-rated tax carrying a VATEX-OM- code is refused, and so is an exempt tax carrying a VATZR-OM- code.
  • A line with no tax is treated as out of scope. GoRoute will not assume a missing tax means standard-rated, because silently taxing an untaxed line is the worse mistake.

Matching is case-insensitive, and the code may sit anywhere in the name or description.

Exports. When your company is Omani and the customer's country is outside Oman โ€” the other GCC states included โ€” the connector sends the invoice (or credit note) as an export (BTOM-001 position 7). Odoo has no export switch; the customer's country decides. A customer with no country, or in Oman, is domestic as before. The export goes out with the customer's VAT number and no Peppol ID of its own, so GoRoute addresses it to the Tax Authority's export stand-in.

An export's zero-rated lines need an export zero-rating code. Give export lines their own 0% tax, for example one named VAT 0% Export (VATZR-OM-10) (VATZR-OM-11 indirect export, VATZR-OM-09 export of service, VATZR-OM-04 / VATZR-OM-05 international transport), or charge them 5%. The invoice is parked, saying why it is an export and what to change, when:

  • a zero-rated line carries a domestic code, such as VATZR-OM-01 (food) or VATZR-OM-02 (medicines);
  • a line is a re-export of goods (VATZR-OM-12) or an export of services (VATZR-OM-09). These need a supporting document (IBR-013-OM) or a service type (IBR-155-OM), and Odoo has no field for either. Send these through the invoice API or CSV import โ€” see Zero-rated export lines;
  • the customer has no VAT number;
  • the customer's VAT number is an Omani one (it starts with OM) but its country is outside Oman. GoRoute cannot tell whether that is an export or a sale to an Omani taxpayer, so it does not guess. Correct the customer's country or VAT number.

The other half of the rule: a customer in Oman, or with no country, is not an export, so a 0% tax carrying one of the four export codes (VATZR-OM-09, -10, -11, -12) on its invoice is parked too, asking you to set the customer's country if it is abroad or to use a domestic code. VATZR-OM-04 and -05 (international transport) are allowed on a domestic invoice.

The customer's country must be a real ISO 3166-1 country. A code that is not one (a code typed onto a custom country, or XK) is parked rather than taken as "outside Oman".

Companies outside Oman are not affected: their invoices are never marked.

Zoho Booksโ€‹

Zoho connects over OAuth. The portal runs a one-click flow; if you are integrating directly, POST /api/v1/connectors/zoho/oauth/start begins it and the callback parks a refresh token you then consume with use_pending: true.

Supply the data centre your Zoho account lives in โ€” com, eu, in, au, jp or sa โ€” and the organization_id of the Zoho Books organization to send for.

Zoho also supports a webhook, so an invoice can leave Zoho and reach the network without waiting for the next poll. GET /api/v1/connectors/{id}/zoho/webhook-url returns the URL to register in Zoho.

Treat that URL like a password: the token in it is the only thing that authenticates Zoho's call. It is never shown to a partner acting on your behalf, whatever access level they were given โ€” only the organization itself can read it or regenerate it. Anyone holding an API key for your organization that may manage connectors can read it, including a key your operator issued for you. You can regenerate it from the Zoho panel in the portal (Regenerate) โ€” the old URL stops working at once, so paste the new one into the Zoho workflow rule. Until you do, invoices still arrive on the regular poll, only not instantly. If you are integrating directly rather than using the portal, the same thing is POST /api/v1/connectors/{id}/zoho/webhook-url/rotate, which answers the new URL. The URL does not expire on its own, because Zoho calls a fixed URL it cannot refresh.

Getting an Oman reason code onto the invoice (Zoho Books)โ€‹

Zoho Books cannot hold an Oman reason code on a line. Its Oman edition offers only three tax-exemption values โ€” EXEMPT, OUTOFSCOPE and PROFITMARGINSCHEME โ€” and none of them is a VATZR-OM- or VATEX-OM- code. So you state the code once, on the connector, and GoRoute applies it to every line it fits.

Two settings live in the connector's config object:

SettingApplied to
oman_zero_rate_codeEvery zero-rated line. One of VATZR-OM-01 โ€ฆ VATZR-OM-16.
oman_exempt_codeEvery exempt line. One of VATEX-OM-01 โ€ฆ VATEX-OM-12.
oman_export_zero_rate_codeEvery zero-rated line on an export (see below). One of VATZR-OM-10, -11, -04 or -05. If it is empty, export lines use oman_zero_rate_code.

Which setting a line uses:

The Zoho lineWhat GoRoute sends
Tax percentage above 0Standard-rated. Neither setting is used.
OUTOFSCOPEOutside the scope of VAT. Rule IBR-070-OM forbids a reason code here, so none is added and neither setting is needed.
EXEMPTExempt, carrying oman_exempt_code.
PROFITMARGINSCHEME, or any other line at 0%Zero-rated, carrying oman_zero_rate_code.

If the setting a line needs is empty, the invoice is not sent. It is parked, and the error tells you which setting to fill in. GoRoute will not choose a code for you: pick the one that describes what you actually sell, from the published lists.

Exports. When the customer's billing country is outside Oman, the other GCC states included, the connector sends the invoice (or credit note) as an export (BTOM-001 position 7). If the billing address has no country, or the country is Oman, the invoice is domestic, as before. GoRoute reads the country name that Zoho Books shows (for example United Arab Emirates or U.S.A, and Oman also as OMN, Muscat or Muscat, Oman). A country field holding only a dash or N/A counts as no country. If GoRoute does not recognise the name, the invoice is parked until you correct the customer's billing address. The export goes out with the customer's tax registration number and no Omani participant ID, so GoRoute addresses it to the Tax Authority's export stand-in.

An export's zero-rated lines take oman_export_zero_rate_code. Set it when you export, so that your domestic zero-rating code (for example VATZR-OM-02 for medicines) is not filed on an export. The invoice is parked, and the error names that setting, in these cases:

  • the code a zero-rated export line would get is not an export code;
  • the code is VATZR-OM-12 (re-export) or VATZR-OM-09 (export of services), which need a supporting document or a service type that Zoho Books has no field for. Send these through the invoice API or CSV import. See Zero-rated export lines;
  • the customer has no tax registration number;
  • the customer's tax registration number is an Omani VAT number (it starts with OM) but the billing country is outside Oman. Correct one of the two.
This changed on 16 September 2026 โ€” check which behaviour you have

Before 16 September 2026 the Zoho Books connector did not ask. It put VATZR-OM-01 on every zero-rated line and VATEX-OM-01 on every exempt one. Against the Tax Authority's published lists, that means zero-rated medicine was filed as a qualifying supply of food items, and every exempt supply was filed as qualifying financial services. Both are valid codes, so nothing was rejected and nothing appeared in any error report.

From 16 September 2026 the connector stops guessing and asks you for the code instead, as described above.

How to tell which one you are on: send a zero-rated invoice from Zoho Books with neither setting filled in. If it goes out, you still have the old behaviour. If it parks with an error naming oman_zero_rate_code, you have the new one.

This change is live on GoRoute's production and test services alike โ€” both run the same build. If your Zoho Books connector still shows the old behaviour, tell us: you are on a private cell that has not been updated, and we will update it.

Setting the codesโ€‹

There is no screen for these yet, in the portal or anywhere else. Set them through the Connector API with PATCH, which changes only the keys you send and keeps everything else GoRoute holds in config (the record of which invoices are stuck, an in-flight Zoho authorisation):

curl -X PATCH https://app.goroute.ai/peppol-api/api/v1/connectors/CONNECTOR_ID \
-H "X-API-Key: your_api_key" \
-H "Content-Type: application/json" \
-d '{"config": {"oman_zero_rate_code": "VATZR-OM-02",
"oman_exempt_code": "VATEX-OM-03",
"oman_export_zero_rate_code": "VATZR-OM-10"}}'

Send a key as null to remove it. Do not use POST /api/v1/connectors for this: POST still replaces the whole config. Until 24 September 2026 PATCH replaced it too, and the advice here was to read the whole object and send it back; that still works. How to tell which behaviour your service has is on the Connector API page.

oman_zero_rate_code is the code of your domestic zero-rated supplies. It must not be one of the four export codes (VATZR-OM-09 to -12): a customer with no billing country, or in Oman, is domestic, and a domestic invoice carrying an export code is parked, asking you to set the customer's country or a domestic code.

SAP S/4HANAโ€‹

SAP connects over OData. It takes config and credentials objects rather than flat fields, because it carries a party mapping that the others have no equivalent for.

POST /api/v1/connectors/{id}/documents/{doc_id}/send sends one specific document.

When an invoice cannot be sentโ€‹

Some invoices cannot be fixed by trying again โ€” a missing Oman reason code, a re-used invoice number, incomplete party details. Rather than stop the queue behind them, GoRoute parks them: the invoice is set aside with the reason, and the rest of the batch carries on.

Parked invoices are listed by name in the connector's needs_attention field, and last_error usually carries a one-line summary of everything currently parked โ€” but not always: when GoRoute could not log in to your system at all, last_error holds that instead, and no amount of correcting invoices will clear it. See last_error is not always about invoices for the field that tells the two apart.

Both fields come back on the connector:

curl https://app.goroute.ai/peppol-api/api/v1/connectors \
-H "X-API-Key: your_api_key"

Both the Odoo and the Zoho Books poller park invoices this way.

needs_attention gives you names and nothing else. To read why each invoice is held โ€” the reason, whether you or GoRoute has to fix it, and how long it has been stuck โ€” see Held invoices, with reasons, which also says which of GoRoute's services answer that call today.

If a first Odoo sync holds every invoice at once, read Before you connect Odoo: what the first sync sends before you start correcting invoices: the usual cause is that the sending company is not registered on the network yet, and no change to any invoice will release it.

Three more reasons an Odoo invoice is heldโ€‹

Since 22 September 2026 the Odoo connector holds an invoice in three situations it used to let through or blame on the wrong thing. All three are yours to fix in Odoo, so all three are tried again by themselves, at most about every fifteen minutes, as in the table below.

1. The e-invoice would not agree with what Odoo posted. GoRoute now compares its finished e-invoice with Odoo's own figures โ€” line by line, then the invoice as a whole โ€” and holds the invoice when they differ by more than rounding. The message names the figure Odoo shows and the figure the lines come to.

The commonest cause is a tax rate edited after the invoice was posted. Odoo keeps a posted invoice's tax lines as they were, but GoRoute rebuilds each line from the tax's rate today, so the books say one thing and the e-invoice says another. The remedy the message gives is to reset the invoice to draft in Odoo, let it recompute, and post it again; GoRoute then sends it.

Two other causes give their own message, because the remedy is different: a line whose net does not match Odoo's (something on the line GoRoute cannot read, such as a tax included in the price or a mark-up entered as a negative discount), and every line agreeing while the invoice total does not (something changed outside the lines, such as a tax amount edited by hand). In those two, recomputing the tax is not the fix and the message does not suggest it.

This one is worth taking seriously even if you are sure nothing is wrong. It was written because three real invoices had already been filed with a Tax Authority stating VAT of 25.00 where the seller's own books showed 75.000, after exactly the edited-tax case above.

2. A tax that is included in the price. Where a line's tax is set up as included in the unit price, Odoo's price already holds the VAT. An e-invoice takes a price without the tax and adds the VAT itself, so sent as it stood the line was taxed twice. GoRoute now holds the invoice instead of converting it, because the tax-free price is rarely a whole number of baisa and Oman's rules allow a unit price no more than three decimals โ€” converting it would file a figure that is not what Odoo booked. Use a tax that is not included in the price, or write to support@goroute.ai. A 0% tax included in the price adds nothing and still goes out.

3. A cash rounding. Odoo's cash rounding moves an invoice's total to a whole amount of cash, either with a rounding line or by adjusting a tax. Neither reaches the e-invoice, so the document would state a different total from the one Odoo posted. GoRoute now holds such an invoice and says so โ€” reset it to draft, clear its cash rounding and post it again, or ask support. A rounding that changes none of the figures the document states is still sent, so a shop that rounds every invoice to cash is not held for nothing.

This last one also corrects a misleading message. Before 22 September 2026 a cash-rounded invoice was held with the reason "the tax was edited after it was posted" โ€” a cause customers went looking for and could never find.

Two changes you would not otherwise seeโ€‹

  • A line discount now reaches the e-invoice. Before 22 September 2026 GoRoute read only Odoo's undiscounted unit price, so a discounted line went out at full price and the totals filed were higher than the invoice. The discount is now carried as a line allowance, and the buyer sees the price and the discount as the Odoo invoice shows them.
  • Invoices sent before 22 September 2026 may carry the old figures. Nothing is corrected retrospectively. If you have used line discounts, tax-included prices, cash rounding, or edited a tax rate after posting, invoices already sent were filed as they were sent.
Availability โ€” on both services, checked 23 September 2026

All five behaviours in this subsection are on GoRoute's production service and on its test service. They reached test on 22 September 2026 and production later the same day, on 22 September 2026 at 22:05 UTC, when production was upgraded to the build test was already running. The rest of this page, including the retry table below, describes both services.

If you read this page before 23 September 2026 it said these five were on test only. That was true when it was written and it is no longer true: your production invoices are checked in all five ways described above.

If nothing at all is moving, check the connection before the invoicesโ€‹

There is one case where the reasons on the screen are a distraction. If nothing is syncing โ€” no new invoice has gone out for hours, and the reasons listed against the parked ones are the same ones that were there yesterday โ€” the likely cause is that GoRoute can no longer log in to your accounting system. The credential it holds may have been changed, revoked or have stopped working.

When that happens, the parked list does not change, because it only changes on a poll that reached your system. So the list goes stale and keeps showing reasons that can no longer be acted on. Correcting those invoices will not start anything moving.

What to do: press Test on the connector. If it fails, reconnect with a fresh credential from your accounting system and press Connect. Sending resumes from there โ€” GoRoute does not make you wait, and the parked invoices are tried again on the next cycle once it can get in.

We describe only what GoRoute does here. Why a credential stopped working is a question for your accounting system's own documentation and support.

Getting a parked invoice moving againโ€‹

Odoo. Correct the cause in Odoo. That is all you have to do โ€” the connector comes back for parked invoices on its own and tries them again.

It fetches each parked invoice by name, so it does not matter where the fix was made. A correction on the invoice, on the tax, on the customer record or on your company record all work the same way. This matters most in Oman, where adding a reason code is done on the tax rather than on the invoice: the correct fix leaves the invoice itself untouched, and the invoice is still released.

How often it tries again depends on whose fault it was:

What went wrongWhen it is tried again
Something you fix in Odoo โ€” a missing reason code, incomplete customer detailsAt most about every fifteen minutes
Something GoRoute has to fixEvery polling cycle, until it works

This is how the Odoo connector behaves on GoRoute's production service as well as on the test service.

Saving the invoice in Odoo is no longer required. It still works, and it is still the fastest way to bring one specific invoice back straight away rather than waiting for the next attempt, so use it if you are in a hurry. It is a shortcut now, not the fix.

You may also see a note in Odoo. When an invoice parks, the connector tries to post the rejection reason onto that invoice in Odoo, so the person who raised it can see why it did not go. Treat this as a convenience rather than a guarantee: Odoo can refuse the note, and an invoice that parked before this was introduced never had one written. The connector's last_error and needs_attention fields are the reliable place to read what is held and why โ€” see the Connector API.

Zoho Books. There is no automatic retry. The poller works forward through a modified-time cursor and does not come back for a parked invoice, so setting the missing code is not enough on its own: open each parked invoice in Zoho Books and save it, so that its modified time moves and the next poll picks it up. We have raised this with engineering.

What comes backโ€‹

A connector is not one-way. Once a document has been sent and the network has responded, the outcome is written back where your accountants already work โ€” so the accounting system, not just the GoRoute portal, shows what happened to each invoice.

Next Stepsโ€‹