API · Events

Events are the immutable audit records your application sends. Recording authenticates with a project API key; reading authenticates with a personal access token and requires viewer on the project.

The event object

{
  "id": "0b6f8a3e-6f43-4a0e-9a53-1f2d0c4b7e11",
  "tenant_id": "acme-corp",
  "occurred_at": "2026-10-30T14:07:02.551Z",
  "actor": { "type": "user", "id": "u_42", "display_name": "Alice", "email": "[email protected]" },
  "action": "invoice.paid",
  "target": { "type": "invoice", "id": "inv_981" },
  "metadata": { "amount_cents": 4900, "currency": "usd" },
  "origin": { "ip": "203.0.113.7", "user_agent": "Mozilla/5.0 ...", "request_id": "req_7f3a" },
  "result": { "status": "ok", "code": 200 },
  "change": { "before": { "status": "open" }, "after": { "status": "paid" } },
  "idempotency_key": "invoice-981-paid"
}
Field Required Description
id yes A UUID you generate, new for every event.
action yes What happened, as a dotted verb: user.login, invoice.paid.
actor no Who did it: type, id, display_name, email.
occurred_at no RFC 3339 timestamp. Defaults to the time the server receives the event.
tenant_id no Your end-customer's ID, if your product is multi-tenant. Leave it out otherwise.
target no What was acted on: type, id.
metadata no Any JSON object of extra context.
origin no Where the request came from: ip, user_agent, request_id.
result no The outcome: status (ok, error, or denied), code, message.
change no For mutations: before and after snapshots, and optionally patch.
idempotency_key no Makes retries safe. See Retries.

This is the same shape the SDKs send; the Go event shape page describes each field in more depth.

Record an event

POST /v1/projects/{projectID}/events

Authenticates with a project API key for the project in the path. A key from a different project is rejected with 401. The body is one event object. Returns 202 Accepted with an empty body once the event is stored.

curl -X POST https://api.everscribe.io/v1/projects/$PROJECT_ID/events \
  -H "Authorization: Bearer $EVERSCRIBE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "id": "0b6f8a3e-6f43-4a0e-9a53-1f2d0c4b7e11",
    "action": "invoice.paid",
    "actor": { "type": "user", "id": "u_42" },
    "target": { "type": "invoice", "id": "inv_981" }
  }'

The body is strict about its top-level fields: an unknown field such as a misspelled occured_at fails with 400 invalid request body. The contents of the nested objects aren't checked. Bodies are capped at 4 MB.

Status Body When
400 invalid request body Not JSON, an unknown top-level field, or over 4 MB.
400 event.action is required action is missing or empty.
400 event.id is required / event.id must be a valid UUID Bad id.
401 unauthorized Missing, malformed, or revoked key; key for another project; project deleted.
409 event id already exists The id is already used by an event in a different project.

Recording never fails because of your plan's monthly event quota. Going over it sends you an email, but events are still accepted.

Record a batch

POST /v1/projects/{projectID}/events/batch

Same authentication as a single event. The body wraps an array:

{ "events": [ { "id": "...", "action": "user.login" }, { "id": "...", "action": "user.logout" } ] }

Returns 202 with an empty body. Events already stored in this project (same id or idempotency_key) are skipped and the rest are stored. Otherwise the batch is all or nothing: if any event fails validation, the response is the 400 for that event, and if any id belongs to another project, the response is 409. Either way, none of the batch is stored. An empty events array is accepted and does nothing. There's no limit on the number of events beyond the 4 MB body cap; the SDKs send about 100 per batch.

Retries

Ingest is safe to retry. Re-send the event with the same id: if the project already has an event with that id, the duplicate is skipped and you still get 202. That makes retries after a timeout or a dropped connection safe, even when the first attempt did get through.

idempotency_key adds a second, caller-chosen key for the same purpose. If an event with the same key already exists in the project, the new one is skipped and you get 202, even when its id is different. Use it when the same real-world action might be recorded more than once with fresh ids, and generate it from something stable on your side, like invoice-981-paid.

