---
title: "Agent authentication"
description: "How an agent gets and uses an API key for the Peppol API of peppol.sh."
canonical: https://peppol.sh/auth
last-updated: 2026-10-06
---

# auth.md

> How an agent gets and uses credentials for the Peppol API of peppol.sh. One method: an API key, sent as a Bearer token.

## Discover

| What | URL |
|------|-----|
| Sandbox API | https://sandbox.peppol.sh |
| Live API | https://api.peppol.sh |
| OpenAPI spec | [api.peppol.sh/v1/openapi.json](https://api.peppol.sh/v1/openapi.json) |
| API reference | [api.peppol.sh](https://api.peppol.sh/) |
| Site index for agents | [peppol.sh/llms.txt](https://peppol.sh/llms.txt) |

All endpoints need an API key, with these exceptions: `POST /v1/signup`, `GET /v1/health`, `GET /v1/openapi.json`, `/v1/lookup/*` and `/v1/ubl/*`.

## Pick a method

There is one method: an API key. The prefix of the key selects the environment.

| Prefix | Environment | Host | How to get it |
|--------|-------------|------|---------------|
| `ps_test_` | Sandbox | sandbox.peppol.sh | Returned by `POST /v1/signup` |
| `ps_live_` | Live | api.peppol.sh | `POST /v1/account/keys` with `{"sandbox": false}` |

The API has no authorization server, no scopes and no token endpoint.

## Register

Send an email address. The response contains a sandbox API key. The key works immediately.

```bash
curl -X POST https://api.peppol.sh/v1/signup \
  -H "Content-Type: application/json" \
  -d '{"email": "agent@example.com"}'
```

Response (`201`):

```json
{
  "id": "acc_V1StGXR8Z5jdHi6BmyT9a",
  "email": "agent@example.com",
  "status": "active",
  "api_key": "ps_test_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"
}
```

- `name` is an optional second field in the request body.
- The response shows the full key one time only. Keep it in a safe place. The server keeps a SHA-256 hash, not the key.
- One account for each email address. A second signup with the same address returns `409` with code `email_taken`.
- Limit: 10 signups for each IP address in one hour.

## Claim

There is no claim step. The key from the signup response is active immediately, and no person has to approve the sandbox.

## Exchange

There is no exchange step. You do not trade the API key for an access token. Send the API key itself with each request.

## Use

Send the key in the `Authorization` header:

```bash
curl https://sandbox.peppol.sh/v1/account \
  -H "Authorization: Bearer ps_test_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"
```

Use `ps_test_` keys on sandbox.peppol.sh and `ps_live_` keys on api.peppol.sh. A key on the wrong host returns `403` with code `wrong_environment`.

To make one more key, call `POST /v1/account/keys`. Only the owner or an admin of the workspace can do this.

```bash
curl -X POST https://sandbox.peppol.sh/v1/account/keys \
  -H "Authorization: Bearer ps_test_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6" \
  -H "Content-Type: application/json" \
  -d '{"sandbox": false, "label": "production"}'
```

Response (`201`):

```json
{
  "api_key": "ps_live_q1w2e3r4t5y6u7i8o9p0a1s2d3f4g5h6",
  "prefix": "ps_live_q1w2e3r...",
  "sandbox": false,
  "company_id": null,
  "label": "production"
}
```

Before you send documents on the live Peppol network, a person must verify the workspace (KYC). The steps are in the [documentation index](https://peppol.sh/docs/llms.txt).

## Errors

Each response that is not `2xx` has the same JSON shape:

```json
{
  "error": {
    "type": "authentication_error",
    "code": "invalid_api_key",
    "message": "Invalid API key",
    "retry": { "retryable": false }
  }
}
```

`param` and `details` are optional fields of `error`.

| Status | Type | Code | Cause |
|--------|------|------|-------|
| 400 | `validation_error` | `invalid_field` | The signup email is missing or not valid |
| 401 | `authentication_error` | `missing_api_key` | No `Authorization: Bearer` header |
| 401 | `authentication_error` | `invalid_api_key` | The key has the wrong format, is unknown, or is revoked |
| 403 | `authorization_error` | `wrong_environment` | A sandbox key on api.peppol.sh, or a live key on sandbox.peppol.sh |
| 403 | `authorization_error` | `insufficient_role` | Only an owner or an admin can create API keys |
| 403 | `authentication_error` | `disabled_account` | The account is disabled |
| 404 | `not_found` | `api_key_not_found` | The key to revoke does not exist or is already revoked |
| 409 | `validation_error` | `email_taken` | An account with this email address exists |
| 429 | `rate_limit_error` | `rate_limit_exceeded` | Too many requests. Wait for the time in the `Retry-After` header |

## Revocation

Revoke a key with its `prefix`. The prefix is the first 15 characters of the key and three dots. `GET /v1/account` lists the prefix of each key in `api_keys`.

```bash
curl -X DELETE "https://sandbox.peppol.sh/v1/account/keys/ps_test_a1b2c3d..." \
  -H "Authorization: Bearer ps_test_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"
```

Response (`200`):

```json
{ "revoked": true, "prefix": "ps_test_a1b2c3d..." }
```

A revoked key stops working immediately and returns `401` with code `invalid_api_key`. To rotate a key, create a new key first, then revoke the old one.
