Store Connect Reference
How a store app gets an API key for a merchant's GoRoute account without asking the merchant to create one, copy it and paste it in. The merchant presses a button in their store, signs in to GoRoute, approves, and the store's server receives a key that is already scoped for what the store needs.
This page is the field-by-field reference for the three calls. If you are a merchant installing one of the apps, you do not need any of this โ see Store apps instead. It is written for whoever is building a store integration.
There are four calls in the Connect group. Three of them make up the connection flow and are
described here. The fourth, PUT /api/v1/connect/profile, is how a connected store keeps the
organisation's business details in step afterwards, and it is described under
Organizations.
The flowโ
- The store sends the merchant to GoRoute. The app opens GoRoute's hosted consent page,
/app/connect.html, passing the store's own callback URL and a nonce of the app's choosing, which travels asstate. - The merchant signs in and approves. GoRoute shows who is about to connect โ the call
behind that screen is
GET /api/v1/connect/context. On approval the portal callsPOST /api/v1/connect/authorize, which records what was approved under a one-time code and returns it. The browser is then sent to the store's callback URL withcodeandstate. - The store's server exchanges the code.
POST /api/v1/connect/exchangetakes the code and returns a newly minted API key, the organisation's identity, the Peppol participant its invoices will issue under, and the webhook event names the store may subscribe to.
Step 3 is a server-to-server call and needs no credential of its own: the code is the credential. Why that is safe sets out the protections, because an unauthenticated call that mints an API key deserves the explanation rather than your trust.
Check state yourself when the browser comes back. GoRoute returns the value you sent,
unchanged, and does nothing else with it; it is there so the store can tell its own request
apart from someone else's.
Sourcesโ
source names the kind of store, and decides both the URL rules and nothing else โ the scopes
are the same for all three.
source | Label used in the key's name | Where the code may be returned |
|---|---|---|
woocommerce | WooCommerce | The store's own host โ a WooCommerce shop hosts its own callback |
shopify | Shopify | A GoRoute-hosted app host (see below). The store itself must be a .myshopify.com domain |
wix | Wix | A GoRoute-hosted app host (see below) |
A Shopify or Wix store cannot host a callback of its own, so for those two sources the callback
must be one of GoRoute's own app hosts: connect.goroute.ai or connect-test.goroute.ai for
both, and additionally shopify.goroute.ai or shopify-test.goroute.ai for Shopify. A
WooCommerce store is the other way round: the callback must be on the same hostname as the
store URL, so a code issued for one shop cannot be redirected to another.
Any other value of source is refused with 400 and the message
Unknown source 'x'.
URL rules that apply to every sourceโ
Both store_url and callback_url must be absolute http or https URLs with a hostname.
https is required, with one exception: localhost and 127.0.0.1 may use plain http, so a
developer can run the flow against a local store.
GET /api/v1/connect/contextโ
Who is about to connect. This is what the consent screen reads, so that the merchant is shown the organisation they are about to attach the store to, and the seller identity their invoices will carry.
Requires the apikeys:manage permission โ connecting a store mints a key, so it is the same
permission as managing keys by hand.
| Query parameter | Description |
|---|---|
country | Optional. The store's country as an ISO 3166-1 alpha-2 code, for example BE. Picks the seller participant of that country. Omit it and the best participant of any country is returned. |
{
"organization": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Nordlys Trading",
"legal_name": "Nordlys Trading LLC",
"slug": "nordlys-trading",
"country": "OM"
},
"country": "BE",
"seller": null,
"identifier_countries": ["OM"],
"sources": {
"woocommerce": "WooCommerce",
"shopify": "Shopify",
"wix": "Wix"
}
}
| Field | Description |
|---|---|
organization | The signed-in organisation: id, name, legal_name, slug and country. |
country | The store country as GoRoute understood it, or null when what you sent was not a usable country code. Read it back rather than assuming your input was accepted. |
seller | The participant this store's invoices would issue under, or null when the organisation has no usable participant for that country. |
identifier_countries | Every country the organisation already holds an identifier for, sorted. This is what lets the consent page say "your identifier is Omani" when the store is Belgian. |
sources | The source values this build accepts, and their labels. Read this rather than hard-coding the three names. |
The seller object, when it is not null:
| Field | Description |
|---|---|
identifier | The participant identifier. |
vat_number | The same value as identifier, under the name a store's set-up form usually asks for. |
scheme | The Peppol identifier scheme, for example 0248 for Oman or 0208 for Belgium. |
peppol_id | scheme:identifier, ready to use. |
country | The participant's country, where one is recorded. |
registered | Whether the participant is registered on the SMP โ that is, reachable on the Peppol network. |
name | The participant's business name. |
seller being null is a normal answer, not an errorA store belongs to one country, and its invoices must carry an identifier of that country. So
when you pass country, only a participant of that country is offered. An Omani organisation
connecting a Belgian shop is told plainly that it has no Belgian identifier yet, rather than
being promised invoices under its Omani VAT number. Show the merchant that they need to
register an identifier for the store's country; identifier_countries tells you which ones
they do have.
Only an active participant is ever offered. A participant that has been deleted is marked inactive rather than removed, and an inactive one is not a valid sender, so it is left out. Of the participants that qualify, one registered on the SMP is preferred over one that is not.
POST /api/v1/connect/authorizeโ
Records the merchant's approval under a one-time code and returns the code. Called by GoRoute's consent page, not by the store.
Requires the apikeys:manage permission.
| Body field | Required | Description |
|---|---|---|
source | yes | One of the source values above. At most 40 characters. |
store_url | yes | The store's own URL. At most 500 characters. |
callback_url | yes | Where the browser should be sent with the code. At most 1000 characters, and subject to the per-source rule above. |
state | yes | The app's nonce, returned to you unchanged. Between 8 and 200 characters. |
store_name | no | A display name for the store. At most 120 characters. Defaults to the hostname of store_url. |
country | no | The store's country, ISO 3166-1 alpha-2, exactly two characters. Decides which seller participant the exchange returns. |
{
"code": "<the one-time code>",
"expires_in": 600,
"callback_url": "https://shop.example.com/wp-json/goroute/v1/connected",
"state": "9f2b7c41a8e35d06"
}
| Field | Description |
|---|---|
code | The one-time code. Send the browser to callback_url with this and state. |
expires_in | Seconds the code remains valid: 600, ten minutes. |
callback_url | Echoed back, so the caller has it without holding state. |
state | Echoed back unchanged. |
Nothing secret is stored under the code. The API key does not exist yet โ it is minted at exchange time โ so what is recorded against the code is only which organisation approved what.
POST /api/v1/connect/exchangeโ
Turns the code into an API key. Called by the store's server, never by the browser.
This call is unauthenticated. It carries no API key and no session; the one-time code is the credential.
| Body field | Required | Description |
|---|---|---|
code | yes | The code from the callback. Between 20 and 200 characters. |
store_url | yes | The store's URL. Must have the same hostname as the store_url the code was issued for. At most 500 characters. |
{
"api_key": "<the new API key โ keep it>",
"key_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"key_name": "WooCommerce โ shop.example.com",
"scopes": ["send", "read", "write", "integrate"],
"organization": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Nordlys Trading",
"legal_name": "Nordlys Trading LLC",
"slug": "nordlys-trading",
"country": "OM"
},
"country": "OM",
"seller": {
"identifier": "OM1100012345",
"vat_number": "OM1100012345",
"scheme": "0248",
"peppol_id": "0248:OM1100012345",
"country": "OM",
"registered": true,
"name": "Nordlys Trading LLC"
},
"webhook_events": [
"transaction.queued",
"transaction.delivered",
"transaction.retrying",
"transaction.failed"
]
}
| Field | Description |
|---|---|
api_key | The new key, beginning pk_live_. This response is where the flow hands it to you โ store it before you do anything else. |
key_id | The key's own identifier, for managing it later from the dashboard. |
key_name | The name GoRoute gave the key: the source's label, an em dash, and the store's hostname โ for example WooCommerce โ shop.example.com. It appears under this name in the merchant's API-key list, so they can see which store a key belongs to. |
scopes | Always send, read, write and integrate. See below. |
organization | The same shape as on GET /connect/context. Use it to fill the seller's name and address in the store's set-up. |
country | The store country recorded at authorize time, or null. |
seller | The participant this store's invoices issue under, chosen by the store's country, or null. Same shape as on GET /connect/context. |
webhook_events | The event names a store may subscribe to. |
What the key can doโ
The four scopes are the least a working store integration needs, and no more:
| Scope | What it grants |
|---|---|
send | Sending invoices. |
read | Reading invoices, transactions, reports, participants, approvals and webhooks. |
write | Creating invoices, and reading and exporting transactions. |
integrate | Managing webhooks and participants, and branding โ so the store can subscribe to delivery events and register the merchant's participant without a second visit to the dashboard. |
Two consequences worth knowing before you build:
- The key cannot manage API keys, so it cannot start another connect flow or mint a second key. Connecting a second store means sending the merchant through the consent page again.
- The key can call
PUT /api/v1/connect/profile, becauseintegrategrants the participant permission that call requires. That is how the store keeps the organisation's legal name and address in step.
webhook_events is the list to useโ
The four names in webhook_events are names the platform really dispatches. Subscribe to those
and you will receive something. The full catalogue is longer than the list of events anything
actually sends, so do not widen the subscription by reading names off the catalogue without
checking each one's status โ see
Reading the event list from the API.
When a call is refusedโ
Every refusal below is the real message the API returns.
| Status | Message | What happened |
|---|---|---|
400 | store_url must be an absolute http(s) URL | The URL had no scheme or no hostname. The same message appears with callback_url in place of store_url. |
400 | store_url must use https | Plain http, and the host was not localhost or 127.0.0.1. Also possible for callback_url. |
400 | store_url must be a .myshopify.com domain | source was shopify and the store URL was not a Shopify domain. |
400 | callback_url must be the app's own host | source was shopify or wix and the callback was not one of GoRoute's app hosts. |
400 | callback_url must be on the store's own host | source was woocommerce and the callback hostname did not match the store's. |
400 | Unknown source 'x' | source was not one of the values in sources. |
400 | This connection code is invalid, already used, or has expired. Start again from the store. | Exactly what it says. The three cases are deliberately not distinguished. Send the merchant through the consent page again. |
400 | This code was issued for a different store | The store_url hostname in the exchange did not match the one the code was issued for. |
400 | The organisation that approved this connection no longer exists | The account was removed between approval and exchange. |
429 | Too many attempts; try again later | More than 30 exchange attempts from one IP address within ten minutes. |
Why an unauthenticated exchange is safeโ
POST /api/v1/connect/exchange mints an API key and accepts no credential, so here is exactly
what stands between a stranger and a key. These are the protections the code implements, and
this page claims nothing beyond them.
- The code is 256 bits of randomness, from the operating system's cryptographic random source. It is not guessable and not enumerable.
- Only a hash of the code is stored. GoRoute keeps a SHA-256 digest, so the code itself is not sitting in the store that holds it.
- It can be exchanged exactly once. Reading it and deleting it happen in a single transaction, so two simultaneous exchanges cannot both succeed. A replay gets the invalid-or-used refusal.
- It expires in ten minutes. After 600 seconds it is gone whether or not it was used.
- It is bound to the store it was issued for. The exchange compares the hostname of the
store_urlyou send against the one recorded at approval, and refuses a mismatch. A code intercepted on its way to one shop cannot be redeemed as another. - It can only be returned to an allowed host in the first place. For WooCommerce that is the store's own hostname; for Shopify and Wix, GoRoute's own app hosts. A callback URL pointing somewhere else is refused at authorize time, before any code exists.
- Exchanges are rate limited to 30 attempts per IP address per ten minutes.
- Nothing secret exists until the exchange. The key is minted when the code is redeemed, so a code that is never exchanged, or is exchanged and refused, leaves no credential behind.
- An approval is always a signed-in human act. The code only exists because somebody with
the
apikeys:managepermission approved that specific store on the consent page.
What this design does not protect against is a store server that leaks the code, or a merchant who approves a connection for a store that is not theirs. The code is a bearer credential for its ten minutes: treat it like the key it becomes, keep it out of logs and URLs you retain, and exchange it as soon as it arrives.
Next stepsโ
- Store apps โ the merchant-facing side, and which app to install.
- Organizations โ
PUT /api/v1/connect/profile, the fourth call in this group. - Webhooks โ the events the four names in
webhook_eventsdeliver.