Skip to main content

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.

Two things to read before you use this API
  1. The CSV export stops at 10,000 rows and does not tell you it stopped. See Export the trail as CSV.
  2. POST /api/v1/audit/cleanup permanently 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 pathWhat it does
GET /api/v1/auditList entries, newest first, paginated and filterable
GET /api/v1/audit/{log_id}One entry in full
GET /api/v1/audit/statsCounts by severity and by action over a date range
GET /api/v1/audit/actionsThe action names and severities you can filter on
GET /api/v1/audit/actorsThe 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.csvDownload the filtered trail as a CSV file
POST /api/v1/audit/cleanupPermanently 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"
}
FieldNotes
idThe entry's own identifier, a UUID
org_idYour organisation
actor.idMay be null โ€” for example when the action had no signed-in user
actor.typeWhat kind of caller it was, such as a user or an API key
actor.name, actor.ipMay be null
actionA dotted name such as invoice.sent. The full list comes from GET /api/v1/audit/actions
severityOne of debug, info, warning, error, critical
resourceThe object acted on. Absent entirely when the entry has no resource type
descriptionA short human sentence
metadataFree-form extra data; an empty object when there is none
changes{"old": โ€ฆ, "new": โ€ฆ} when the action recorded a before and after, otherwise null
request_idCorrelates the entry with a single API request; may be null
created_atUTC timestamp in ISO 8601

List entriesโ€‹

GET /api/v1/audit โ€” returns a page of entries, newest first.

ParameterDefaultLimits
page11 or higher
page_size501 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.

ParameterDefaultLimits
limit1001 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.

ParameterDefault
date_from30 days ago
date_tonow
{
"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 only

The 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 stops at 10,000 rows and says nothing about it

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:

FilterOn the list endpointOn the CSV export
action, actor, resource_type, date_from, date_to, searchyesyes
severityyesno
resource_idyesno

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.

This destroys audit records and cannot be undone

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.

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

StatusWhen
401The API key is missing or invalid
403The key lacks the audit:read permission
404GET /api/v1/audit/{log_id} โ€” no entry with that id in your organisation
422A 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.