Skip to main content
API reference · Audit events

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/events

Paginated read of your tenant's audit events, newest first. Required scope: audit:read.

Query parameters

NameTypeDefaultNotes
actionstringFilter by action enum (e.g. phi.read).
resource_typestringFilter by resource type (e.g. claim, upload).
phi_involvedbooltrue for PHI events, false for non-PHI, omit for both.
sinceISO-8601Inclusive lower bound (UTC).
untilISO-8601Exclusive upper bound (UTC).
limitint1001..1000.
cursoropaquePass the previous response's next_cursor verbatim.
Cursor format
The cursor is a base64url-encoded ISO timestamp; this is an implementation detail and may change. Do not parse the cursor, generate one client-side, or persist its decoded form across versions — treat it as opaque.

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/events

Response

{
  "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.

Tenant isolation
A 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/export

Synchronous 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: false

Event 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.

FieldTypeNotes
event_idUUIDStable, unique. Pass to /events/{id} for the canonical row.
timestampISO-8601UTC, millisecond precision.
actionenumDotted slug, e.g. phi.read, admin.key.create.
user_id, user_rolestringActor identity inside your tenant.
resource_type, resource_idstringWhat was acted on. Resource ids may be tokenized in sidecar mode.
fields_accessedJSON stringSub-fields read on PHI access. Empty for non-PHI events.
phi_involvedboolPHI flag — drives HIPAA reporting filters.
source_ip, user_agentstringOrigin metadata of the request.
justificationstringOptional caller-supplied reason for PHI access.
successbooltrue on accepted access; false on denied / errored.
detailsJSON stringFree-form context per action type. Parse with care.

Errors

See the authentication error table for 401/403/429. Endpoint-specific errors:

StatusMeaning
404Event not found (or belongs to a different tenant — see callout above).
422Invalid 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.