Skip to main content

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โ€‹

  1. 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 as state.
  2. 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 calls POST /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 with code and state.
  3. The store's server exchanges the code. POST /api/v1/connect/exchange takes 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.

sourceLabel used in the key's nameWhere the code may be returned
woocommerceWooCommerceThe store's own host โ€” a WooCommerce shop hosts its own callback
shopifyShopifyA GoRoute-hosted app host (see below). The store itself must be a .myshopify.com domain
wixWixA 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 parameterDescription
countryOptional. 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"
}
}
FieldDescription
organizationThe signed-in organisation: id, name, legal_name, slug and country.
countryThe 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.
sellerThe participant this store's invoices would issue under, or null when the organisation has no usable participant for that country.
identifier_countriesEvery 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.
sourcesThe 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:

FieldDescription
identifierThe participant identifier.
vat_numberThe same value as identifier, under the name a store's set-up form usually asks for.
schemeThe Peppol identifier scheme, for example 0248 for Oman or 0208 for Belgium.
peppol_idscheme:identifier, ready to use.
countryThe participant's country, where one is recorded.
registeredWhether the participant is registered on the SMP โ€” that is, reachable on the Peppol network.
nameThe participant's business name.
seller being null is a normal answer, not an error

A 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 fieldRequiredDescription
sourceyesOne of the source values above. At most 40 characters.
store_urlyesThe store's own URL. At most 500 characters.
callback_urlyesWhere the browser should be sent with the code. At most 1000 characters, and subject to the per-source rule above.
stateyesThe app's nonce, returned to you unchanged. Between 8 and 200 characters.
store_namenoA display name for the store. At most 120 characters. Defaults to the hostname of store_url.
countrynoThe 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"
}
FieldDescription
codeThe one-time code. Send the browser to callback_url with this and state.
expires_inSeconds the code remains valid: 600, ten minutes.
callback_urlEchoed back, so the caller has it without holding state.
stateEchoed 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 fieldRequiredDescription
codeyesThe code from the callback. Between 20 and 200 characters.
store_urlyesThe 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"
]
}
FieldDescription
api_keyThe new key, beginning pk_live_. This response is where the flow hands it to you โ€” store it before you do anything else.
key_idThe key's own identifier, for managing it later from the dashboard.
key_nameThe 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.
scopesAlways send, read, write and integrate. See below.
organizationThe same shape as on GET /connect/context. Use it to fill the seller's name and address in the store's set-up.
countryThe store country recorded at authorize time, or null.
sellerThe participant this store's invoices issue under, chosen by the store's country, or null. Same shape as on GET /connect/context.
webhook_eventsThe 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:

ScopeWhat it grants
sendSending invoices.
readReading invoices, transactions, reports, participants, approvals and webhooks.
writeCreating invoices, and reading and exporting transactions.
integrateManaging 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, because integrate grants 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.

StatusMessageWhat happened
400store_url must be an absolute http(s) URLThe URL had no scheme or no hostname. The same message appears with callback_url in place of store_url.
400store_url must use httpsPlain http, and the host was not localhost or 127.0.0.1. Also possible for callback_url.
400store_url must be a .myshopify.com domainsource was shopify and the store URL was not a Shopify domain.
400callback_url must be the app's own hostsource was shopify or wix and the callback was not one of GoRoute's app hosts.
400callback_url must be on the store's own hostsource was woocommerce and the callback hostname did not match the store's.
400Unknown source 'x'source was not one of the values in sources.
400This 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.
400This code was issued for a different storeThe store_url hostname in the exchange did not match the one the code was issued for.
400The organisation that approved this connection no longer existsThe account was removed between approval and exchange.
429Too many attempts; try again laterMore 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_url you 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:manage permission 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_events deliver.