Authentication
GoRoute uses API keys to authenticate requests. All API requests must include your API key in the X-API-Key header.
API Key Formatโ
GoRoute API keys are environment-prefixed so test and production credentials are never confused:
| Environment | Prefix | Example |
|---|---|---|
| Sandbox / test | sk_test_ | sk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx |
| Production / live | pk_live_ | pk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx |
Both are sent the same way, in the X-API-Key header. The examples below use pk_live_your_key_here โ substitute your sandbox key (sk_test_...) while testing.
Using Your API Keyโ
Include your API key in every request:
curl -X GET https://app.goroute.ai/peppol-api/api/v1/settings/organization \
-H "X-API-Key: pk_live_your_key_here"
Python Exampleโ
import requests
headers = {
"X-API-Key": "pk_live_your_key_here",
"Content-Type": "application/json"
}
response = requests.get(
"https://app.goroute.ai/peppol-api/api/v1/settings/organization",
headers=headers
)
Node.js Exampleโ
const axios = require('axios');
const response = await axios.get(
'https://app.goroute.ai/peppol-api/api/v1/settings/organization',
{
headers: {
'X-API-Key': 'pk_live_your_key_here'
}
}
);
C# Exampleโ
using System.Net.Http;
var client = new HttpClient();
client.DefaultRequestHeaders.Add("X-API-Key", "pk_live_your_key_here");
var response = await client.GetAsync(
"https://app.goroute.ai/peppol-api/api/v1/settings/organization"
);
Creating API Keysโ
- Log in to the GoRoute Dashboard
- Navigate to Settings โ API Keys
- Click Create API Key
- Give it a descriptive name (e.g., "Production Backend", "CI/CD Pipeline")
- Copy the key immediately โ it won't be shown again
Managing API Keys Over the APIโ
Keys can also be created, listed, inspected and revoked programmatically. All four endpoints live
under /api/v1/api-keys and act on the organization that owns the calling key โ you can never see
or revoke another organization's keys.
| Method and path | What it does | Success status |
|---|---|---|
POST /api/v1/api-keys | Create a key | 201 |
GET /api/v1/api-keys | List your organization's keys | 200 |
GET /api/v1/api-keys/{api_key_id} | Fetch one key by ID | 200 |
DELETE /api/v1/api-keys/{api_key_id} | Revoke a key permanently | 204 |
Creating a key is the only response in the whole API that contains the key field. Every other
response returns key_prefix โ the leading characters, enough to identify the key in a list and
useless for authentication. No endpoint will ever show you the secret again.
If you have lost it and you still hold another working key, revoke the lost one with
DELETE /api/v1/api-keys/{api_key_id} and create a replacement.
If you have lost the only key you had, you are not stuck: there is a self-service recovery call, Regenerate. Read what it costs before you use it โ it revokes every key your organization holds, not just the lost one.
Your first key, and a key you have lostโ
Every call in the table above needs an API key you already hold, so none of them can give you
your first one. Two further calls cover that. They do not sit under /api/v1/api-keys:
| Method and path | What it does | Success status |
|---|---|---|
POST /api/v1/public/bootstrap-api-key | Mint the first key for an organization that has none | 200 |
POST /api/v1/public/regenerate-api-key | Revoke every key the organization holds, then mint one replacement | 200 |
Both require the apikeys:manage permission. Both accept either credential the API accepts
anywhere else: a signed-in dashboard session (the Authorization: Bearer access token the
dashboard itself sends), or an existing API key with the admin scope in X-API-Key. With
neither, you get 401.
The note further down this page explains that on the four /api/v1/api-keys endpoints the
apikeys:manage check may run in shadow mode โ logged, but allowed through. These two calls
are different. They use the strict form of the check, which ignores the shadow-mode setting
and refuses a caller without the permission in every environment, with 403 and
INSUFFICIENT_PERMISSION. A read-only viewer cannot mint or regenerate your keys.
Whichever call mints the key, the key it mints is the same: scopes read, write, send and
admin, a limit of 60 requests a minute and 10,000 a day, no expiry, and a name of the form
Dashboard Key (your-name). That scope set is deliberately wider than the ["send", "read"] a
key gets by default from POST /api/v1/api-keys โ if you want a narrower key for a particular
integration, mint one with POST /api/v1/api-keys once you are holding this one.
Both return the same body, and the key field is the only time the secret exists outside
GoRoute:
{
"status": "created",
"key": "pk_live_...",
"key_prefix": "pk_live_acme",
"key_id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Dashboard Key (jamila)",
"scopes": ["read", "write", "send", "admin"],
"message": "Your API key has been generated! Copy it now โ it will not be shown again for security reasons."
}
Bootstrap โ only when you have noneโ
curl -X POST https://app.goroute.ai/peppol-api/api/v1/public/bootstrap-api-key \
-H "Authorization: Bearer your_dashboard_access_token"
Bootstrap mints a key only if your organization has no active key at all. If it has one,
the call refuses with 409 and tells you what you already have, so that you go and find that
key rather than accumulating another:
{
"error": "keys_exist",
"message": "Your organization already has active API key(s). If you lost your key, use Regenerate or contact support.",
"existing_keys": ["pk_live_acme... (CI/CD Pipeline)"]
}
existing_keys lists at most three keys, newest first, each as its prefix and its name. It
never contains a secret.
One consequence is worth stating plainly: authenticating this call with an API key can only
ever return 409. Holding a working key is itself proof that your organization has one. So
in practice bootstrap is the signed-in-session call, and the dashboard's Create API Key
button is what most people will use.
Regenerate โ the fire escape, not a convenienceโ
It does not replace the one key you name โ it cannot, because you do not name one. It revokes
all of your organization's active keys in a single step, then mints one new key. Every
integration still running on any old key โ your ERP connector, your CI pipeline, a partner's
middleware โ starts failing with 401 immediately, and revocation is permanent.
If you are holding a working key and simply want a new one, do not use this. Use
POST /api/v1/api-keys and then revoke the old key, which is the
zero-downtime rotation below.
curl -X POST https://app.goroute.ai/peppol-api/api/v1/public/regenerate-api-key \
-H "Authorization: Bearer your_dashboard_access_token"
The message in the response tells you how many keys it destroyed โ "New API key generated!
3 previous key(s) have been revoked." Read that number. If it is higher than you expected,
something you had forgotten about has just stopped working, and the count is the only warning
you will get.
If you call this one with an admin-scoped API key rather than a dashboard session, the key
you authenticated with is one of the keys it revokes.
Create a keyโ
| Field | Type | Required | Constraint / default |
|---|---|---|---|
name | string | yes | 1โ100 characters |
description | string | no | up to 500 characters |
scopes | array of strings | no | defaults to ["send", "read"] |
rate_limit_per_minute | integer | no | 1โ1000, defaults to 60 |
rate_limit_per_day | integer | no | 1โ1000000, defaults to 10000 |
expires_at | timestamp | no | omit or send null for a key that never expires |
curl -X POST https://app.goroute.ai/peppol-api/api/v1/api-keys \
-H "X-API-Key: pk_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"name": "CI/CD Pipeline",
"description": "Sends invoices from the build server",
"scopes": ["send", "read"],
"rate_limit_per_minute": 100,
"rate_limit_per_day": 50000
}'
The 201 response carries the secret once, alongside the same fields every other key response
returns:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"org_id": "6f1c2f9e-6a1e-4a6f-9a2b-0d5f4c3b2a19",
"key": "pk_live_...",
"key_prefix": "pk_live_acme",
"name": "CI/CD Pipeline",
"description": "Sends invoices from the build server",
"scopes": ["send", "read"],
"rate_limit_per_minute": 100,
"rate_limit_per_day": 50000,
"status": "active",
"last_used_at": null,
"request_count": 0,
"created_at": "2026-08-09T09:00:00Z",
"expires_at": null
}
The per-key rate limits you set here can only ever lower what the key is allowed to do. See Rate Limiting below.
List and inspect keysโ
curl -X GET "https://app.goroute.ai/peppol-api/api/v1/api-keys?page=1&page_size=50&status=active" \
-H "X-API-Key: pk_live_your_key_here"
| Query parameter | Default | Notes |
|---|---|---|
page | 1 | 1 or greater |
page_size | 50 | 1 to 100 |
status | (none) | Filter by active or revoked |
Fetch a single key with GET /api/v1/api-keys/{api_key_id}. Neither response contains the secret.
Revoke a keyโ
curl -X DELETE https://app.goroute.ai/peppol-api/api/v1/api-keys/550e8400-e29b-41d4-a716-446655440000 \
-H "X-API-Key: pk_live_your_key_here"
A successful revocation returns 204 with no body. Revocation is permanent โ a revoked key
cannot be reactivated, and every subsequent request made with it fails with 401.
| Condition | HTTP | error_code |
|---|---|---|
| No such key in your organization | 404 | API_KEY_NOT_FOUND |
| Key is already revoked | 400 | API_KEY_ALREADY_REVOKED |
Scopesโ
| Scope | Grants |
|---|---|
send | Send documents via Peppol |
read | Read transactions, participants and lookups |
write | Create and update participants, delete transactions |
admin | Manage webhooks and API keys |
A key holding admin satisfies any scope check. A request whose key lacks the scope an endpoint
requires is rejected with 403 and INSUFFICIENT_SCOPE.
Key expiryโ
expires_at is optional and there is no default โ a key created without one never expires. Once
the timestamp has passed, the key is rejected with 401 and EXPIRED_API_KEY. Expiry is separate
from revocation: an expired key still exists and still appears in the list with status active.
apikeys:manage permissionAll four endpoints declare a required permission, apikeys:manage. Whether that declaration is
actually enforced depends on deployment configuration: role-based access control ships in shadow
mode by default, in which a missing permission is logged and the call is allowed through. This page
states what the endpoints declare. It does not promise that a call without the permission is
refused โ confirm the behaviour of your own tenant before relying on it as a control.
API Key Best Practicesโ
โ Doโ
- Store keys in environment variables or secret managers
- Use different keys for development, staging, and production
- Rotate keys periodically (every 90 days recommended)
- Use descriptive names to identify key purposes
- Revoke unused keys promptly
โ Don'tโ
- Commit keys to source control (Git, SVN)
- Expose keys in client-side code (JavaScript, mobile apps)
- Share keys via email or chat
- Use the same key across multiple environments
- Log API keys in application logs
Environment Variablesโ
Store your API key as an environment variable:
# Linux/macOS
export GOROUTE_API_KEY="pk_live_your_key_here"
# Windows PowerShell
$env:GOROUTE_API_KEY = "pk_live_your_key_here"
# Windows CMD
set GOROUTE_API_KEY=pk_live_your_key_here
Then use it in your code:
import os
API_KEY = os.environ.get("GOROUTE_API_KEY")
Key Rotationโ
To rotate an API key:
- Create a new API key โ in the dashboard, or with
POST /api/v1/api-keys - Update your application to use the new key
- Deploy the update
- Verify the new key works
- Revoke the old key โ
DELETE /api/v1/api-keys/{api_key_id}
Confirm step 4 before step 5 rather than after: revocation is permanent and cannot be undone. If
you want a rotation deadline enforced for you, set expires_at on the outgoing key instead of
revoking it by hand.
GoRoute allows multiple active API keys per organization. Create the new key before revoking the old one to avoid downtime.
POST /api/v1/public/regenerate-api-key is not a rotation and is not zero-downtime: it
revokes every key you hold before it mints the replacement, so everything running on the old
keys breaks until you have deployed the new one. Reach for it only when you have lost the
secret and the five steps above are not open to you โ see
Regenerate.
Error Responsesโ
Authentication, authorization and rate-limit failures all return a flat JSON object. The four fields sit at the top level of the body:
| Field | Description |
|---|---|
error | Short category, such as unauthorized, forbidden or rate_limit_exceeded |
error_code | The specific condition. Branch your code on this, never on the message |
message | Human-readable explanation. The wording may change without notice |
request_id | Correlation ID for this request. Quote it when you contact support |
error_code at the top levelEarlier revisions of this page showed these fields wrapped inside a nested object and gave every
authentication failure the same generic code. The API has never returned that shape. A client that
reaches into a nested error object, or that distinguishes failures by message text, will break
against the real response โ read the top-level error_code instead.
Authentication and authorization codesโ
| Condition | HTTP | error_code |
|---|---|---|
No X-API-Key header on the request | 401 | MISSING_API_KEY |
| Key is unknown or has been revoked | 401 | INVALID_API_KEY |
Key is past its expires_at | 401 | EXPIRED_API_KEY |
| Key is valid but lacks the scope the endpoint requires | 403 | INSUFFICIENT_SCOPE |
| Per-minute rate limit exceeded | 429 | RATE_LIMIT_MINUTE |
| Per-day rate limit exceeded | 429 | RATE_LIMIT_DAY |
A missing key example:
{
"error": "unauthorized",
"error_code": "MISSING_API_KEY",
"message": "API key is required. Include X-API-Key header.",
"request_id": "3f6c1b9a-2f4e-4b8d-9c31-7a2d5e8f0b14"
}
Both return 401 with INVALID_API_KEY and the same message. This is deliberate: the API does not
confirm that a key it is refusing ever existed, so a revoked credential cannot be used to probe the
platform. Do not build retry or alerting logic that expects to tell the two apart โ check the key's
status through GET /api/v1/api-keys instead.
Rate Limitingโ
Rate limits are applied per organization, not per key. Two keys belonging to the same organization draw down the same per-minute and per-day budget.
Each organization's ceiling comes from its billing plan:
| Plan | Requests/minute | Requests/day |
|---|---|---|
free | 120 | 5,000 |
starter | 120 | 5,000 |
professional | 120 | 5,000 |
business | 300 | 20,000 |
enterprise | 1,000 | 100,000 |
smp_only | 30 | 500 |
developer_sandbox | 30 | 1,000 |
demo_readonly | 30 | 500 |
Changed on 7 September 2026. The
freeandstarterfigures above are what both the production and the test service enforce today โ they run the same build. Before that date,freeallowed 10 requests a minute and 100 a day, andstarterallowed 60 and 1,000. Those older figures are kept here only so you can make sense of behaviour recorded before 7 September 2026; size your integration against the table above.
An individual key carries its own rate_limit_per_minute and rate_limit_per_day. A key limit
can only lower the plan limit, never raise it โ the effective limit is the lower of the two. A
key created on the professional plan with rate_limit_per_minute set to 500 is still limited to
120 requests a minute; setting a key limit above your plan has no effect at all.
Every response carries the current rate-limit state, and a 429 adds Retry-After:
| Header | Meaning |
|---|---|
Retry-After | Seconds until the exceeded window resets |
X-RateLimit-Limit | The effective limit for the window that was exceeded |
X-RateLimit-Remaining | Requests left in the window โ 0 on a 429 |
X-RateLimit-Reset | Unix timestamp at which the window resets |
A rate-limited response also repeats the wait in the body as retry_after, in seconds:
{
"error": "rate_limit_exceeded",
"error_code": "RATE_LIMIT_MINUTE",
"message": "Rate limit exceeded. Limit: 60/minute",
"retry_after": 42,
"request_id": "3f6c1b9a-2f4e-4b8d-9c31-7a2d5e8f0b14"
}
Back off using Retry-After rather than retrying on a fixed interval, and check error_code to see
whether you have hit the per-minute window โ which clears in under a minute โ or the per-day one,
which does not.
See Rate Limits for more details.
Audit Logsโ
The platform records, for each API key:
- the timestamp of its last use (
last_used_at) - the source IP address of the most recent request
- a cumulative request count (
request_count)
last_used_at and request_count are returned by GET /api/v1/api-keys and by
GET /api/v1/api-keys/{api_key_id}, so you can identify a key that has stopped being used โ or one
that is being used when it should not be โ before you revoke it.