Skip to main content

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 ANONYMIZE record — 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.

note

For layout brevity, the /api/v1 base path prefix is omitted from the endpoint tables below.


Endpoints

EndpointAuthDescription
POST /audit/recordsWrite a single audit record
POST /audit/records/batchWrite multiple audit records
GET /audit/recordsQuery audit records (paginated)
GET /audit/records/:idGet a single audit record
GET /audit/entity/:type/:idFull history for a specific entity
GET /audit/exportExport audit records (JSON / CSV)
POST /audit/anonymizeGDPR 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/records always returns 202 Accepted immediately. 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

FieldRequiredDescription
actionAction type in domain.entity.verb format (e.g. auth.user.created)
entityTypeEntity kind: user, wallet, module, role, flag, file, etc.
entityIdID of the affected entity
beforeState snapshot before the change (omit for creation events)
afterState snapshot after the change (omit for deletion events)
metaArbitrary JSON metadata (e.g. transaction ID, IP, request ID)

Fields injected automatically by the Audit Service (not accepted from clients):

Injected fieldSource
actorIdJWT sub
actorIpRequest IP (from Gateway X-Forwarded-For)
actorUserAgentUser-Agent header
tenantIdJWT tenantId
timestampServer time (immutable)
traceIdOpenTelemetry 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"] } }
]
}
LimitValue
Max records per batch500
Exceeded400 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 paramDescription
actionExact match or prefix (e.g. money.* matches all money actions)
entityTypeFilter by entity type
entityIdFilter by entity ID
actorIdFilter by actor (user who performed the action)
sinceISO 8601 start timestamp (inclusive)
untilISO 8601 end timestamp (exclusive)
limitPage size (max 100, default 20)
cursorOpaque 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 … FINAL to guarantee that anonymized records (ANONYMIZE overlay) are correctly applied — ClickHouse ReplacingMergeTree requires FINAL to 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
ParameterValuesDefault
formatjson, csvjson
sinceISO 8601Required
untilISO 8601Required

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 Accepted with a jobId for 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

StepDetail
1. Valkey lockSET 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 recordAppended to ClickHouse — email → [REDACTED], ip → 0.0.0.0, name → [REDACTED]
3. Query layerSELECT … FINAL sees only the latest (ANONYMIZE) version — original PII is inaccessible via all API endpoints
4. Cold storageuserId added to anonymization_log (PostgreSQL). Monthly background job restores Glacier segments, applies anonymizations, and re-uploads clean versions
5. ExceptionsFinancial 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
StepDetail
Kafka OKEvents buffered up to 7 days. ClickHouse consumer catches up on recovery
Kafka downFallback to audit_wal PostgreSQL table. WAL retention: 7 days
ClickHouse downConsumer waits; Kafka holds events — no data loss
Business operationkernel.audit().record() failure never blocks the calling operation

Retention Policy

PeriodStorageAccessConfiguration
0–90 daysClickHouse (hot)Instant millisecond searchAUDIT_HOT_DAYS=90
91 days – 7 yearsS3 Glacier (cold)On-request, 3–12 h restore SLAAUDIT_COLD_YEARS=7
After 7 yearsPermanently deletedNo accessAML/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

CategoryExamples
All financial transactionsCredits, debits, payouts
All admin actionsCommission changes, account suspensions
All API callsPostbacks, webhooks, integrations
All settings changesRBAC, roles, permissions
All tracking eventsClicks, registrations, FTDs
All configuration changesFeature flags, module registry

Error Reference

Error typeStatusTrigger
problems/audit-record-not-found404Audit record ID does not exist
problems/batch-limit-exceeded400Batch write exceeds 500 records
problems/anonymize-conflict409Concurrent anonymization for same userId in progress
problems/export-range-too-large400Export range exceeds 90 days for hot-only export
problems/cold-export-initiated202Range includes cold storage — async job started
problems/anonymize-financial-record403Attempt to anonymize money.* records (AML protected)