Events and identity
Ingestion modes, idempotency, the client-event trust contract and customer identity.
POST /v1/events accepts one event or up to 100 (events: [...], 32 KB each). The client
supplies id (your business id, unique per environment forever), name (dotted snake_case),
customer_id or anonymous_id, occurred_at (at most 24 h in the future, see
event time; older than 30 days is flagged late) and properties.
customer_id is your external id for the customer (your own user id), never Orishare's
internal cust_... id; an event for an external id nobody holds creates that customer. The same
value may be sent as external_customer_id or as customer: { "external_id": ... }. To address
a customer by internal id, send customer: { "id": "cust_..." } instead (an unknown one is
rejected with resource_not_found). A value sent as the external id that is an existing
customer's internal id, while no customer has it as its external id, is refused with
validation_failed on that field, so an internal id sent by mistake never creates a second
customer. Any other value is an ordinary external id, whatever its spelling.
- async (default):
202with the stored event'sevt_...id; pollGET /v1/events/{id}with that id forstatus. Your business id is the event'sclient_event_id; the poll route does not take it. - sync:
200witheffectsandtrace; falls back to202 fallback: trueif evaluation cannot finish in time. Sync is for secret keys only. - batch:
207with one result per item, matched byindex(accepted,evaluated,duplicate,rejected); a rejected item has anerrorand noid. The agent guide lists every field.
A stored id sent again answers duplicate with the original evt_... id and admits nothing:
execution_id and effects are those of the original if it was already evaluated, otherwise
null and []. The same id with a different payload is 409 event_id_conflict. A secret-key
POST /v1/events does not read Idempotency-Key; the event id is its dedupe. Customer events
(POST /v1/customer/events) require Idempotency-Key and have no duplicate status; see
Customer integration.
Customer event ids are per customer. The id a customer session sends (POST /v1/customer/events,
or a publishable POST /v1/events with session proof) names an event of that customer only:
two customers may send the same id and both events count, and no customer can use up an id
that another customer or your backend will send. Secret-key ids keep one namespace for the whole
environment and are never matched against a customer's ids. After a
merge, a resend of an id that any
merged customer sent before answers as that earlier event, the surviving customer's own event
first; earlier events are never renamed. Responses, receipt URLs, GET /v1/events and the customer export show the
id as it was sent. Customer events stored before ids were per customer keep their stored id and
still answer a resend from their own customer. Event ids cannot start with cs: on any route
(that prefix is reserved): a new event with such an id is refused with validation_failed.
Event time and clock skew
occurred_at is when the activity happened by your clock; Orishare stamps received_at when the
event arrives. Clocks drift, so a server running a little fast sends times slightly ahead of
ours. The rules:
- Up to 24 hours ahead of
received_atis accepted. The event is evaluated now, atmin(occurred_at, received_at): limit windows, daily caps, campaign budgets, counters, program periods (streak days, seasons, levels) and enrollments all use that clamped instant. A match stamped 00:00:21 that arrives at 23:59:50 counts toward the day that is ending, never toward a day that has not started.mode: "sync"returns its effects as for any present-time event. - The stored
occurred_atstays exactly what you sent;GET /v1/events/{id}shows it. - More than 24 hours ahead is refused with
400 occurred_at_in_future(in a batch, that item isrejectedwith the same code andparamevents.<i>.occurred_at). Fix the sender's clock or the value; nothing was stored. - A past
occurred_atis honoured for server events (late events count toward the period they belong to). Browser and app events use receipt time unless the event definition'stime_policy.client.offline_secondsallows an older activity time; a device clock ahead of ours always gets receipt time, never a refusal.
An accepted event is never parked because of its time. If evaluation fails for another reason
it is retried, and after five failed attempts its status becomes poison with the reason
recorded. POST /v1/events/{id}/retry runs it again with its original identity against the
current rules.
Event definitions and schemas
Every event name gets a definition (GET /v1/event_definitions, GET /v1/event_definitions/{id})
with the observed_schema Orishare recorded from accepted events, a few samples, and what you
add with PATCH /v1/event_definitions/{id}: a description, client_allowed, a
confirmed_schema and schema_enforcement.
A definition is created the first time a secret key sends a well-formed name it has never seen:
such an event is accepted, not rejected, and the new definition is server-only
(client_allowed: false). A misspelled name therefore becomes a new event; check names on your
side or against GET /v1/event_definitions.
The confirmed schema says what the event should look like. It is a flat map of property to a
type (string, number, boolean, null, array, object, timestamp), a list of types, or
{ "type": ..., "required": true }. Listed properties are type checked when present, extra
properties are allowed. It feeds the rule builder's field list and AI drafting. By itself it
rejects nothing: schema_enforcement decides that, and a new definition starts off.
schema_enforcement | What ingestion does |
|---|---|
off | Nothing. The schema is documentation only. |
warn | Events that break the schema are accepted and counted: schema_violation_count and schema_last_violation (property, message, event id, time and a bounded sample) show what enforce would reject. The count starts again when the schema or the mode changes. |
enforce | Events that break the schema are rejected before anything is stored: validation_failed with param events.<i>.properties.<name>. A corrected retry with the same id is accepted. |
warn and enforce need a confirmed schema (validation_failed otherwise), and clearing the
schema (confirmed_schema: null) sets the mode back to off. Run warn for a while before
enforce. In a configuration document schema_enforcement is
optional: when it is absent a new definition is created off and an existing one keeps its
mode; enforce or warn without a schema is refused.
Trust contract
Publishable keys may send only events whose definition has client_allowed: true.
In an activated customer environment they require a live customer session in
X-Orishare-Customer-Token; identity comes only from that proof. Use /customer/events
(or the session-bound /events adapter), a stable event ID and Idempotency-Key, then
poll the safe receipt. No inline evaluation is promised. Valuable events such as
order.paid remain server-only and return 403 event_requires_secret_key from a browser.
See Customer integration for the complete paired-credential flow.
Identity
A trusted backend maps its external identity to a customer: POST /v1/customers upserts by
external_id (201 created, 200 updated) and returns the internal id. An upsert advances
version by one only when it carries non-empty attributes (even unchanged ones) or a timezone;
with attributes omitted or {} and no timezone the version stays as it was, so always use the
version the response returns. A verified provider connection
or trusted backend exchange issues an opaque session; an anonymous grant remains anonymous.
Customer routes reject caller-supplied identity and trust selectors. Linking an anonymous
source to an authenticated target requires two proofs through the merge API. Ownership
links preserve physical event, ledger and program history; source sessions are revoked.
The legacy alias-only operation does not perform this full ownership merge.
Before activation, the retained legacy browser path uses anonymous_id or a server-minted
HMAC customer_token. After activation those credentials fail with
410 customer_credential_retired; migrate to sessions rather than retrying old tokens.
Secret-key event ingestion and v1 rules retain their behavior.
Deleting any retained member of a linked customer group tombstones and redacts the whole group, revokes its sessions and enqueues analytics redaction per physical member. Accounting and permanent ownership evidence remain for liability and replay safety.