Skip to content

Smpl Audit

Smpl Audit is an append-only, permanent record of "things that happen" — events your application emits to a small, RESTful API for compliance, security review, customer-support investigations, or any other reason you need a tamper-evident log of who did what and when.

You write events with a single API call. You query them later by actor, resource, event type, time range, or any combination.

What an event looks like

Each event carries:

FieldDescription
event_typeWhat happened, in {resource_type}.{verb} form. Example: order.placed, user.login_failed, payment.refunded.
resource_typeThe kind of thing the event acts on (order, user, payment).
resource_idIdentifier of the affected resource. Any text — your own domain identifiers, not constrained to UUIDs.
descriptionOptional free-text description of the event. Included alongside resource_id in the filter[search] substring target.
severityOne of TRACE, DEBUG, INFO, WARN, ERROR, FATAL. Optional; omitted events are recorded at INFO. Always present on read.
categoryOptional free-form bucket label — auth, billing, config-change, anything you choose. Drives the filter[category] filter and the GET /api/v1/categories discovery endpoint.
actor_typeFree-form string identifying the kind of actor — for example USER, API_KEY, SYSTEM, or any custom value (EXTERNAL_SERVICE, WEBHOOK, ...). Optional; the audit service does not backfill it.
actor_idFree-form identifier of the actor. Any string scheme works — UUIDs, email addresses, opaque tokens — not constrained to UUIDs. Optional.
actor_labelDisplay string captured at write time (e.g. alice@example.com). Doesn't change if the actor is later renamed or removed. Optional.
occurred_atWhen the event happened in your application. Defaults to event-creation time if not supplied.
environmentThe environment the event occurred in (production, staging, ...). On write, optionally names the target environment: omit it and a single-environment credential implies it, while a multi-environment or unrestricted credential must name it (and the named environment must be one you can access). Always present on read as the resolved environment. See Environment scoping below.
idempotency_keyAuto-generated from event content; supply your own to deduplicate retries on a stable key (e.g. an order id with a status).
dataFree-form JSON. The single payload field on every event — record snapshots, request IDs, IP addresses, deploy SHAs, and any other contextual detail you want preserved with the event. See Forensic detail below.

A representative event:

json
{
  "event_type": "order.placed",
  "resource_type": "order",
  "resource_id": "o-9876",
  "description": "Alice placed order o-9876 ($89.90, 2 items).",
  "severity": "INFO",
  "category": "checkout",
  "actor_type": "USER",
  "actor_id": "8a1c5e3a-2b9f-4d12-9e76-1d3c8b7a4e5f",
  "actor_label": "alice@example.com",
  "occurred_at": "2026-05-08T14:22:18Z",
  "environment": "production",
  "data": {
    "snapshot": {
      "id": "o-9876",
      "customer_id": "c-1234",
      "items": [
        { "sku": "shirt-md-blue", "qty": 1, "price_cents": 4995 },
        { "sku": "hat-onesize",   "qty": 1, "price_cents": 3995 }
      ],
      "total_cents": 8990
    },
    "ip": "203.0.113.42",
    "request_id": "req-d4f8a1c0"
  }
}

Events are immutable. Once written, an event row stays — there is no PUT, PATCH, or DELETE on the events endpoint. Retention sweeps eventually drop events that exceed your tier's retention window, but no one can rewrite history within that window.

Environment scoping

Every audit event belongs to exactly one environment (production, staging, and so on). On write you optionally name the target environment in the request body's environment field; on read it's always present as the resolved environment.

How the environment is resolved when you record an event:

  • An API key scoped to a single environment records into that environment automatically. Nothing extra to send — omit the environment field.
  • A multi-environment or unrestricted credential must name the target environment in the request body's environment field. The smplkit SDK sets this field for you from the environment you configured on the client, so SDK callers never set it by hand. For raw REST calls, include environment in the JSON body yourself.

The named environment must be one you can access and must exist and be managed for your account. The same content recorded in two environments produces two distinct events — one per environment.

