Audit REST API Reference
The Audit Log Service stores an immutable, append-only record of every action taken on the platform. Records are written to ClickHouse via Kafka with a PostgreSQL WAL fallback — a record is never lost, even during broker outages.
Immutability guarantee: Audit records cannot be updated or deleted. GDPR "right to be forgotten" is fulfilled via a new append-only
ANONYMIZErecord — the original record stays intact but PII fields become inaccessible through the query layer (SELECT … FINAL).
See the REST API Overview for authentication, error format, pagination, and rate limiting.
For layout brevity, the /api/v1 base path prefix is omitted from the
endpoint tables below.
Endpoints
| Endpoint | Auth | Description |
|---|---|---|
POST /audit/records | ✅ | Write a single audit record |
POST /audit/records/batch | ✅ | Write multiple audit records |
GET /audit/records | ✅ | Query audit records (paginated) |
GET /audit/records/:id | ✅ | Get a single audit record |
GET /audit/entity/:type/:id | ✅ | Full history for a specific entity |
GET /audit/export | ✅ | Export audit records (JSON / CSV) |
POST /audit/anonymize | ✅ | GDPR anonymize PII for a user |
POST /api/v1/audit/records — Write Audit Record
POST https://api.septemcore.com/v1/audit/records
Authorization: Bearer <access_token>
Content-Type: application/json
{
"action": "money.wallet.credited",
"entityType": "wallet",
"entityId": "01j9pwlt0000000000000001",
"before": { "balanceCents": 10000 },
"after": { "balanceCents": 15000 },
"meta": { "txId": "tx_01j9ptx0000000000001" }
}
Response 202 Accepted (write is async via Kafka — non-blocking):
{
"auditId": "01j9paud0000000000000001",
"status": "queued",
"createdAt": "2026-04-22T04:10:00Z"
}
POST /audit/recordsalways returns202 Acceptedimmediately. The business operation that triggers the audit call is never blocked by audit write latency. If Kafka is unavailable, the record goes to the PostgreSQL WAL fallback automatically.
Audit Record Fields
| Field | Required | Description |
|---|---|---|
action | ✅ | Action type in domain.entity.verb format (e.g. auth.user.created) |
entityType | ✅ | Entity kind: user, wallet, module, role, flag, file, etc. |
entityId | ✅ | ID of the affected entity |
before | ☐ | State snapshot before the change (omit for creation events) |
after | ☐ | State snapshot after the change (omit for deletion events) |
meta | ☐ | Arbitrary JSON metadata (e.g. transaction ID, IP, request ID) |
Fields injected automatically by the Audit Service (not accepted from clients):
| Injected field | Source |
|---|---|
actorId | JWT sub |
actorIp | Request IP (from Gateway X-Forwarded-For) |
actorUserAgent | User-Agent header |
tenantId | JWT tenantId |
timestamp | Server time (immutable) |
traceId | OpenTelemetry trace ID |
POST /api/v1/audit/records/batch — Batch Write
POST https://api.septemcore.com/v1/audit/records/batch
Authorization: Bearer <access_token>
Content-Type: application/json
{
"records": [
{ "action": "auth.user.created", "entityType": "user", "entityId": "usr_001" },
{ "action": "auth.role.changed", "entityType": "user", "entityId": "usr_001",
"before": { "roles": ["viewer"] }, "after": { "roles": ["editor"] } }
]
}
| Limit | Value |
|---|---|
| Max records per batch | 500 |
| Exceeded | 400 Bad Request (problems/batch-limit-exceeded) |
GET /api/v1/audit/records — Query Records
Full-text and structured search over the audit log (ClickHouse hot store, instant response for records ≤ 90 days old):
GET https://api.septemcore.com/v1/audit/records
?action=money.wallet.credited
&actorId=01j9pusr0000000000000001
&since=2026-04-01T00:00:00Z
&until=2026-04-22T00:00:00Z
&limit=20
&cursor=<opaque_cursor>
Authorization: Bearer <access_token>
| Query param | Description |
|---|---|
action | Exact match or prefix (e.g. money.* matches all money actions) |
entityType | Filter by entity type |
entityId | Filter by entity ID |
actorId | Filter by actor (user who performed the action) |
since | ISO 8601 start timestamp (inclusive) |
until | ISO 8601 end timestamp (exclusive) |
limit | Page size (max 100, default 20) |
cursor | Opaque cursor from previous response |
Response:
{
"data": [
{
"id": "01j9paud0000000000000001",
"action": "money.wallet.credited",
"entityType": "wallet",
"entityId": "01j9pwlt0000000000000001",
"actorId": "01j9pusr0000000000000001",
"actorIp": "203.0.113.42",
"tenantId": "01j9ptnt0000000000000001",
"before": { "balanceCents": 10000 },
"after": { "balanceCents": 15000 },
"traceId": "4bf92f3577b34da6a3ce929d0e0e4736",
"timestamp": "2026-04-22T04:10:00Z"
}
],
"meta": { "cursor": "eyJhZnRlciI6Ijk...", "hasMore": true }
}
All queries use
SELECT … FINALto guarantee that anonymized records (ANONYMIZEoverlay) are correctly applied — ClickHouseReplacingMergeTreerequiresFINALto deduplicate before background merge runs.
GET /api/v1/audit/records/:id — Single Record
GET https://api.septemcore.com/v1/audit/records/01j9paud0000000000000001
Authorization: Bearer <access_token>
Returns the full audit record object (same schema as query results above).
GET /api/v1/audit/entity/:type/:id — Entity History
Returns the complete audit trail for a single entity (all actions, all actors):
GET https://api.septemcore.com/v1/audit/entity/wallet/01j9pwlt0000000000000001
?since=2026-01-01T00:00:00Z
&limit=50
Authorization: Bearer <access_token>
Useful for compliance reviews: "show me every action ever taken on wallet X".
GET /api/v1/audit/export — Export Audit Records
Exports a filtered subset of audit records as JSON or CSV. The export blocks until any pending GDPR anonymizations for records in the result set are complete — this guarantees GDPR-compliant exports.
GET https://api.septemcore.com/v1/audit/export
?format=csv
&since=2026-04-01T00:00:00Z
&until=2026-04-22T00:00:00Z
Authorization: Bearer <access_token>
Response 200 OK — file download (streamed):
Content-Disposition: attachment; filename="audit-2026-04-01_2026-04-22.csv"
Content-Type: text/csv
| Parameter | Values | Default |
|---|---|---|
format | json, csv | json |
since | ISO 8601 | Required |
until | ISO 8601 | Required |
Records older than 90 days are in S3 Glacier cold storage. Cold export requires a recovery request (3–12 hour SLA) — the API returns
202 Acceptedwith ajobIdfor cold-range exports.
POST /api/v1/audit/anonymize — GDPR Anonymize
Anonymizes all PII fields for a specific user across the entire audit log. This satisfies GDPR "right to be forgotten" while preserving the audit trail:
POST https://api.septemcore.com/v1/audit/anonymize
Authorization: Bearer <access_token>
Content-Type: application/json
{
"userId": "01j9pusr0000000000000001"
}
Response 200 OK (synchronous — waits for ClickHouse INSERT FINAL confirmation):
{
"userId": "01j9pusr0000000000000001",
"recordsAffected": 1247,
"completedAt": "2026-04-22T04:10:03Z"
}
How Anonymization Works
| Step | Detail |
|---|---|
| 1. Valkey lock | SET anonymizing:{userId} 1 EX 60 NX — blocks concurrent SELECTs for this userId while write is in flight (polling 100 ms, max 5 s) |
| 2. New ANONYMIZE record | Appended to ClickHouse — email → [REDACTED], ip → 0.0.0.0, name → [REDACTED] |
| 3. Query layer | SELECT … FINAL sees only the latest (ANONYMIZE) version — original PII is inaccessible via all API endpoints |
| 4. Cold storage | userId added to anonymization_log (PostgreSQL). Monthly background job restores Glacier segments, applies anonymizations, and re-uploads clean versions |
| 5. Exceptions | Financial records (money.*) are never anonymized — AML/KYC compliance |
Write Pipeline — Dual-Write Guarantee
Primary: Service → Kafka (platform.audit.events) → Consumer → ClickHouse
Fallback: Kafka unavailable → PostgreSQL audit_wal table
→ Background goroutine (30-sec ticker) → replay to Kafka
| Step | Detail |
|---|---|
| Kafka OK | Events buffered up to 7 days. ClickHouse consumer catches up on recovery |
| Kafka down | Fallback to audit_wal PostgreSQL table. WAL retention: 7 days |
| ClickHouse down | Consumer waits; Kafka holds events — no data loss |
| Business operation | kernel.audit().record() failure never blocks the calling operation |
Retention Policy
| Period | Storage | Access | Configuration |
|---|---|---|---|
| 0–90 days | ClickHouse (hot) | Instant millisecond search | AUDIT_HOT_DAYS=90 |
| 91 days – 7 years | S3 Glacier (cold) | On-request, 3–12 h restore SLA | AUDIT_COLD_YEARS=7 |
| After 7 years | Permanently deleted | No access | AML/KYC requirement |
ClickHouse TTL: TTL toDate(timestamp) + INTERVAL 90 DAY TO VOLUME 's3_cold'
Compression: LZ4 (~70% size reduction, default ClickHouse codec).
S3 Lifecycle rule: Glacier → Delete after AUDIT_COLD_YEARS.
Mandatory Audit Categories
| Category | Examples |
|---|---|
| All financial transactions | Credits, debits, payouts |
| All admin actions | Commission changes, account suspensions |
| All API calls | Postbacks, webhooks, integrations |
| All settings changes | RBAC, roles, permissions |
| All tracking events | Clicks, registrations, FTDs |
| All configuration changes | Feature flags, module registry |
Error Reference
| Error type | Status | Trigger |
|---|---|---|
problems/audit-record-not-found | 404 | Audit record ID does not exist |
problems/batch-limit-exceeded | 400 | Batch write exceeds 500 records |
problems/anonymize-conflict | 409 | Concurrent anonymization for same userId in progress |
problems/export-range-too-large | 400 | Export range exceeds 90 days for hot-only export |
problems/cold-export-initiated | 202 | Range includes cold storage — async job started |
problems/anonymize-financial-record | 403 | Attempt to anonymize money.* records (AML protected) |