Organizations & Multi-Tenancy
GoRoute is built for platforms that serve multiple businesses. Each business operates within its own organization, and every organization's data is isolated from every other one.
What is an Organization?โ
An organization represents a single business entity on the platform. Each organization has:
- Its own identifier
- Its own Peppol participant registration(s)
- Isolated invoice and transaction data
- Its own webhook configuration
- Its own API keys
How scoping actually worksโ
An API key belongs to exactly one organization. Every request you make is scoped to that organization automatically โ there is no organization parameter to pass and no header to set.
# The key decides the organization. Nothing else to specify.
curl -X GET https://app.goroute.ai/peppol-api/api/v1/transactions \
-H "X-API-Key: YOUR_API_KEY"
There is no X-Organization-Id header and no /api/v1/organizations/{id}/โฆ route.
A single key cannot write another organization's data, and cannot be pointed at a
different organization at request time. A platform serving many businesses holds
one key per organization โ see the platform pattern
below.
Two narrow exceptions exist for partner accounts whose customers are organisations
beneath them: a partner key can read across that group with scope=group, and a signed-in
partner user (not an API key) can open a customer's portal read-only. See
For software vendors and resellers.
Multi-Tenant Architectureโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Your Platform โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Customer A โ Customer B โ Customer C โ
โ (key A) โ (key B) โ (key C) โ
โโโโโโโโฌโโโโโโโโดโโโโโโโฌโโโโโโโโดโโโโโโโฌโโโโโโโโโโโโโโโโโ
โ โ โ
โผ โผ โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ GoRoute API โ
โ Each key resolves to exactly one organization โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โ โ
โผ โผ โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Peppol Network โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Creating an Organizationโ
Organizations are created during onboarding, not through the public API. You create one in the portal, or your account manager provisions it for you, and you receive an API key bound to it.
Partners create their customers' organisations in the GoRoute partner console, beneath their own organisation โ see For software vendors and resellers. If you onboard customers at volume, talk to us about programmatic provisioning โ it is an account-level arrangement rather than a public endpoint.
Related: Client Onboarding.
Reading your organization profileโ
To see which organization a key belongs to, and the details registered against it:
curl -X GET https://app.goroute.ai/peppol-api/api/v1/settings/organization \
-H "X-API-Key: YOUR_API_KEY"
Response:
{
"id": "3617777c-0e1b-4c2a-9f55-1d2a4b6c8e90",
"name": "Acme Corporation",
"peppol_id": "0106:123456789",
"vat_number": "NL123456789B01",
"contact_name": "Accounts Payable",
"contact_email": "invoices@acme.example.com",
"phone": "+31201234567",
"address_line1": "123 Business Street",
"city": "Amsterdam",
"postal_code": "1012 AB",
"country": "NL",
"status": "active"
}
Organization countryโ
Every organization carries a country, and it is required when the organization is
created. It must be an officially assigned
ISO 3166-1 alpha-2 code, such as OM,
GB or NL. A missing value, or one that only looks like a code (EU, UN, XX, USA),
is refused. Three common non-ISO spellings
are accepted and stored as the proper code: UK โ GB, EL โ GR, XI โ GB.
The country is set when your account is set up. PATCH /api/v1/settings/organization
does not change it: a country in the request body is ignored. Ask GoRoute if yours is
wrong.
Updating your organization profileโ
curl -X PATCH https://app.goroute.ai/peppol-api/api/v1/settings/organization \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"contact_email": "newemail@acme.example.com"
}'
Send only the fields you are changing.
When a store app keeps the profile in stepโ
The WooCommerce, Shopify and Wix apps copy the business details from the store's set-up
(legal name, address, website, contact) into this profile with
PUT /api/v1/connect/profile. That call does not change a country the organization
already has. If the store sends a different country, the country stays as it is and
appears under ignored in the response; the other fields are still saved. The call sets a
country only when the organization has none yet, and only to an ISO 3166-1 alpha-2 code
such as OM. Registering a participant in another country does not change the
organization's country either. If your organization's country is wrong, ask GoRoute to
change it.
PUT /api/v1/connect/profile is the last of the four calls in the Connect group. The other
three are how the store got its API key in the first place, without the merchant copying one by
hand โ see Store Connect for that flow and for the exact fields of
each call.
Your teamโ
An organization is people as well as settings. These calls cover the people:
- list who is in the organization;
- invite someone new;
- change someone's role;
- deactivate someone;
- switch someone back on;
- remove someone permanently.
Every one of them requires the users:manage permission. An API key with the admin scope holds
it; a key scoped only to read or write does not. Without it the call answers 403 with
error_code: INSUFFICIENT_PERMISSION.
Who is in my organization?โ
GET /api/v1/team/members
curl -X GET https://app.goroute.ai/peppol-api/api/v1/team/members \
-H "X-API-Key: your_api_key"
The response holds members, total, and assignable_roles โ the roles you are allowed to give
someone. Each member carries:
| Field | Meaning |
|---|---|
id | The member's stable key within the organization |
email | Their email address. Empty for a member whose sign-in account no longer exists (see below). |
name | Their name, or null |
role | Their role, or null when no role has been assigned yet |
status | active, inactive, or invited. From the 6โ7 October 2026 change, also login_disabled: the person is a member, but their sign-in account is disabled, so they cannot sign in. |
login_enabled | New with the 6โ7 October 2026 change. true when their sign-in account can sign in, false when it is disabled, null when they have no sign-in account. |
is_self | true for the member the API key or session is acting as |
From the 6โ7 October 2026 change, the list also shows a member whose sign-in
account no longer exists. Their email is empty, and you can still remove them with the call below.
Invite a colleagueโ
POST /api/v1/team/invite โ answers 201.
Send an email address, a name, and the role to give them:
curl -X POST https://app.goroute.ai/peppol-api/api/v1/team/invite \
-H "X-API-Key: your_api_key" \
-H "Content-Type: application/json" \
-d '{"email": "colleague@example.com", "name": "Sara Al Balushi", "role": "finance"}'
The roles you can assign are org_owner, org_admin, finance, operations, developer and
viewer. Read assignable_roles from the call above rather than hard-coding that list. An
unrecognised role is refused with 400, and the message names the roles that are accepted.
The response tells you what happened: id, email, name, role, status (invited), and
four more fields:
| Field | Meaning |
|---|---|
reused_existing | true when that email address already had a GoRoute login and the invite reused it instead of creating one. false for a brand-new account. |
activation_url | The one-time link a brand-new account uses to set its own password. null when reused_existing is true โ read the dated note below before you rely on this field. |
email_sent | true when the invitation email was handed to the mail provider. false when it was not sent at all. |
email_error | A message when building or sending the email raised an error. null otherwise โ including when email_sent is false for a reason that is not an error. Read email_sent to decide what to do; treat email_error as extra detail that may not be there. |
What the person receives. For a brand-new account, GoRoute creates the login and emails them an invitation with an activation link, which they use to set their own password. You do not choose a password for them, and the link is valid for 72 hours. If the address already had a GoRoute login, no second account is made โ the invitation email still goes to them, and the link in it goes only to that mailbox.
From the 6โ7 October 2026 change, only the newest activation link works. An older link stops working once the account has changed since it was sent, for example because you invited the same person again. The person then sees a message asking them to use the most recent invitation email, or to ask you for a new one. So tell them to use the most recent email.
A GoRoute build released on 5 October 2026 changed what this call does when the email address already has a GoRoute login. Since the evening of 5 October 2026, GoRoute's test service, its production service and its Oman service all run that build or a later one, so every one of them behaves the newer way. Nothing below changes what happens for a brand-new account.
- Before that build: every invite returned an
activation_url, and an invite could claim a login that already existed and hand the caller its link. - From that build: an invite that reuses a login that already existed returns
activation_url: null. The activation link is sent only to that address's own mailbox and is never returned to you.
How to check you are getting the current behaviour. Look for reused_existing in the 201
response. Every GoRoute service now includes it, so its presence confirms the newer behaviour. If a
response ever carries no reused_existing field at all, it came from the older build: ask GoRoute
support, because the services report their version only as 1.0.0 and this field is the only
reliable signal.
What this breaks in existing code. A client that treats activation_url as a string that is
always there will now receive null and may fail to parse the response. Allow for a null value
before you read it.
email_sent before you assume they were toldSending the email is best effort, and the invitation is created either way. For a brand-new
account โ reused_existing is false โ the account still exists when email_sent is false, and
activation_url is still in the response, so you can pass that link to the person yourself.
When reused_existing is true there is no link in the response to pass on, whatever
email_sent says. The only copy of it went to that address's own mailbox. If that email does not
arrive, the person signs in with the account they already had, or asks GoRoute support โ you cannot
let them in from your side.
email_sent: false with email_error: null is a real combination, not a bug. The commonest
cause is an address GoRoute will not mail: colleague@example.com in the example above is a
reserved documentation address (RFC 2606 / RFC 6761) with no mailbox, and a GoRoute service that
enforces that skips the email silently rather than failing the invite. The member is created, the
response is still 201, and nothing is sent. Use a real address when you want the person to get
the email, and hand them activation_url when you do not โ which only works for a brand-new
account.
Four refusals worth handling:
- Only an organization owner โ or a caller holding owner-level permissions โ may hand out the
org_ownerrole. Anyone else gets403with the message "Only an organization owner can grant or change the owner role." - If that email address already belongs to a different organization, the invite is refused with
409. The same address can be re-invited within your own organization โ but from the 5 October 2026 build that holds only while GoRoute's records on the service you are calling still show the person as a member of it, which is the next refusal. 409when the address already has a GoRoute login and GoRoute's records on the service you are calling do not show it as a member of your organization. The message is "An account for โฆ already exists and is not a member of this organisation, so it cannot be invited, reset or removed from here. Ask them to sign in with their existing account, or contact support to link it." The wording is deliberately identical whatever the underlying reason, so it tells you nothing about where that account lives. Treat it as "this person already has an account you cannot act on" and follow the message: have them sign in, or contact support to link it. New with the 5 October 2026 build.403, "This email address cannot be invited." for the small set of addresses GoRoute keeps for running the platform itself. The message says no more than that, on purpose, and the same403comes back whether or not an account exists for the address. There is nothing to retry โ use a different address. New with the 5 October 2026 build.
Change someone's roleโ
PATCH /api/v1/team/members/{member_id}/role โ send the new role, get the member back.
curl -X PATCH https://app.goroute.ai/peppol-api/api/v1/team/members/MEMBER_ID/role \
-H "X-API-Key: your_api_key" \
-H "Content-Type: application/json" \
-d '{"role": "operations"}'
MEMBER_ID is the id from GET /api/v1/team/members. The role must be one of org_owner,
org_admin, finance, operations, developer or viewer; member is still accepted as an
old name for operations. Anything else is refused with 400, and the message lists the roles
that are accepted.
The response is one member: id, role, status and is_self. Its email field comes back
empty on this call โ read the member list if you need the address.
Three refusals to expect:
403if you are not an owner and you try to grant or take awayorg_owner. Ordinaryusers:manageis enough for every other role change, but the owner role needs an owner (or a caller with owner-level permissions, such as a super admin or a key holdingbilling:manage).400, "The organization must keep at least one owner", if the change would move the last active owner off the owner role. There is no way to leave an organization ownerless.404if the member id does not belong to your organization.
Deactivating and removing changed on 6โ7 October 2026โ
A GoRoute build changed what three calls do to the person's sign-in account: deactivating, switching back on and removing. GoRoute's test service has run that build since 13:12 UTC on 6 October 2026. Its production service has run it since early on 7 October 2026 (UTC). GoRoute's Oman service does not have it yet, and still behaves the older way.
- Before that build, and on the Oman service today: deactivating someone disables their sign-in account. Switching them back on re-enables it. Removing them deletes it.
- From that build: the three calls change only the person's membership of your organization. Their sign-in account is kept as it is. Deactivating or removing them still ends their access to your organization on their next request. Switching them back on never re-enables a sign-in account that is disabled.
How to tell which behaviour you have. The newer build adds a login field to the response of a
deactivate or activate call, and a login_deleted field to the response of a delete call. If the
field is there, you have the newer behaviour. If it is missing, you have the older one.
What this breaks in existing code. Code that assumed deactivating or removing someone ends their sign-in everywhere. With the newer behaviour their sign-in account still works, so they may still sign in to GoRoute, for example to another organization they belong to. They cannot reach yours.
Deactivate someoneโ
POST /api/v1/team/members/{member_id}/deactivate โ switches the person off without deleting
them. Their membership is marked inactive, which ends their access to your organization on their
next request. Nothing they created is touched.
- Before the 6โ7 October 2026 change, and on the Oman service today: their sign-in account is disabled as well.
- From that change: their sign-in account is kept.
curl -X POST https://app.goroute.ai/peppol-api/api/v1/team/members/MEMBER_ID/deactivate \
-H "X-API-Key: your_api_key"
The response is the same member object, with status: "inactive" and, again, an empty email.
From the change it also carries three fields:
| Field | Meaning |
|---|---|
login | What happened to their sign-in account. On GoRoute's services today this is kept, or none when they no longer have a sign-in account. |
login_note | With kept: "They no longer have access to this organisation. Their sign-in account itself was kept, because it may also be used elsewhere." null with none. |
login_enabled | Whether their sign-in account can sign in: true, false if it was already disabled, or null when they have none. |
Refusals:
400if you aim it at yourself. You cannot deactivate the account you are calling with.400if it would deactivate the last active owner.403if the target is a GoRoute platform administrator โ those accounts are not managed from here.404if the member id does not belong to your organization.409if the person also administers your organization from GoRoute's admin console. The message is "This person also administers this organisation from the admin console, so removing or deactivating them here would not end their access. Their admin-console access must be disabled or removed first (Admin Users)." Have that access disabled or removed first, then try again. New with the 6โ7 October 2026 change.
Switch someone back onโ
POST /api/v1/team/members/{member_id}/activate โ the reverse of deactivating. Their membership
is marked active again, which gives back their access to your organization.
- Before the 6โ7 October 2026 change, and on the Oman service today: their
sign-in account is re-enabled as well. The response is the member object with
status: "active"and, as on the calls above, an emptyemail. - From that change: their sign-in account is never re-enabled. The response still has an empty
email, and itsloginfield tells you whether they can get in.
login | status | What it means |
|---|---|---|
unchanged | active | Their sign-in account works, so they have access again. login_note says "Access to this organisation is restored. Their sign-in account was not changed." |
still_disabled | login_disabled | They are a member again, but their sign-in account is disabled, so they cannot sign in yet. login_note says to contact support to re-enable it. You cannot re-enable it from here. |
none | active | They no longer have a sign-in account. |
login_enabled is true, false or null to match.
curl -X POST https://app.goroute.ai/peppol-api/api/v1/team/members/MEMBER_ID/activate \
-H "X-API-Key: your_api_key"
still_disabledSomeone deactivated before the change, under the older behaviour, had their sign-in account disabled
at the same time. Switching them back on now gives back their membership but not their sign-in
account, unless GoRoute has already re-enabled it. If you get still_disabled, contact GoRoute
support, as the note says. Until it is re-enabled, the member list shows them as login_disabled.
Refusals:
403if the target is a GoRoute platform administrator.404if the member id does not belong to your organization.
Unlike deactivating and deleting, this call does not refuse to act on your own account.
Remove someone permanentlyโ
DELETE /api/v1/team/members/{member_id} โ removes their membership of your organization, which
ends their access on their next request.
- Before the 6โ7 October 2026 change, and on the Oman service today: their sign-in account is deleted as well.
- From that change: their sign-in account is kept.
There is no way to restore a removed member. Their membership rows are removed. Before the change, and on the Oman service today, their sign-in account is deleted too. From the change their sign-in account is kept, but their membership is still gone for good: you cannot simply invite them again (see below). If you only want to stop someone using your organization for now, deactivate them instead โ that is reversible with the call above.
curl -X DELETE https://app.goroute.ai/peppol-api/api/v1/team/members/MEMBER_ID \
-H "X-API-Key: your_api_key"
The response is not a member object.
- Before the change, and on the Oman service today: it is just
{"id": "MEMBER_ID", "deleted": true}. - From the change: it is
{"id": "MEMBER_ID", "deleted": true, "login_deleted": false, "login_note": "โฆ"}.login_deletedisfalsebecause the sign-in account was kept.login_noteis the same sentence as a deactivation'skeptnote, ornullwhen they had no sign-in account left.
Inviting them again. Before the change, and on the Oman service today, an invite after a removal
creates a brand-new account. From the change, their sign-in account still exists but is no longer a
member of your organization. So an invite answers 409 with the message "An account for โฆ already
exists and is not a member of this organisation", described under
Invite a colleague. Follow that message: contact support to link the account
to your organization again.
Work the organization owns is kept. Invoices, transactions and audit history belong to the organization, not to the person who created them, so none of it is deleted with the account.
Refusals:
400if you aim it at yourself. You cannot delete the account you are calling with.400, "The organization must keep at least one owner", if it would remove the last active owner.403if the target is a GoRoute platform administrator.404if the member id does not belong to your organization.409if the person also administers your organization from GoRoute's admin console, with the same message as for deactivating. New with the 6โ7 October 2026 change.
Both calls need the users:manage permission, the same as inviting, changing a role and
deactivating. A 404 here means "not a member of your organization" โ it is the same answer
you get for a member id that does not exist anywhere, deliberately, so that nobody can use
this endpoint to discover who belongs to another organization.
Both are also available in the dashboard, which is the easiest place to do either.
Data Isolationโ
Each organization's data is isolated:
| Data Type | Isolation |
|---|---|
| Invoices | Fully isolated |
| Transactions | Fully isolated |
| Participants | Isolated; an identifier can be active in only one organisation tree |
| Webhooks | Fully isolated |
| API Keys | Scoped to one organization |
| Rate Limits | Applied per organization |
Every query is filtered by the organization the API key resolves to. Because the organization is derived from the key rather than supplied in the request, a caller cannot widen its own scope by changing a parameter. The only wider reads are a partner's group views over the organisations beneath it.
Organization Statusโ
| Status | Description |
|---|---|
active | Fully operational |
suspended | Temporarily disabled (e.g. billing issue) |
deleted | Removed |
Platform Integration Patternโ
For platforms serving multiple customers, hold one key per customer organization and select the key โ not a header โ when acting on their behalf:
import requests
class GoRouteClient:
"""One instance per customer organization."""
def __init__(self, api_key: str):
self.api_key = api_key
self.base_url = "https://app.goroute.ai/peppol-api"
def _headers(self):
return {"X-API-Key": self.api_key, "Content-Type": "application/json"}
def send_invoice(self, invoice_data: dict):
"""Create and send an invoice for this organization."""
response = requests.post(
f"{self.base_url}/api/v1/invoices",
headers=self._headers(),
json=invoice_data,
)
response.raise_for_status()
return response.json()
def get_transactions(self):
"""List this organization's transactions."""
response = requests.get(
f"{self.base_url}/api/v1/transactions",
headers=self._headers(),
)
response.raise_for_status()
return response.json()
# Map your internal customer to the key you hold for them.
clients = {customer_id: GoRouteClient(key) for customer_id, key in your_key_store.items()}
Store customer keys the way you would store any other credential: encrypted at rest, never in source control, and rotatable without a code change.
Webhooks per Organizationโ
Webhooks are registered per organization, which means they are registered with the key for that organization:
curl -X POST https://app.goroute.ai/peppol-api/api/v1/webhooks \
-H "X-API-Key: CUSTOMER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://yourplatform.com/webhooks/acme",
"events": ["transaction.delivered", "transaction.received"],
"description": "Acme Corporation delivery events"
}'
The signing secret is generated by GoRoute and returned in the response โ you do not supply it. See Webhooks for the event list and signature verification.
Best Practicesโ
- One organization per business โ do not share an organization between customers
- One key per organization โ map your internal customer ID to the key you hold
- Validate ownership in your own layer โ make sure a user of your platform can only reach the key that belongs to them
- Separate webhook endpoints โ a distinct URL per customer makes delivery failures easy to attribute