Reads cover every environment you can access. Listing events, searching, downloading, and the discovery endpoints (/resource_types, /event_types, /categories) return data from all environments your credential may access by default. To narrow a read, pass a comma-separated filter[environment] (e.g. production,staging); a single-environment key only ever sees its one environment. The reserved value smplkit selects platform change-history events smplkit records about your own resources — included by default when your plan grants change history, and returning 402 if you request it explicitly without that entitlement. Fetching a single event by id (GET /api/v1/events/{id}) returns the event only if its environment is one your credential may access; otherwise it returns 404, identical to a non-existent id, so existence never leaks across environments.

Breaking change: environment scoping

Environment scoping changes the behavior of the audit API. If you were using it before this change, review the following:

  • Events now carry an environment. Every event belongs to one environment, resolved at record time. The same content recorded in two environments is now two distinct events rather than one.
  • Recording with a multi-environment or unrestricted credential now requires naming the target environment in the request body's environment field. Single-environment API keys are unaffected — the environment is implied. The smplkit SDK sets the field for you from the client's configured environment, so SDK callers don't need to change anything. Raw REST callers using a multi-environment credential must add the environment field to the request body.
  • Reads cover every environment you can access. Listing, searching, downloading, and the discovery endpoints return data across all environments your credential may access by default. Narrow to a subset with a comma-separated filter[environment].
  • Forwarders changed shape. Forwarder enablement and per-environment delivery configuration moved into a new environments map, and the top-level enabled field is now read-only. See Migrating forwarders to environment scoping.

Querying events

The query API supports filtering on every event field. Common patterns:

  • By actor. Every event a specific user took, or every event type a particular API key performed.
  • By resource. The full history of one record — every change to one order, one user, one configuration item.
  • By resource search. A case-insensitive substring match on resource_id or description via filter[search] — useful for finding events without knowing the exact identifier.
  • By event type. Every login failure across the account, or every refund.
  • By severity. filter[severity]=ERROR to surface only events you flagged as ERROR. Exact match against one of the six labels.
  • By category. filter[category]=auth to scope to a single bucket you've been tagging events with.
  • By time range. This month's events, or yesterday's, bounded as tightly as you need.

GET /api/v1/events accepts calls with no parameters — that returns the account's most recent events, paginated. Two filter-combination rules to be aware of:

  • filter[resource_id] must be accompanied by filter[resource_type] (the underlying index is keyed on the pair).
  • filter[search] must be accompanied by either filter[occurred_at] or filter[resource_type] + filter[resource_id] (substring matching has no index, so an unbounded substring scan is rejected).

filter[occurred_at] is otherwise unbounded — any span is allowed; pagination is what caps the response.

Pagination is cursor-based; page[size] defaults to 1000 and must not exceed 1000. See API Reference — Audit for the exact filter syntax.

The smplkit console's Events page is a UI front-end on the same query API. Its Resource type and Event type filter dropdowns are populated from GET /api/v1/resource_types and GET /api/v1/event_types — small, fast endpoints that return the distinct slugs your account has actually emitted, sorted alphabetically and cursor-paginated. The dropdowns also support a cascading filter: passing ?filter[resource_type]=order to /api/v1/event_types returns only the event types you've recorded under order.

GET /api/v1/categories is the equivalent for the category field — it returns the distinct category values your account has tagged events with. The severity filter has no discovery endpoint because severity is the same fixed six-value enum everywhere.

Downloading events

The audit API ships two delivery paths for streaming a filtered events download. Both paths share a single server-side serializer — same CSV column list, same JSONL line shape, same flat-memory cursor walk — so it's only the authorization model that differs.

From a script or CLI (header-authenticated)

GET /api/v1/events accepts an optional format query parameter that switches the endpoint from paginated JSON output to a streaming file download. Two values:

  • format=CSVtext/csv download. One row per event, with the event payload (data) serialized as a JSON string in a single cell. Fixed column order: id, environment, occurred_at, created_at, event_type, resource_type, resource_id, severity, category, description, actor_type, actor_id, actor_label, idempotency_key, do_not_forward, data.
  • format=JSONLapplication/x-ndjson download. One JSON object per line, with data preserved as a nested object.

Every filter accepted by the read endpoint applies — the download is the full filtered result set, ignoring page[size] and page[after]. sort applies; default is -occurred_at. Available at every tier, alongside the read endpoint itself. Authenticate the same way as any other audit read — Authorization: Bearer <api-key-or-jwt>. Like every other read, the download covers every environment your credential can access by default; narrow it with a comma-separated filter[environment].

