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. If the secret is lost, revoke the key and create another. There is no
endpoint that will show it to you again.
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.
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 | 10 | 100 |
starter | 60 | 1,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 |
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 starter plan with rate_limit_per_minute set to 500 is still limited to 60
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.