Skip to main content

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:

EnvironmentPrefixExample
Sandbox / testsk_test_sk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Production / livepk_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โ€‹

  1. Log in to the GoRoute Dashboard
  2. Navigate to Settings โ†’ API Keys
  3. Click Create API Key
  4. Give it a descriptive name (e.g., "Production Backend", "CI/CD Pipeline")
  5. 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 pathWhat it doesSuccess status
POST /api/v1/api-keysCreate a key201
GET /api/v1/api-keysList your organization's keys200
GET /api/v1/api-keys/{api_key_id}Fetch one key by ID200
DELETE /api/v1/api-keys/{api_key_id}Revoke a key permanently204
The secret is returned exactly once

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 pathWhat it doesSuccess status
POST /api/v1/public/bootstrap-api-keyMint the first key for an organization that has none200
POST /api/v1/public/regenerate-api-keyRevoke every key the organization holds, then mint one replacement200

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 permission is enforced here, whatever your tenant does elsewhere

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

Regenerate revokes every key your organization holds

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

FieldTypeRequiredConstraint / default
namestringyes1โ€“100 characters
descriptionstringnoup to 500 characters
scopesarray of stringsnodefaults to ["send", "read"]
rate_limit_per_minuteintegerno1โ€“1000, defaults to 60
rate_limit_per_dayintegerno1โ€“1000000, defaults to 10000
expires_attimestampnoomit 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 parameterDefaultNotes
page11 or greater
page_size501 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.

ConditionHTTPerror_code
No such key in your organization404API_KEY_NOT_FOUND
Key is already revoked400API_KEY_ALREADY_REVOKED

Scopesโ€‹

ScopeGrants
sendSend documents via Peppol
readRead transactions, participants and lookups
writeCreate and update participants, delete transactions
adminManage 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.

About the apikeys:manage permission

All 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:

  1. Create a new API key โ€” in the dashboard, or with POST /api/v1/api-keys
  2. Update your application to use the new key
  3. Deploy the update
  4. Verify the new key works
  5. 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.

Zero-Downtime Rotation

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:

FieldDescription
errorShort category, such as unauthorized, forbidden or rate_limit_exceeded
error_codeThe specific condition. Branch your code on this, never on the message
messageHuman-readable explanation. The wording may change without notice
request_idCorrelation ID for this request. Quote it when you contact support
Read error_code at the top level

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

ConditionHTTPerror_code
No X-API-Key header on the request401MISSING_API_KEY
Key is unknown or has been revoked401INVALID_API_KEY
Key is past its expires_at401EXPIRED_API_KEY
Key is valid but lacks the scope the endpoint requires403INSUFFICIENT_SCOPE
Per-minute rate limit exceeded429RATE_LIMIT_MINUTE
Per-day rate limit exceeded429RATE_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"
}
A revoked key and an unknown key look identical

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:

PlanRequests/minuteRequests/day
free1205,000
starter1205,000
professional1205,000
business30020,000
enterprise1,000100,000
smp_only30500
developer_sandbox301,000
demo_readonly30500

Changed on 7 September 2026. The free and starter figures above are what both the production and the test service enforce today โ€” they run the same build. Before that date, free allowed 10 requests a minute and 100 a day, and starter allowed 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:

HeaderMeaning
Retry-AfterSeconds until the exceeded window resets
X-RateLimit-LimitThe effective limit for the window that was exceeded
X-RateLimit-RemainingRequests left in the window โ€” 0 on a 429
X-RateLimit-ResetUnix 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.

Next Stepsโ€‹