curl -H "Authorization: Bearer $SMPLKIT_API_KEY" \
     -o orders.csv \
     "https://audit.smplkit.com/api/v1/events?format=CSV&filter[resource_type]=order&filter[occurred_at]=[2026-05-01T00:00:00Z,2026-06-01T00:00:00Z)"

The download streams from the server, so memory stays flat for both client and server even on large result sets. The response carries Content-Disposition: attachment with a UTC-timestamped filename (e.g. audit-events-20260527T103000Z.csv). An invalid format value returns 400 Bad Request.

From a browser (signed download URL)

The smplkit console's Events page exposes a Download control next to the toolbar's refresh button; the per-resource History tab on each resource detail page exposes the same control scoped to that resource. These trigger a native streaming download — the browser writes straight to disk, with no JavaScript buffering of the file body and no upper bound on size.

Under the covers, the console uses a two-call mint-then-stream pattern that any browser caller can use directly:

  1. MintPOST /api/v1/exports with the same customer JWT the rest of the console uses. The JSON:API body specifies format (CSV or JSONL) and any subset of the filters the read endpoint accepts. The response carries a JSON:API export resource whose attributes.url is a short-lived (30-second) signed download URL and whose attributes.expires_at is the absolute expiry.
  2. Stream — open the returned url in the browser (anchor click, top-level navigation, or window.open). No Authorization header is required at download time — the URL's HMAC signature is the authorization. The browser streams the response straight to disk via the same Content-Disposition: attachment header.
POST /api/v1/exports
Content-Type: application/vnd.api+json
Authorization: Bearer <session-jwt>

{
  "data": {
    "type": "export",
    "attributes": {
      "format": "CSV",
      "filter[resource_type]": "order",
      "filter[occurred_at]": "[2026-05-01T00:00:00Z,2026-06-01T00:00:00Z)"
    }
  }
}
HTTP/1.1 201 Created
Content-Type: application/vnd.api+json

{
  "data": {
    "id": "11111111-2222-3333-4444-555555555555",
    "type": "export",
    "attributes": {
      "format": "CSV",
      "filter[resource_type]": "order",
      "filter[occurred_at]": "[2026-05-01T00:00:00Z,2026-06-01T00:00:00Z)",
      "url": "https://audit.smplkit.com/api/v1/exports/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.…",
      "expires_at": "2026-05-27T12:00:30Z"
    }
  }
}

The signed URL is stateless and replayable until expiry — concurrent or duplicate opens (browser retries, AV scanners, prefetchers) all succeed. Any failure mode at the consume endpoint (bad signature, expired, malformed) returns 404 Not Found; the endpoint deliberately doesn't disclose which check failed. Mint a new URL if the existing one expired.

The companion advanced-search endpoint POST /api/v1/events/search (JSON Logic body) does not currently accept format — see API Reference — Audit for the canonical surface.

Wiping an account

Audit data is cleared as part of the platform-level account wipe — POST /api/v1/accounts/current/actions/wipe on the app service. That action wipes every per-account resource across all four product services in a single transaction, including the entire audit trail (events, quota counters, forwarders, deliveries, and the side tables that back the Events page filter dropdowns). There is no audit-only wipe endpoint; the operation is intentionally only initiated through the platform's confirmation flow.

The wipe is irreversible. The console renders a confirmation dialog before submitting the action.

Forensic detail

The data field is unconstrained JSON — anything that helps your future self reconstruct what happened. To record the post-save state of a domain object, smplkit's own services use data.snapshot, but the convention is yours to set; pick the shape that fits your data. Snapshots are full state, not diffs — diffs are computed at read time when you need them. Common uses: capture the post-save state of a domain object so you can answer "what did this look like at the time?" later, attach the input that triggered an event for replay, or record a multi-resource view that doesn't fit into the primary resource_type/resource_id pair.

Retention and quota

Each plan publishes a per-month event quota and a retention window. Both vary by plan; once an event passes the retention window it's removed automatically.

Your current plan, quota, and usage are visible under Account → Subscriptions in the smplkit console.

SIEM streaming

Events are also forwardable to your SIEM (Splunk, Datadog, Sumo Logic) or any HTTP endpoint as they're recorded. See SIEM streaming.