Events API rules
Scope
This page defines runtime rules for:
POST /eventsPOST /events/batch
It focuses on behavior rules and processing semantics. Endpoint fields are covered by API reference docs.
Event identity and request idempotency
Every item must provide an opaque event_key. Vluna binds that key to one event
fact within the resolved Billing User:
- the first
(realm_id, billing_user_id, event_key)write creates the stored event - every later submission of that key for the same user returns the stored event, even when event fields, payload, or labels differ
- different users may reuse the same event key
- two events may have identical content when they use different event keys
The key defines the producer's desired uniqueness boundary. For example, use
signup for one signup event per user, login:2026-09-08 for one login event per
user per day, or invite-accepted:inv_456 for one acceptance of an invitation.
Vluna does not require event type or event key registration.
Idempotency-Key is separate: it identifies the HTTP write request. A batch has
one request-level Idempotency-Key, while every item has its own durable
event_key. Reuse the request key for an exact transport retry. Use a new request
key when intentionally making another submission that converges on an existing
business event key.
request_hash is a diagnostic digest of the canonicalized facts accepted by the
first write:
- Realm and resolved Billing Account/User
semantic_kindevent_type- normalized
occurred_at subject_ref- nested
payload - normalized labels
Neither event_key nor the transport Idempotency-Key is part of that digest.
The digest is not used to decide whether an event key is a duplicate.
User, account, and realm scoping
- Billing user is resolved from
X-Principal-Id + X-User-Idor from bearer token claims, not caller-provided body identity. - Billing account remains the parent payor scope for the resolved user.
- Writes are user-scoped and realm-scoped.
- Missing runtime user context rejects the request.
Validation rules
- Runtime context must include a resolved
billing_user_id. event_keyis required, must not have surrounding whitespace, and has a max length of 255.semantic_kindmust beactivityoroutcome.occurred_atmust be a valid timestamp.event_typeis developer-managed, case-sensitive, required, must have no surrounding whitespace, and has a max length of 128.subject_refis optional; max length is 256.payloadmust be a JSON object; omitted payload defaults to{}.
occurred_at semantics
occurred_at is the business event time, not request arrival time.
How it is used:
- Policy version resolution uses
occurred_atto choose the effective active version. - Contract term resolution uses
occurred_atto select effective term values. - Event-to-rating conversion evaluates rules against the event's
occurred_at.
Impact on downstream ratings and reports:
- Late-arriving events are rated under the policy/contract context that was effective at their
occurred_at. - Time-window reports and traceability views should treat event-time and rating-time as different dimensions.
- Correct
occurred_atis required for accurate backfill, reconciliation, and period analysis.
Recommended usage:
- Send UTC timestamps from your source system's event clock.
- Preserve original event time on retries and replays.
- Do not overwrite
occurred_atwith API call time.
Payload design for E2R rules
payload is a freeform object, but E2R rules rely on stable field paths and stable value types.
Design rules:
- Keep field paths stable over time (for example
usage.input_tokens). - Keep type stable per field (do not alternate between number/string/object).
- Store billable quantities as numeric values in minor units.
- Use explicit schema versioning (for example
schema_version) when payload shape evolves. - Keep high-cardinality diagnostics out of billable rule paths.
How E2R reads payload:
- Single-event rules read payload by dot-path (for example
payload_int: usage.input_tokens). - Predicate comparisons and numeric expressions coerce numeric-like values.
- Aggregate numeric ops (
sum/avg/min/max) only work reliably on numeric sources.
Recommended split:
payload: business facts and quantities used by pricing/rating logic.labels: low-cardinality dimensions for filtering and reporting dimensions.
Example payload:
{
"schema_version": "v1",
"usage": {
"input_tokens": 1200,
"output_tokens": 340
},
"model": {
"name": "gpt-5",
"tier": "pro"
},
"request": {
"region": "us-central1"
}
}
/events response semantics
- First accepted write returns
201. - A later submission of the same key for the same user returns
200, regardless of submitted content. - Response returns
event_id,event_key,request_hash, and the stored fact.
/events/batch response semantics
Batch always returns 207 with per-item status.
Per-item status values:
accepted: inserted as new eventduplicate: deduped against existing eventinvalid: payload-level validation failedfailed: processing failed
Batch summary includes:
accepted_countfailed_count
Label persistence rules
- Labels are stored only when an event is newly accepted.
- Duplicate replays do not rewrite stored labels.
- Replay responses return the labels stored with the original fact.
- Label keys are normalized to lowercase and typed into one value slot (
text/uuid/bool/number).
Campaign matching reads nested payload directly. It does not require Campaign
fields to be duplicated into labels.
Hand-off to events -> ratings pipeline
POST /events:- accepted event is enqueued
- event processing is also attempted inline
POST /events/batch:- accepted events are enqueued
- processing is deferred to worker/sweep flow
Verify checklist
- Same user and
event_keyreturns duplicate behavior (200orduplicateitem status), even if submitted content changed. - Different users may reuse the same
event_key. - A genuinely different occurrence for the same user uses a different key.
- Batch result preserves input order by
index. - Accepted events appear in processing queue for downstream rating.