Audit Log API
Every significant action in your GoRoute organisation is recorded as an audit entry: who did it, what they did it to, when, and from which IP address. This page covers the eight endpoints that let you read that trail, summarise it, export it, and delete old parts of it.
All paths below are relative to https://app.goroute.ai/peppol-api. Every call is scoped to
the organisation of the API key you send โ you cannot read another organisation's trail.
- The CSV export stops at 10,000 rows and does not tell you it stopped. See Export the trail as CSV.
POST /api/v1/audit/cleanuppermanently deletes audit entries, and it is guarded by the same read permission as everything else on this page. See Delete old entries.
Permissionsโ
All eight endpoints require the audit:read permission, including the one that deletes.
There is no separate write or delete permission for the audit trail today.
The endpoints at a glanceโ
| Method and path | What it does |
|---|---|
GET /api/v1/audit | List entries, newest first, paginated and filterable |
GET /api/v1/audit/{log_id} | One entry in full |
GET /api/v1/audit/stats | Counts by severity and by action over a date range |
GET /api/v1/audit/actions | The action names and severities you can filter on |
GET /api/v1/audit/actors | The people and keys that appear in the trail, with counts |
GET /api/v1/audit/resource/{resource_type}/{resource_id} | Everything that happened to one object |
GET /api/v1/audit/export.csv | Download the filtered trail as a CSV file |
POST /api/v1/audit/cleanup | Permanently deletes entries older than a retention window |
What an audit entry looks likeโ
{
"id": "7c1f7d54-2a3f-4a51-9a2f-1c9d0a6b5e21",
"org_id": "3f2b9c10-6d44-4c1e-9a8a-5b7e3f0a1d22",
"actor": {
"id": "9b0d1a77-4e6c-4f31-b2c9-0d5a8e2f7c13",
"type": "user",
"name": "Alex Fraser",
"ip": "203.0.113.24"
},
"action": "invoice.sent",
"severity": "info",
"resource": {
"type": "invoice",
"id": "INV-2026-000417",
"name": "INV-2026-000417"
},
"description": "Invoice sent to 0192:991825827",
"metadata": {},
"changes": null,
"request_id": "req_01J9Z2X4K7",
"created_at": "2026-08-31T09:14:02.118431"
}
| Field | Notes |
|---|---|
id | The entry's own identifier, a UUID |
org_id | Your organisation |
actor.id | May be null โ for example when the action had no signed-in user |
actor.type | What kind of caller it was, such as a user or an API key |
actor.name, actor.ip | May be null |
action | A dotted name such as invoice.sent. The full list comes from GET /api/v1/audit/actions |
severity | One of debug, info, warning, error, critical |
resource | The object acted on. Absent entirely when the entry has no resource type |
description | A short human sentence |
metadata | Free-form extra data; an empty object when there is none |
changes | {"old": โฆ, "new": โฆ} when the action recorded a before and after, otherwise null |
request_id | Correlates the entry with a single API request; may be null |
created_at | UTC timestamp in ISO 8601 |
List entriesโ
GET /api/v1/audit โ returns a page of entries, newest first.
| Parameter | Default | Limits |
|---|---|---|
page | 1 | 1 or higher |
page_size | 50 | 1 to 100 |
action | โ | Exact match, for example invoice.sent |
severity | โ | Exact match: debug, info, warning, error, critical |
resource_type | โ | Exact match, for example invoice |
resource_id | โ | Exact match |
actor | โ | Partial, case-insensitive match on the actor's name or recorded email |
date_from, date_to | โ | Date-times; the range is inclusive at both ends |
search | โ | Partial, case-insensitive match on the description, the resource name or the actor name |
Filters combine with AND: every filter you send must match for an entry to be returned.
curl -H "X-API-Key: your_api_key" \
"https://app.goroute.ai/peppol-api/api/v1/audit?severity=error&page_size=100"
{
"items": [],
"total": 0,
"page": 1,
"page_size": 100,
"total_pages": 0
}
total is the number of entries matching your filters, not the number on this page.
total_pages is derived from total and page_size.
Get one entryโ
GET /api/v1/audit/{log_id} โ {log_id} is the entry's UUID. Returns the single entry in
the shape shown above, or 404 if no entry with that id exists in your organisation.
import requests
BASE_URL = "https://app.goroute.ai/peppol-api"
log_id = "7c1f7d54-2a3f-4a51-9a2f-1c9d0a6b5e21"
response = requests.get(
f"{BASE_URL}/api/v1/audit/{log_id}",
headers={"X-API-Key": "your_api_key"},
timeout=30,
)
Everything that happened to one objectโ
GET /api/v1/audit/resource/{resource_type}/{resource_id} โ returns a plain list of entries
for one object, newest first. Use it when you want the history of a single invoice, draft or
webhook rather than a filtered slice of the whole trail.
| Parameter | Default | Limits |
|---|---|---|
limit | 100 | 1 to 500 |
The response is a JSON array of entries, not a paginated object. There is no page parameter:
if an object has more than limit entries, you get the most recent limit of them.
Summary countsโ
GET /api/v1/audit/stats โ counts over a date range.
| Parameter | Default |
|---|---|
date_from | 30 days ago |
date_to | now |
{
"total": 1284,
"date_from": "2026-08-02T09:00:00",
"date_to": "2026-09-01T09:00:00",
"by_severity": { "info": 1201, "warning": 71, "error": 12 },
"by_action": { "invoice.sent": 604, "invoice.delivered": 588 }
}
by_action is the top ten actions onlyThe by_action object lists the ten most frequent actions in the range, largest first. It is
not a complete breakdown, and its values will not add up to total unless your organisation
used ten or fewer distinct actions in that window. by_severity is complete.
What can I filter on?โ
GET /api/v1/audit/actions โ the vocabulary, so a filter drop-down does not have to hardcode
it. Action names come back grouped by the part before the dot.
{
"actions": {
"invoice": [
{ "value": "invoice.created", "name": "INVOICE_CREATED" },
{ "value": "invoice.sent", "name": "INVOICE_SENT" }
],
"auth": [
{ "value": "auth.login_success", "name": "LOGIN_SUCCESS" }
]
},
"severities": ["debug", "info", "warning", "error", "critical"]
}
Send the value, not the name, as the action filter.
GET /api/v1/audit/actors โ the distinct actors that appear in the trail, ordered by how many
entries each has, largest first. Accepts date_from and date_to.
{
"actors": [
{ "name": "Alex Fraser", "type": "user", "count": 812 },
{ "name": "Unknown", "type": "api_key", "count": 96 }
],
"count": 2
}
An actor whose name was never recorded is grouped under the literal name Unknown.
Export the trail as CSVโ
GET /api/v1/audit/export.csv โ returns a text/csv file as a download, named
audit-trail-YYYYMMDD.csv after today's date in UTC.
The export returns at most the 10,000 most recent entries matching your filters. When
there are more, the rest are simply absent. The file carries no total, no row count and no
"there is more" marker, and the HTTP response is a normal 200. Nothing in the download tells
you it is incomplete.
If you are producing a file for an auditor, check the size of the set first with
GET /api/v1/audit?page_size=1 and read the total field. If total is above 10,000, split
the export into several narrower date ranges and export each one.
The export accepts a smaller set of filters than the list endpoint:
| Filter | On the list endpoint | On the CSV export |
|---|---|---|
action, actor, resource_type, date_from, date_to, search | yes | yes |
severity | yes | no |
resource_id | yes | no |
Sending severity or resource_id to the export does not narrow the file. To export a
severity-filtered set, narrow by date instead and filter the CSV afterwards.
The columns, in order, are:
timestamp_utc, user, user_email, actor_type, action, severity, resource_type,
invoice, description, changed_fields, actor_ip
Two of those names are worth spelling out. The invoice column holds the resource's name, or
its id when there is no name โ so for a non-invoice entry it holds that object's name, not an
invoice. The changed_fields column is a single string of the form
field: old -> new; field: old -> new, and it is empty when the entry recorded no changes.
curl -H "X-API-Key: your_api_key" \
-o audit-trail.csv \
"https://app.goroute.ai/peppol-api/api/v1/audit/export.csv?date_from=2026-08-01T00:00:00"
Delete old entriesโ
POST /api/v1/audit/cleanup โ deletes every entry in your organisation older than the
retention window you give it.
The deletion is permanent. There is no soft delete, no archive and no recovery path in this API. An entry removed here is gone.
Note also that this endpoint requires only the audit:read permission. Anyone who can
read your audit trail can therefore delete the older part of it. If that is not what you
want, restrict who holds audit:read in your organisation. This is a statement about how the
endpoint is guarded today, not advice that the guard is sufficient.
| Parameter | Default | Limits |
|---|---|---|
retention_days | 90 | 30 to 365 |
The floor of 30 means you cannot use this endpoint to clear the last month. Entries newer than
retention_days are untouched.
curl -X POST \
-H "X-API-Key: your_api_key" \
"https://app.goroute.ai/peppol-api/api/v1/audit/cleanup?retention_days=365"
{
"success": true,
"deleted_count": 4211,
"retention_days": 365,
"message": "Deleted 4211 audit logs older than 365 days"
}
deleted_count is the number of entries removed by this call.
Errorsโ
| Status | When |
|---|---|
| 401 | The API key is missing or invalid |
| 403 | The key lacks the audit:read permission |
| 404 | GET /api/v1/audit/{log_id} โ no entry with that id in your organisation |
| 422 | A parameter is the wrong type or outside its allowed range, such as page_size=500 |
See the Error Codes Reference for the shared error format.
What this page does not sayโ
GoRoute publishes no default retention policy for audit entries here. The numbers on this page are the limits the cleanup endpoint enforces on a call you make yourself; they are not a statement about how long GoRoute keeps your trail. Nothing on this page is a description of observed behaviour in a running system โ it is written from the API's own definition.