Connector API
A connector is one integration your organization runs โ an ERP or accounts-payable system that exchanges documents with GoRoute. The connector registry lets you register the connectors you operate and read their health.
There is exactly one row per (organization, connector type). Registering the same type twice updates the existing row rather than creating a second one.
connectors:manage permission โ most API keys do not have itEvery endpoint on this page is gated by connectors:manage, with one exception:
held invoices is gated by invoices:read instead, so that the person who
has to correct a stuck invoice can read it without being an administrator.
For an API key, that permission is granted only by the admin scope (and the legacy
all scope). The read, write, send and integrate scopes do not include it, so a
key with ordinary integration scopes receives HTTP 403 on every call here.
For a signed-in user, the org_owner, org_admin and developer roles hold it.
If you are getting 403 from these endpoints with a working API key, the key's scope is the reason โ not the key, and not your organization.
A refused call returns HTTP 403 with this body:
{
"error": "forbidden",
"error_code": "INSUFFICIENT_PERMISSION",
"message": "This action requires the 'connectors:manage' permission."
}
Connector typesโ
connector_type must be one of these twelve values. Any other value is rejected with
HTTP 400.
| Value | System |
|---|---|
xero | Xero |
quickbooks | QuickBooks |
odoo | Odoo |
zoho | Zoho Books |
bol | bol.com marketplace (Retailer API) |
amazon | Amazon seller account (Selling Partner API) |
zuora | Zuora |
oracle_ebs | Oracle E-Business Suite |
tally | Tally |
sap | SAP |
api | Direct API integration |
custom | Anything else you operate |
List your connectorsโ
GET /api/v1/connectors
Returns the connectors belonging to the calling organization, ordered by
connector_type, each with its derived health.
curl https://app.goroute.ai/peppol-api/api/v1/connectors \
-H "X-API-Key: your_api_key"
The response is an object with a single connectors array:
{
"connectors": [
{
"id": "3f7c1b9e-2a44-4d31-9d0e-8b1f2c6a55d1",
"connector_type": "odoo",
"status": "active",
"display_name": "Odoo Online (production)",
"config": {
"environment": "production"
},
"api_key_id": "c1d2e3f4-5678-4abc-9def-0123456789ab",
"api_key_prefix": "pk_live_ab12",
"last_seen_at": "2026-08-16T09:12:44+00:00",
"last_document_at": "2026-08-16T09:12:44+00:00",
"health_status": "healthy",
"last_error": null,
"needs_attention": [],
"created_at": "2026-05-02T11:00:00+00:00",
"updated_at": "2026-08-16T09:12:44+00:00",
"poll_interval_seconds": 300
}
]
}
Register or update a connectorโ
POST /api/v1/connectors
POST is idempotent on (organization, connector type). Sending it for a
connector_type you already have updates that connector โ it does not create a second
one, and it does not return a conflict. If you expected create semantics, this will
surprise you.
Only the top-level fields you send are changed; omitted top-level fields are left as they
are. config is the exception โ on POST, if you send it at all, it replaces the stored
object entirely. To change one setting, use PATCH, which merges. See
how config is written.
| Field | Required | Notes |
|---|---|---|
connector_type | Yes | One of the ten values above. Anything else is HTTP 400. |
display_name | No | Free text, maximum 120 characters. |
config | No | Settings object. On POST, replaces the stored object in full โ see below. |
api_key_id | No | An API key belonging to your organization. A key from another organization is HTTP 400. |
curl -X POST https://app.goroute.ai/peppol-api/api/v1/connectors \
-H "X-API-Key: your_api_key" \
-H "Content-Type: application/json" \
-d '{
"connector_type": "odoo",
"display_name": "Odoo Online (production)",
"config": {"environment": "production"}
}'
The response is the single connector object, in the same shape as the list entries.
Response fieldsโ
| Field | Meaning |
|---|---|
id | Connector UUID. |
connector_type | One of the ten values above. |
status | configured, active or disabled. |
display_name | Your label, or null. |
config | Settings object. Returned to callers in full โ see the warnings below. |
api_key_id / api_key_prefix | The API key tagged to this connector, if any. Only the prefix is shown. |
last_seen_at | Last time traffic was attributed to this connector. |
last_document_at | Last document handled. |
health_status | Derived at read time โ see below. Not stored. |
last_error | One line of text, usually summarising every invoice currently parked and who has to fix them โ but not always. Read it together with the next field, and see below. Null when there is nothing to report. |
last_error_is_held_summary | true when last_error only restates the parked list; false when it is something else, such as a connection that could not be made โ see last_error is not always about invoices. On both the production and test services since 22 September 2026; the availability note in that section has the dates. |
needs_attention | Sorted list of parked invoice names, and nothing else about them โ see below. |
held_invoices | The same parked invoices, but each with its reason and who has to fix it โ see Held invoices, with reasons. On both the production and test services since 22 September 2026; the availability note in that section has the dates. |
created_at / updated_at | Timestamps. |
poll_interval_seconds | The cadence the poller actually runs at, reported rather than assumed. |
needs_attention โ the invoices a person has to unstickโ
needs_attention lists invoices that were skipped because only a person can fix them โ
for example a rejected invoice that would otherwise block the queue. It is the sorted list
of keys held in the connector's parked-invoice register.
Both pollers write that register: Odoo and Zoho Books. It is still not a universal
mechanism โ a connector type nobody polls has nothing to park, so it will always show an
empty needs_attention, and for those types an empty list should not be read as "nothing
is wrong".
What to do about a parked invoice, and the difference between Odoo and Zoho Books in how it comes back, is on Connectors โ When an invoice cannot be sent.
last_error โ a summary of what is parked, and whose fault it isโ
last_error is not the last thing that went wrong. It is rebuilt on every poll from the
whole parked register, so it describes everything currently parked, not only the most
recent one. Both pollers, Odoo and Zoho Books, build it the same way.
The part worth reading is that it says who can fix it:
- A fault the customer has to fix reads "
<invoice>was not accepted:<reason>Correct it in your accounting system and it will be picked up automatically." - A fault GoRoute has to fix reads "
<invoice>could not be processed because of an error on GoRoute's side:<reason>We retry it automatically โ no action is needed in Odoo." That sentence names Odoo whichever connector it came from, including Zoho Books; read it as "no action is needed in your accounting system". - With several parked at once it names up to four invoices, counts the rest, and โ when the two kinds are mixed โ says how many of them failed on GoRoute's side and retry automatically.
Do not tell a customer to go and correct invoices that the line says are waiting on us.
The line is capped at 500 characters, so a long reason behind many parked invoices is cut
short; needs_attention always has the full list of names.
last_error is still not a permanent record. Once the register is empty and a cycle
passes with no failure, both pollers set it back to null and reset the error count. The
record that survives is needs_attention: an invoice stays listed there until it is either
sent or superseded.
last_error is not always about invoicesโ
Everything above describes a poll that reached your accounting system. A poll that could
not log in never gets as far as the parked register, so last_error then holds the
connection failure instead โ text like "Odoo rejected the credentials" rather than a list
of invoices.
That is the trap: the line still reads like an invoice problem, and a person who treats it as one can spend an afternoon correcting invoices that were never the cause.
last_error_is_held_summary is the field that tells the two apart.
last_error | last_error_is_held_summary | What it means |
|---|---|---|
| Set | true | The line restates the parked list. Correcting those invoices is the remedy. |
| Set | false | The line is about something else โ most often that GoRoute could not reach or log in to your system. Correcting invoices will not help. Fix the connection first. |
null | false | Nothing to report. |
The field is deliberately cautious: GoRoute answers true only when last_error is
character-for-character the summary it builds from the parked register. Anything it cannot be
sure about answers false, which shows you the message rather than hiding it. So a false
on a connector that is otherwise fine costs you a second look; it never costs you a hidden
outage.
last_error_is_held_summary is on GoRoute's production service and on its test
service. It reached test on 20 September 2026 and production on 22 September 2026, when
production was upgraded to the build test was already running.
You can still check rather than trust this page: call GET /api/v1/connectors on the service
you use and look for last_error_is_held_summary on a connector.
Run a sync now, and read what it didโ
POST /api/v1/connectors/{connector_id}/sync
Runs one poll cycle immediately instead of waiting for the schedule, and returns what that
cycle did. It needs connectors:manage, like the rest of this page, and it returns HTTP 400
if the connector is not connected.
| Field | Meaning |
|---|---|
fetched | Invoices read from your accounting system this cycle. |
submitted | Invoices sent onward to the network. |
failed | Invoices that could not be submitted. |
unreachable | 1 when the cycle could not reach your system at all, otherwise 0. |
last_error | The connector's last_error after the cycle โ read it with last_error_is_held_summary above. |
connector | The whole connector object, in the same shape as a list entry. |
failed: 0 does not mean the sync workedfailed counts invoices, so a cycle that never logged in has no invoices to count and
comes back {"fetched": 0, "submitted": 0, "failed": 0} โ byte for byte what an idle,
healthy connector returns. That is exactly what a customer saw for four hours on
20 September 2026 while nothing at all was being sent.
unreachable is the field that separates the two. unreachable: 1 with zeros everywhere
else means the cycle never got in, not that there was nothing new.
A cycle counts as unreachable in four situations: the stored credentials cannot be read, the connector is sitting out a wait after earlier failures (below), the stored address is one GoRoute refuses to dial, or the connection itself failed.
unreachable is on GoRoute's production service and on its test service. It reached
test on 20 September 2026 and production on 22 September 2026, when production was
upgraded to the build test was already running. Call the endpoint on your own service and see
whether the key comes back. Zoho Books connectors return 0 here; the counter is written by
the Odoo poll.
After repeated failures, GoRoute waits before dialling againโ
A connector whose credentials have stopped working used to be re-dialled on every cycle, for as long as it stayed broken. It now backs off: after the first failed connection GoRoute waits 30 seconds before trying again, and doubles that wait with each further failure, up to a ceiling of ten minutes.
What an operator needs to know about it is short:
- The ordinary recovery never waits. Pressing Connect or Test โ or calling
POST /api/v1/connectors/{connector_id}/connector.../testโ clears the wait immediately, because both of those prove the connection before they return. Disconnecting clears it too. So pasting a working credential and pressing Connect takes effect at once. - Connect and Test also clear
last_errorand the error count, so the health badge turns from red as soon as the connection is proved, rather than at the next successful poll. - While the wait is running, cycles still report
unreachable: 1. A connector is no healthier for not having been dialled, so a skipped cycle is reported rather than shown as idle. - Anything GoRoute is unsure about resolves towards dialling, not towards waiting. A connector that has been fixed is never held down by a wait it cannot see.
The wait is on GoRoute's production service and on its test service. It reached test
on 20 September 2026 and production on 22 September 2026, when production was upgraded to
the build test was already running. There is no field that reports it directly; the way to
tell is that connect_backoff appears in the connector's config โ see the table under
how config is written.
Held invoices, with reasonsโ
GET /api/v1/connectors/held-invoices
One entry per invoice your connectors are currently holding, across all of your
connectors, each carrying the reason it was held. This is the read side of the same register
that needs_attention lists by name: the names told you which invoices were stuck, and
never why.
This call, and the held_invoices field on GET /api/v1/connectors, are on GoRoute's
production service (app.goroute.ai) and on its test service (app-test.goroute.ai).
They reached test on 19 September 2026 and production on 22 September 2026, when
production was upgraded to the build test was already running.
On 19 September 2026 production still answered 404 here; on 23 September 2026 it answers
401, the same as GET /api/v1/connectors on that host, which is the answer a real address
gives to a caller with no key.
Rather than trust this page, call the address on the service you use. A 404 would mean your
service does not have it: fall back to needs_attention and last_error, which are on both.
Permissionโ
This one call needs invoices:read, not the connectors:manage that the rest of this
page needs. That is deliberate. The connector page is an administrator's page, but the
person who can actually correct a held invoice works in finance โ and a held invoice never
becomes a transaction or a draft, so without this call there is no screen on which that
person can learn that an invoice they raised went nowhere, or why.
- Signed-in users: every organization role holds
invoices:readโorg_owner,org_admin,finance,operations,developerandviewer. - API keys: the
read,write,adminand legacyallscopes include it. Thesendandintegratescopes do not, so a key with only those scopes gets HTTP 403.
curl https://app.goroute.ai/peppol-api/api/v1/connectors/held-invoices \
-H "X-API-Key: your_api_key"
Responseโ
{
"held_invoices": [
{
"name": "INV/2026/00042",
"reason": "VATEX-OM-01 is an exemption code but the tax is zero-rated.",
"kind": "customer",
"first_failed_at": "2026-09-18T07:14:02.481913+00:00",
"last_tried_at": "2026-09-19T06:02:11.903244+00:00",
"document": "invoice",
"customer": "Muscat Horizon Trading LLC",
"invoice_date": "2026-09-17",
"amount_total": 819.0,
"currency": "OMR",
"source_url": "https://mycompany.odoo.com/web#id=1042&model=account.move&view_type=form",
"source_note": "posted",
"connector_id": "3f7c1b9e-2a44-4d31-9d0e-8b1f2c6a55d1",
"connector_type": "odoo"
}
],
"total": 1
}
Entries come back oldest failure first, so the invoice that has been stuck longest is at
the top. total is the number of entries in the list.
| Field | Meaning |
|---|---|
name | The invoice number as your accounting system holds it. The one field that is always present. |
reason | Why it was held, in the words the sender was given. null when the entry was written by a release that did not record one โ reported as missing rather than guessed at. |
kind | Who has to fix it. customer โ the remedy is in your accounting system, and the invoice is re-presented by itself once you correct it. platform โ GoRoute's code failed, and nothing you change will help. Entries written before this field existed read customer. |
first_failed_at | When it was first held. This is the "how long has this been stuck" field; it is not overwritten on a retry. |
last_tried_at | When it was last attempted. |
document | invoice or credit_note. |
customer | The buyer named on it. |
invoice_date | The invoice date. |
amount_total | The gross total, as a number. |
currency | The currency as your accounting system names it. |
source_url | A link that opens the invoice where it was written. Odoo only, and only when the stored address plainly begins https:// or http://; otherwise null. |
source_note | Whether the reason also reached the invoice in the source system: posted, failed, or null when that was never attempted. failed matters โ it means this list is the only place the reason can be read. |
connector_id / connector_type | Which of your connectors is holding it. |
Fields other than name may be absent on older entries: the register has grown a field at a
time since August 2026, and rows written by earlier releases are still in it.
What to do about a held invoice, and how Odoo and Zoho Books differ in bringing it back, is on Connectors โ When an invoice cannot be sent.
Derived healthโ
health_status is computed when you read it, from status and the freshness
timestamps. It is not a stored column, so it cannot drift out of sync.
| Bucket | Condition |
|---|---|
disabled | status is disabled. |
error | Error count above zero. |
unknown | Never seen โ just registered, or no attributed traffic yet. |
healthy | Seen within the last 24 hours. |
degraded | Seen within 7 days, but not in the last 24 hours. |
stale | Not seen for more than 7 days. |
The thresholds are deliberately generous: a connector that is idle but fine should not
alarm. Show last_seen_at next to the badge so a person can judge borderline cases.
config is not for secretsโ
config holds settings rather than credentials โ environment, external company or tenant
id, webhook source and similar. It is serialised back on every read of this endpoint. Do
not put credentials in it.
Credentials for connectors that must be polled are sent separately, through the
connector connect endpoint. They are stored encrypted and are never returned by the
API.
How config is written: POST replaces, PATCH mergesโ
GoRoute keeps its own working state in config, alongside your settings (the table below).
So how a call writes config decides whether a well-formed call that sets one setting also
destroys state you never knew was there.
PATCH /api/v1/connectors/{connector_id}merges. Each key you send is set to the value you send, replacing that key's whole value. A key you send asnullis removed. Every key you do not send is kept as it is. So aPATCHthat sets one key changes only that key.POST /api/v1/connectorsreplaces. Registering a connector type you already have, withconfigin the body, assigns theconfigyou send over the stored object. Anything that was inconfigand is not in your request is gone. UsePATCHto change a setting.
PATCH used to replace tooUntil 24 September 2026, PATCH assigned config over the stored object exactly as POST
does, and these pages told you to read the whole object and send it back. That still works
and gives the same result, because every key you send back is set to what you read. Two
things differ:
- To remove a key, send it as
null. Leaving it out keeps it. {"config": {}}changes nothing. It used to clear the whole object.
The merge happens in the database, so a poll that records something into config while
your call is on its way keeps what it recorded. It works the other way too: a poll cycle
(Odoo, Zoho Books or bol.) writes back only its own keys, merged the same way, so a PATCH
that lands while a cycle is running is not put back by it. For Zoho Books this holds from
the build that shipped it on 24 September 2026; before it, a Zoho cycle that was running
when your PATCH arrived could restore the value it had loaded.
Check which behaviour your service has before relying on a single-key PATCH: the change
reaches a service when it is deployed there. Send a PATCH with one harmless key to a
connector that holds parked_invoices or another GoRoute key, and read config back. If the
other keys are still there, you have the merge.
Keys GoRoute writes into config itself:
| Key | What it is | What losing it costs |
|---|---|---|
parked_invoices | The register behind needs_attention | Every stuck invoice stops being reported, and nothing brings it back |
pending_oauth | An in-flight Zoho authorisation, held between the OAuth callback and the connect call | The authorisation is abandoned; the customer has to start the Zoho connection again |
smp_hold | A hold set when the sender is not registered on the network | The hold is lost and sending is retried against a registration that is not there |
cn_cursor | How far the Zoho credit-note poll has read | Credit notes are re-read from the beginning |
evidence_blocked | A flag on the Zoho evidence write-back | The write-back state is reset |
sync_cursor_handled | The Odoo invoices already handled around the poll's bookmark | Invoices around the bookmark are read again, and may be offered a second time |
connect_backoff | How many connections in a row have failed, and when the last one did โ see the wait | The wait is forgotten and dialling restarts at full rate. Harmless in itself, and a disconnect clears it deliberately |
reference_fields | The one key in this table you write, not GoRoute โ which of your own Odoo fields carry the contract, project and PO references, see below | Your mapping is gone and invoices go out with the sales order alone, silently. It has an endpoint of its own, which checks the mapping |
sync_cursor_handled, connect_backoff and reference_fields are on both the production
and test services. The first two reached test on 20 September 2026 and reference_fields on
22 September 2026; all three reached production on 22 September 2026, when production was
upgraded to the build test was already running. Read your own connector's config to see
which keys your service writes; the rule below does not depend on knowing them all, which is
the point of it.
Changing a settingโ
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_export_zero_rate_code": "VATZR-OM-10"}}'
To remove a setting, send it as null:
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_export_zero_rate_code": null}}'
Never send config to POST /api/v1/connectors for a connector you already have, and never
send one of the keys in the table above in a PATCH unless you mean to overwrite GoRoute's
own record: a PATCH of {"config": {"parked_invoices": {}}} still empties the list of stuck
invoices.
Odoo: which of your fields carry the contract, project and PO referencesโ
The Odoo connector sends each invoice's sales order (Odoo's Source Document,
invoice_origin) as the seller's order reference, with no setting. Odoo has no standard
field for a contract, a project or the customer's purchase order on an invoice,
so if you keep one in a field of your own you can tell the connector which it is:
| Target | On the e-invoice | Example field |
|---|---|---|
contract_reference | Contract reference (IBT-012) | contract_id |
project_reference | Project reference (IBT-011) | x_studio_project |
order_reference | Purchase order reference (IBT-013) | x_customer_po |
Set it with its own endpoint, not with PATCH โ this one changes the one setting and
leaves the rest of config exactly as it is:
PUT /api/v1/connectors/{connector_id}/reference-fields
curl -X PUT https://app.goroute.ai/peppol-api/api/v1/connectors/CONNECTOR_ID/reference-fields \
-H "X-API-Key: your_api_key" \
-H "Content-Type: application/json" \
-d '{"contract_reference": "contract_id"}'
- The body names the field on Odoo's invoice (
account.move), lower case. Only the three targets above are accepted; anything else is refused with422. Send{}to remove the setting. - The field must be a text field or a link to another record โ a link is sent as the record's name, as Odoo shows it. Any other kind of field, a field that does not exist, or one the connector's Odoo user cannot read is skipped, with a warning in GoRoute's log. The invoice is still sent, without that reference. A setting can never hold an invoice.
- Whatever you map is written into the e-invoice and filed with the Tax Authority, so a
field that is a secret or names a person is refused with
400, and never read even if it is stored some other way: the portal'saccess_tokenandaccess_url, any field whose name containstoken,passwordorsecret, and fields about people โpartner_id,commercial_partner_id,invoice_user_id,create_uid, anything named for an email, phone, bank or contact. When the connector reads the field it also checks what Odoo says of it, whatever the field is called:- A link is sent as the name of the record it links to, so a link to a person, a bank
account, a message, a secret or an Odoo setting (
res.partner,res.users,res.partner.bank,hr.employee,resource.resource,website.visitor, โฆ) is skipped โ however Odoo fills the link in. - A text field is sent as itself, so it must hold the invoice's own text. A text field Odoo fills in from somewhere else is skipped: a related field (say one showing the salesperson's login) or a computed one. Map the field it comes from instead โ a link is sent as its record's name.
- A link is sent as the name of the record it links to, so a link to a person, a bank
account, a message, a secret or an Odoo setting (
- It applies from the next poll on. An invoice already sent keeps what it was sent with.
- Only the Odoo connector reads it (
400on any other type). - It is safe to set while a poll is running: the poller writes back only its own
bookkeeping, never the whole
config, so it cannot put the old value back.
This call is on GoRoute's production service and on its test service. It 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.
Test it yourself rather than trusting this page: send the PUT above to the service you use,
with your API key. Anything other than 405 Method Not Allowed means the service has the
call and is answering you about your key, your connector id or your body โ a 401 means the
key, a 404 means the connector id, a 400 or 422 means the body. A 405 would mean
your service does not have this call, because that is the same answer it gives for an address
that does not exist at all. Both services answered 401 when this page was last checked.
Lifecycle endpointsโ
These are gated by the same connectors:manage permission. Only pull connectors
(odoo, sap, zoho, bol, amazon) have credentials to connect, so /connect returns 400
on a push connector โ see Connectors.
PATCH /api/v1/connectors/{connector_id}โ update a connectorDELETE /api/v1/connectors/{connector_id}โ remove itPOST /api/v1/connectors/{connector_id}/connectโ hand over credentialsPOST /api/v1/connectors/{connector_id}/disconnectโ forget themPOST /api/v1/connectors/{connector_id}/testโ confirm we can reach the systemPOST /api/v1/connectors/{connector_id}/syncโ pull now rather than wait; what it returns is under Run a sync nowPOST /api/v1/connectors/{connector_id}/documents/{doc_id}/sendโ SAP, send one document
Zoho Books endpointsโ
Zoho connects over OAuth, so it carries its own endpoints alongside the shared ones:
| Endpoint | Purpose |
|---|---|
GET /api/v1/connectors/zoho/oauth/config | Whether the one-click button can be offered. Returns {"configured": true|false} and no secrets |
POST /api/v1/connectors/zoho/oauth/start | Begin the one-click flow; returns the Zoho authorize URL |
GET /api/v1/connectors/zoho/oauth/callback | Where Zoho redirects back. Authenticated by a signed state token, not an API key |
POST /api/v1/connectors/{connector_id}/zoho/organizations | List the Zoho Books organizations the grant can see, so you can pick one |
POST /api/v1/connectors/zoho/exchange | Self-Client route: exchange a grant token when you are not using the one-click flow |
GET /api/v1/connectors/{connector_id}/zoho/webhook-url | The URL to register in Zoho so an invoice reaches the network without waiting for the next poll. See below |
POST /api/v1/connectors/{connector_id}/zoho/webhook-url/rotate | Issue a new URL and stop every URL issued before. See below |
POST /api/v1/connectors/zoho/webhook/{connector_id} | The doorbell Zoho calls. Token-gated; the body is ignored and the poller re-reads from Zoho |
Full request and response schemas are in the API Reference.
Reading and rotating the Zoho webhook URLโ
Both calls answer with the same two fields:
{
"url": "https://app.goroute.ai/peppol-api/api/v1/connectors/zoho/webhook/<connector_id>?token=<token>",
"rotated": true
}
url is the address to paste into the Zoho Books workflow rule. rotated says whether this
connector's URL has ever been rotated: GET reports false until the first rotation and true
afterwards, and the rotate call always answers true.
The URL is a credential. The token is in it, and whoever holds the URL can make GoRoute pull from Zoho and send for that connector, with nothing else to prove who they are. Treat it as you would a password: do not put it in a ticket, a chat message or a screenshot.
Only the organisation itself can read or rotate it. Both calls require the
connectors:manage permission, and both additionally refuse any session that is acting on behalf
of another organisation โ a partner working in your account, however much access they have. The
refusal is 403 with error_code ACT_AS_READ_ONLY. The code name says read-only, but the check
does not look at the access level: every act-as session is refused, not only a read-only one.
GET is the method that makes this matter. Without the extra check, a partner who can see your
connectors at all could read a live credential of an account they may only look at.
What else is refused. A connector that is not a Zoho connector gets 400 with the message
Not a Zoho connector.; a connector id that does not belong to your organisation gets
404 "Connector not found".
After a rotation, update Zoho. Every URL handed out before the rotation stops working the moment the rotate call returns, so the Zoho Books workflow rule has to be updated with the new URL. Until it is, invoices still reach GoRoute on the regular poll โ they simply stop arriving instantly. Rotate when a URL may have been seen by someone who should not have had it.
bol.com: the connect bodyโ
bol.com adds no endpoints of its own; it uses the shared lifecycle above. POST /api/v1/connectors/{connector_id}/connect on a bol connector takes:
| Field | Purpose |
|---|---|
client_id, client_secret | Retailer API credentials from the bol seller dashboard (Settings โ Services โ API settings). Proven against bol before anything is saved; stored encrypted, never returned |
demo | true points the connector at bol's demo environment (fixed sample orders; uploads are accepted and discarded). Default false |
vat_rate | The VAT rate the shop charges, as a percentage. bol gives prices including VAT and no rate. Default 21 |
invoice_number_prefix | What precedes the bol order id in the invoice number. Default BOL- |
config then carries environment, client_id_prefix, vat_rate and
invoice_number_prefix, plus the poller's own registers (bol_awaiting_shipment,
bol_uploads_pending, bol_consumer_requests, bol_cursor_handled). Those registers
are why the rate and the prefix are not changed through config: to change them,
disconnect and connect again rather than PATCH the config
(see how config is written). What the poller does with an order is described under
bol.com.
Amazon: one-click authorisation and the connect bodyโ
Amazon connects in one click: the portal's Connect with Amazon button asks the API for the
consent URL (the amazon/oauth/start action under the connectors resource, taking the country
whose Seller Central the seller signs in to, or โ when the flow began on the Selling Partner
Appstore โ Amazon's amazon_callback_uri and amazon_state to be sent back), the seller
authorises GoRoute on Amazon's page, and Amazon redirects to the amazon/oauth/callback action
with state, selling_partner_id and spapi_oauth_code. The signed state is bound to the
organisation that started the flow; the code is exchanged for the refresh token, the seller's
stores are discovered and the connector goes active. amazon/oauth/config says whether the
platform's application is registered on the service and whether it is still a draft (the consent
URL then carries version=beta). All four authorisation addresses are now in the API reference:
| Call | What it is for | Who may call it |
|---|---|---|
GET /api/v1/connectors/amazon/oauth/config | Whether the platform's Amazon application is registered on this service, whether it is still a draft, which Seller Central countries are offered, and the redirect address. No secrets are returned | connectors:manage |
POST /api/v1/connectors/amazon/oauth/start | The URL to send the browser to. Answers 409 when the platform's application is not registered on this service โ paste a refresh token instead | connectors:manage |
GET /api/v1/connectors/amazon/oauth/login | The application's OAuth Login URI, used when the flow begins on the Selling Partner Appstore rather than in GoRoute. Redirects the seller into the portal, which then calls amazon/oauth/start | Nobody signs in here โ it is a browser redirect Amazon opens |
GET /api/v1/connectors/amazon/oauth/callback | Where Amazon returns with state, selling_partner_id and spapi_oauth_code | Also a browser redirect; the signed state is the authentication |
The fallback is the shared lifecycle: POST /api/v1/connectors/{connector_id}/connect on an
amazon connector takes:
| Field | Purpose |
|---|---|
refresh_token | The LWA refresh token from a self-authorisation in Seller Central (Apps & Services โ Manage Your Apps). Proven in three named stages โ Amazon's sign-in (lwa), the API (spapi), a VCS store (marketplaces) โ before anything is saved; stored encrypted, never returned |
region | eu (default), na or fe โ the SP-API region of the seller's stores |
sandbox | true points the connector at Amazon's hosted sandbox (static answers; nothing reaches a buyer). Default false |
invoice_number_prefix | What precedes Amazon's shipment id in the invoice number. Default AMZ- |
A failed connect or test answers 400 with {"error", "stage", "message"} so the page can
say which stage refused. config then carries environment, region, authorised_via
(oauth or pasted), selling_partner_id, marketplaces (the VCS stores enabled) and
all_marketplaces, plus the poller's own registers (amazon_orders_seen,
amazon_uploads_pending, amazon_vidr_report, amazon_cursor_handled, amazon_status) โ
written back under the same merge as the other pollers. The seller's confirmation that VCS is set
to I will upload my own invoices is a merged PATCH of config.amazon_vcs_upload_acknowledged
(true, with amazon_vcs_upload_acknowledged_at); there is no endpoint of its own. What the
poller does with a shipment is described under Amazon.
Amazon readinessโ
GET /api/v1/connectors/{connector_id}/amazon/readiness
needs connectors:manage. It is the call behind the readiness list on the connector page, and it
is the only place an integrator can read the same answer.
The call reads what GoRoute already holds โ the connector's config, the organisation's own seller record and the SMP. It does not contact Amazon, so it is cheap and safe to poll, and a green answer says GoRoute is ready rather than that Amazon has just confirmed anything.
Two refusals:
400withNot an Amazon connector.โ the connector exists and is yours, but it is some other kind.404withConnector not foundโ the id is unknown, or it belongs to another organisation. The two cases are deliberately indistinguishable.
The generated reference page for this call shows no response fields. That is a gap in the published API description, not in the call: the handler returns a plain object the description cannot express. The fields are these.
| Field | What it holds |
|---|---|
ready | true when every check that counts has passed โ see the warning below |
checks | The eight checks, in the order the panel shows them. Each is an object with id, ok, label and detail |
awaiting | How many Amazon orders the poller has seen and not yet turned into invoices |
uploads_pending | How many issued documents are still waiting to be uploaded to Amazon, or waiting for Amazon's verdict on a feed |
late | The keys of the pending uploads that have passed Amazon's business-day deadline. Empty when nothing is late |
last_cycle_at | When the poller last ran for this connector, or null if it has not run yet |
held_invoices | Invoices this connector is holding, each with its reason โ the same entries as on GET /api/v1/connectors, described under Held invoices, with reasons |
The eight checks, by id:
id | label | Passes when |
|---|---|---|
connected | Amazon authorised | The connector is active and holds stored credentials. detail then names how it was authorised (oauth or a pasted token) and which environment it points at; otherwise it tells the seller to connect or paste a refresh token |
marketplaces | Stores found | At least one VCS store was discovered. detail lists their country codes |
seller_identity | Seller identity complete | Your organisation has a seller record and nothing is missing from it. detail names the missing fields, which are filled in under Settings โ Company |
seller_registered | Seller registered on the network | The seller's own Peppol identifier is registered on GoRoute's SMP, or โ failing that โ still resolves on the network |
vcs_acknowledged | VCS set to "I will upload my own invoices" | config.amazon_vcs_upload_acknowledged is set. Amazon offers no API for this setting, so it is a confirmation the seller gives after changing it in Seller Central, not something GoRoute can read |
buyer_data | Buyer details released by Amazon | The poller has recorded no problem getting buyer details. This check does not affect ready |
vidr | VAT Invoice Data Report | The last recorded state of the report is good, or none has been requested yet |
on_time | Uploads within Amazon's business day | No pending upload has passed its deadline โ the same list as late |
ready can be true while a check is failingready is computed from seven of the eight checks: buyer_data is excluded. So a connector
whose buyer details Amazon has not released still reports ready: true, while the panel shows that
check in red. If you are gating anything on readiness, read checks and decide for yourself which
failures matter to you rather than trusting ready alone.
Relatedโ
- Organizations โ the tenant a connector belongs to
- Authentication โ API keys and scopes
- Connectors โ pull vs push, and the per-system setup