Event ids are unique across all of Everscribe, not just your project. An id already used in a different project returns 409 event id already exists. Generate ids as random UUIDs (v4) and this won't happen.

List events

GET /v1/projects/{id}/events

Returns events newest first.

Query parameter Description
limit Events per page. Default 25, maximum 200 (larger values are lowered to 200).
cursor The next_cursor from the previous page.
since RFC 3339. Only events at or after this time.
before RFC 3339. Only events before this time.
action Exact match on action.
actor Substring match across actor.id, actor.display_name, and actor.email.
actor_type Exact match on actor.type.
target_type, target_id Exact match on target.type / target.id.
tenant_id Exact match on tenant_id.
result_status Exact match on result.status.
origin_ip Exact match on origin.ip.
q A query expression, for anything the parameters above can't express.

All filters combine with AND.

curl "https://api.everscribe.io/v1/projects/$PROJECT_ID/events?action=user.login&since=2026-10-01T00:00:00Z&limit=100" \
  -H "Authorization: Bearer $EVERSCRIBE_PAT"
{
  "events": [ { "id": "0b6f8a3e-...", "action": "user.login", "...": "..." } ],
  "next_cursor": "MjAyNi0xMC0zMFQxNDowNzowMi41NTFafDBiNmY4YTNl..."
}

To page, pass next_cursor back as cursor, keeping the other parameters the same. When next_cursor is missing from the response, you've reached the last page. Treat the cursor as an opaque string.

Status Body When
400 invalid limit limit is not a positive integer.
400 invalid cursor The cursor wasn't produced by this endpoint.
400 invalid filter: ... since or before isn't RFC 3339, or q parses but can't run.
400 JSON, see below q can't be parsed.

Query expressions

q takes the same filter language as the dashboard's Query tab: clauses of the form field:value, joined with AND (uppercase).

action:user.login AND result.status:denied AND occurred_at:>=2026-10-01T00:00:00Z
Operator Meaning
field:value Equals.
field:!value Not equal.
field:~value Contains.
field:>value, :>=, :<, :<= Comparisons, e.g. on occurred_at or result.code.
field:[a TO b] Between.
field:(a OR b OR c) Any of.

Fields: action, actor (contains only, actor:~alice, searching id, name, and email), actor.id, actor.type, target.type, target.id, tenant_id, result.status, result.code, origin.ip, origin.user_agent, occurred_at, and metadata.<key> for any metadata key. Wrap values containing spaces in double quotes: metadata.environment:"prod canary".

: and :(... OR ...) on metadata.<key> only match string values, so metadata.amount_cents:4900 won't find "amount_cents": 4900. For numbers, use a comparison or a range, which compare numerically: metadata.amount_cents:[4900 TO 4900].

URL-encode q. With curl, -G --data-urlencode does it for you:

curl -G https://api.everscribe.io/v1/projects/$PROJECT_ID/events \
  -H "Authorization: Bearer $EVERSCRIBE_PAT" \
  --data-urlencode 'q=action:user.login AND result.status:denied'

If a clause in q covers the same field as one of the plain query parameters, q wins, and the response includes an X-Filter-Warnings header listing the overridden fields as comma-separated query paths (e.g. occurred_at for since/before, result.status for result_status).

A q that can't be parsed returns 400 with a JSON body, the one JSON error in this API:

{
  "error": "parse_error",
  "message": "expected 'AND' between clauses",
  "position": 18,
  "snippet": "action:user.login and actor.id:u_42"
}

position is the 0-based byte offset of the problem in snippet, which echoes your q.

A q that parses but can't run returns a plain-text 400 invalid filter: <reason> instead. Examples: a value of the wrong type (occurred_at:>=yesterday, result.code:abc), actor with any operator other than :~, or :~ on occurred_at or result.code.

Get an event

GET /v1/projects/{id}/events/{event_id}

Returns {"event": {...}}, or 404 event not found.

curl https://api.everscribe.io/v1/projects/$PROJECT_ID/events/$EVENT_ID \
  -H "Authorization: Bearer $EVERSCRIBE_PAT"