Audit events
Read your tenant's audit trail — every PHI access, every admin action, every export — under one Bearer token. Three endpoints: paginated list, single-event lookup, and synchronous bulk export.
The audit trail is append-only and immutable. Velora writes one row per action; you read those rows back in newest-first order, filterable by action enum, resource type, PHI involvement, or time window. The same data feeds the audit dashboard in the portal and the webhook deliveries documented in Webhooks.
List events
GET /api/v2/public/audit/eventsPaginated read of your tenant's audit events, newest first. Required scope: audit:read.
Query parameters
| Name | Type | Default | Notes |
|---|---|---|---|
| action | string | — | Filter by action enum (e.g. phi.read). |
| resource_type | string | — | Filter by resource type (e.g. claim, upload). |
| phi_involved | bool | — | true for PHI events, false for non-PHI, omit for both. |
| since | ISO-8601 | — | Inclusive lower bound (UTC). |
| until | ISO-8601 | — | Exclusive upper bound (UTC). |
| limit | int | 100 | 1..1000. |
| cursor | opaque | — | Pass the previous response's next_cursor verbatim. |
Example request
curl -G \
-H "Authorization: Bearer $VELORA_API_KEY" \
--data-urlencode "since=2026-05-01T00:00:00+00:00" \
--data-urlencode "until=2026-05-08T00:00:00+00:00" \
--data-urlencode "phi_involved=true" \
--data-urlencode "limit=100" \
https://api.velora.health/api/v2/public/audit/eventsResponse
{
"events": [
{
"event_id": "8b2e5c11-1234-4abc-89de-0123456789ab",
"timestamp": "2026-05-03T14:22:01+00:00",
"action": "phi.read",
"user_id": "u-1234",
"user_role": "data_analyst",
"resource_type": "claim",
"resource_id": "C-7890",
"fields_accessed": "[\"diagnosis_code\"]",
"phi_involved": true,
"tenant_id": "acme",
"source_ip": "203.0.113.42",
"user_agent": "Mozilla/5.0 ...",
"justification": "underwriting renewal",
"success": true,
"details": "{\"workflow\":\"renewal\"}"
}
],
"page": {
"limit": 100,
"returned": 1,
"next_cursor": "MjAyNi0wNS0wM1QxNDoyMjowMSswMDowMA==",
"has_more": false
}
}Pagination loop
Walk the full window by passing the previous page's next_cursor on each call. Stop when has_more is false.
import httpx, os
API = "https://api.velora.health"
HEADERS = {"Authorization": f"Bearer {os.environ['VELORA_API_KEY']}"}
cursor = None
with httpx.Client(headers=HEADERS, timeout=30.0) as client:
while True:
params = {"limit": 1000, "phi_involved": "true"}
if cursor:
params["cursor"] = cursor
r = client.get(f"{API}/api/v2/public/audit/events", params=params)
r.raise_for_status()
page = r.json()
for event in page["events"]:
handle(event)
if not page["page"]["has_more"]:
break
cursor = page["page"]["next_cursor"]Get a single event
GET /api/v2/public/audit/events/{event_id}Returns the full event by id. Required scope: audit:read.
404 is returned both when the event does not exist and when it belongs to a different tenant. Cross-tenant existence is collapsed — there is no oracle. If you expect an event and get a 404, the most likely causes are a typo in the id or the event having been written under a different tenant.Export
POST /api/v2/public/audit/exportSynchronous bulk export. Same filter shape as /events. Returns up to 10,000 rows per call. Required scope: audit:export.
Formats
Pass the format query parameter:
json(default): one JSON object with{events, count, truncated}.ndjson: one JSON event per line. Stream-friendly; pipes cleanly into Splunk / Datadog / S3-as- cold-storage.
Truncation
The response includes X-Velora-Truncated (true | false) so you can detect when you hit the 10,000-row cap. On truncation, take the last event's timestamp and re-run with until=<that-timestamp> to keep walking backwards.
Example request — NDJSON to file
curl -X POST \
-H "Authorization: Bearer $VELORA_API_KEY" \
-H "Content-Type: application/json" \
-D - \
-o events.ndjson \
"https://api.velora.health/api/v2/public/audit/export?format=ndjson" \
-d '{
"since": "2026-05-01T00:00:00+00:00",
"until": "2026-05-08T00:00:00+00:00",
"phi_involved": true
}'
# Inspect the truncation flag in the response headers:
# X-Velora-Truncated: falseEvent shape
Every event in the response carries the same field set. New fields may be added without warning — tolerate unknown fields. Removal or rename is a breaking change and bumps the major API version.
| Field | Type | Notes |
|---|---|---|
| event_id | UUID | Stable, unique. Pass to /events/{id} for the canonical row. |
| timestamp | ISO-8601 | UTC, millisecond precision. |
| action | enum | Dotted slug, e.g. phi.read, admin.key.create. |
| user_id, user_role | string | Actor identity inside your tenant. |
| resource_type, resource_id | string | What was acted on. Resource ids may be tokenized in sidecar mode. |
| fields_accessed | JSON string | Sub-fields read on PHI access. Empty for non-PHI events. |
| phi_involved | bool | PHI flag — drives HIPAA reporting filters. |
| source_ip, user_agent | string | Origin metadata of the request. |
| justification | string | Optional caller-supplied reason for PHI access. |
| success | bool | true on accepted access; false on denied / errored. |
| details | JSON string | Free-form context per action type. Parse with care. |
Errors
See the authentication error table for 401/403/429. Endpoint-specific errors:
| Status | Meaning |
|---|---|
| 404 | Event not found (or belongs to a different tenant — see callout above). |
| 422 | Invalid filter values, malformed cursor, or limit outside 1..1000. |
Next
To push events into your SIEM in real time instead of polling, register a webhook. For runnable end-to-end scripts that hit /events and /export, see Code samples.