Core concepts
You only ever push one thing: events. Everything else — episodes, durations, habits, findings — is derived from them. This page explains the vocabulary, so the endpoint reference reads as a set of obvious choices rather than a list of fields.
The chain
What happens to an event
event → mapping → entity resolution → definition selection
→ business key → derivation (episode | measurement)
→ rule evaluation → finding
Every step is idempotent and replayable: run it again on the same input and you get the same state, with nothing duplicated. That property is what makes retries safe, backfills reproducible, and corrections cheap.
Event
A fact that happened at a point in time, in one of your systems. It carries an identifier that is stable on your side (source_event_id), a type, an occurred_at with a UTC offset, and, optionally, a payload you are free to shape.
sinio.ai works on occurred_at, never on the time it received the event. Out-of-order delivery is normal and expected: an event arriving with a timestamp earlier than episodes already computed triggers a recomputation of the affected key, not an error.
Source
One of your tools — an access-control system, a maintenance software — created in the project with its own API keys. The API, the SDKs and webhook URLs are only channels into it: a project key always writes into its source.
Identifiers and event types are scoped by source, on purpose. Two of your systems can use the same identifier or the same raw code for different things (badge 2 is not asset 2, closed in a ticketing tool is not closed in an access control system) and sinio.ai will not conflate them.
When two of your tools do mean the same thing — technician 2 in a maintenance tool, E123 in your HR system — keep sending each its own identifier and link them once in the console, under Settings › Identifiers: attach a pair by hand, or set a matching rule when both tools write the same external_id for one kind of entity, for the identifiers already there and those to come. Episodes, habits and reports then count them as one; detaching splits them again. Linking is always a decision made in the console: sinio.ai never infers it, and the API has no call for it.
Entity and the four roles
An event can point at up to four structural roles, in the fixed roles envelope — any other key there is refused:
- Name
device- Type
- string
- Description
What produced or recorded the fact — a reader, a sensor, a terminal.
- Name
subject- Type
- string
- Description
Who or what the fact is about — a person, a badge, a vehicle, an asset.
- Name
location- Type
- string
- Description
Where it happened, as a location
external_id.
- Name
actor- Type
- string
- Description
Who performed it, when that differs from the subject — a technician closing someone else's work order.
Everything else stays in payload, free-form. A ticket number, a firmware version, a temperature: sinio.ai stores it whole and reads from it only what the type's published interpretation tells it to.
What a role's identifier is — a person, a badge, a piece of equipment, the device speaking about itself — is not sent with each event. It is said once, when the event type is classified, for that type and source: a badge reader's subject is a badge, a picking scan's subject a person. It cannot change once events of that type are accepted. A device's model (its subtype) belongs to the device itself: declare it with POST /v1/entities or name it in the console.
Entities and business references
Put in roles only the lasting things you would declare once: devices, people, equipment, badges. An order, a return, a ticket, a parcel or a batch number is a business reference: it lives for one piece of work, then is never seen again. Send it in payload:
{
"type": "picking_started",
"roles": { "device": "scanner-3", "actor": "picker-12" },
"payload": { "order": "CMD-4410" }
}
A process then groups its steps by that reference (keyed_by: business_reference, key_reference: order), and each event type says where its payload carries it (interpretation.references: { "order": "$.order" }) — every step of order CMD-4410 joins the same episode, whoever scanned it, even when two tools write it in different fields. Sent as a subject instead, each order would become an undeclared entity to review, one per order.
The reference is never a condition of acceptance. An event naming a device
that has never been declared is accepted, and the device is created as
undeclared. Declaring it later with POST /v1/entities flips it
to declared without detaching the events already attached to it.
Event type
The semantics of type. A type belongs to a domain (attendance, intervention, telemetry, incident, satisfaction, traceability), has a nature (step, measure, annotation), and may carry an interpretation saying what each of its roles stands for and where its payload carries each business reference. The values it carries are read by its source's readings, set in the console (Measurements).
A type you have not declared is not an error. The event is stored with status pending_classification and counted separately in the response: the payload is perfectly valid, only its meaning is not known yet. Event types are configured in the sinio.ai console: once a type is given a meaning there, the waiting events are derived with it.
Episode
The unit of meaning. An episode is a sequence of steps sharing a business key — an attendance from badge in to badge out, an intervention from dispatch to sign-off, a round from first to last checkpoint.
An episode_definition — a process, in the console, where it is configured — says which event types are its steps, how the key is composed (by subject, by subject and location, by location, by device, or by a business reference in the payload), how it closes, and how long it may stay open. sinio.ai selects the most specific active definition matching the event.
An episode always ends up in one of these states:
- Name
open- Type
- status
- Description
Started, not closed yet.
- Name
closed- Type
- status
- Description
Complete, with a computed duration.
- Name
expired- Type
- status
- Description
Stayed open past its
max_openwindow. Raises amissing_stepfinding.
- Name
orphan_close- Type
- status
- Description
A closing step arrived with no episode open for that key. No phantom episode is ever created retroactively.
- Name
orphan_open- Type
- status
- Description
An opening step that never found its pair.
- Name
out_of_order- Type
- status
- Description
The closing step predates the opening one. Duration stays null; the real timestamps survive on the steps.
- Name
ambiguous- Type
- status
- Description
Several definitions matched with equal specificity. Resolved deterministically, and reported as a
configurationfinding.
A duplicate step never creates a second episode. Badge twice at 07:58 and 08:01 and you get one attendance plus a duplicate_step flag — not two attendances, one of which would expire unclosed and poison every duration statistic downstream.
Presence days
For an attendance process, each passage (badge in → badge out) stays an episode, and sinio.ai also gathers a badge holder's passages into a presence day. A lunch break then no longer reads as a second arrival: habits, durations and daily volumes are judged on the day. Nothing changes in what you send — the day is derived, and set up in the console, per population (the subtype of the badge holder):
- per passage or per day, and the hour a day starts — a night from 20:55 to 05:05 is one day, the evening's;
- a maximum interruption: past it, the next entry starts a new day.
The gaps between the passages of a day are absences. An absence is measured when both its exit and its return were observed, and of unknown length when one of them is missing — never zero, and never counted in a sum.
An absence can be qualified as a lunch_break or a break, in this order of precedence:
- The reason your event declares. Say where the payload carries it, as a business reference named
absence_reasonon the event type (interpretation.references: { "absence_reason": "$.reason" }). Send your own raw values —LUNCH,R12,PAUSE_REPAS— and map them once in the console. - The time of the exit, against time slots set in the console.
If your system sends its own pause start and pause end events between entry and exit, send them as ordinary events with their own types:
{ "type": "pause_start", "occurred_at": "2026-09-25T12:02:00+02:00",
"roles": { "subject": "badge-14682", "device": "kiosk-2" } }
The process declares, in the console, which type starts and which ends a pause. They are never steps: they open and close no passage. sinio.ai pairs them within the day, merges a declared pause with the badging gap it overlaps, and deducts it from the day's net presence. A pause boundary outside any presence is reported as pause_outside_presence.
Finding
What sinio.ai has to tell you. A finding carries a category, a kind, a severity decided by the engine, and an explanation shaped as observed versus expected.
- Name
operational- Type
- category
- Description
Something about the field:
missing_step,sla_breach,duration_exceeded,volume_out_of_range,device_silent,late_close, the measurement findings (measure_limit_crossed,measure_off_habit,measure_fleet_outlier,projected_depletion,expected_measure_missing,state_too_long— see Measurements), the pause surveillances (pause_duration_exceeded,pause_count_exceeded,pause_total_exceeded), behavioural anomalies against a subject's own habits.
- Name
configuration- Type
- category
- Description
Something about the setup:
undeclared_entity,undeclared_subkind,overlapping_assignment,ambiguous_definition.
- Name
data_quality- Type
- category
- Description
Something about the data:
unmapped_event_type,source_conflict,pause_outside_presence,measure_value_impossible.
- Name
security- Type
- category
- Description
A real situation that should not be allowed:
revoked_credential_used, a revoked badge still opening a door.
Findings are deduplicated by key, so replaying a derivation never duplicates one, and they survive recomputation — an alert that was once true stays auditable even after the episode it described has been recalculated.
A type sinio.ai does not know yet
There are two ways an event type can be "not handled", and they are both normal states rather than failures:
| What happened | What sinio.ai does | |
|---|---|---|
| Unknown type | The code is not known to the project at all | Accepted and stored as pending_classification, derived once the type is given a meaning |
unmapped_event_type | The type exists, but no episode definition uses it | Accepted and stored, not derived, reported as a data_quality finding |
Neither drops your traffic. You legitimately send types sinio.ai does not derive anything from yet; treating that as a rejection would lose valid events and make a healthy integration look broken.
Measurements
A device's events often carry values: a battery level, a signal strength, a temperature, a door's state. sinio.ai reads them as measurements, one per entity and measure: a device that sends two batteries sends two measures, each with its own name.
The measure dictionary
Every project has a dictionary of measures: the ones it took from the sinio.ai catalog — each with a function, a nature and a unit, as the catalog defines it — and the ones it created. The catalog is a template: a measure enters the dictionary when a key is attached to it. The function is what sinio.ai looks at: the Fleet, the detection and the assistant never guess a meaning from a code.
| Code | Function | Unit |
|---|---|---|
battery | energy | percent |
battery_voltage | energy | V |
rssi | signal | dBm |
snr | signal | dB |
signal_quality | signal | levels POOR … EXCELLENT |
temperature | environment | celsius |
humidity | environment | percent |
co2 | environment | ppm |
pressure | environment | bar |
illuminance | environment | lux |
power | consumption | W |
energy_consumed | consumption | kWh |
passages | counter | count |
door_state | state | CLOSED, OPEN, FORCED |
alarm_state | state | NORMAL, ALARM |
A value is always stored in its measure's unit. A source writing a temperature in fahrenheit, or a voltage in mV, declares that unit and sinio.ai converts it on the way in, so two sources never mix incomparable values in one series.
What a source reads
Every key a source writes in its payloads is inventoried as events arrive, read by something or not. A reading attaches one of them to a measure of the project's dictionary, in the console (To attach, or Settings › Measures): the measure, the entity it is about (device, subject or actor), its path in the payload, the unit it is written in, and the words it translates ("FULL" → 100, "1" → "OPEN"). A reading holds for every event type of the source, or for one type only — a type's own reading wins over the general one. A key nothing reads waits in To attach until it is attached or ignored; a reading whose column gives no readable value is flagged there too.
Slots are stored separately, never averaged. Two battery cells at 10% and 90% are two series, because their mean — 50%, comfortably healthy-looking — hides that half the power supply is gone.
Profiles and norms
An entity's subkind (a probe model, a kind of badge) carries a profile in each project: the measures one of its entities is expected to send, and whether a missing one is worth a finding. The Fleet groups devices by subkind with these columns.
Norms are rules: a limit (threshold) or a rhythm (expected_heartbeat), set in the console on the project, a subkind, a place or a single device. The most precise scope wins — device, then its places, then its subkind, then the project. Without any norm, each device is still compared to its own habit and to the rest of its fleet.
Findings on measurements
| Kind | When |
|---|---|
measure_limit_crossed | A value stays past the limit in force for its duration |
measure_off_habit | A value leaves the device's own usual range |
measure_fleet_outlier | A device reads far from the others of its subkind |
projected_depletion | A battery will be flat within the horizon at its current slope |
expected_measure_missing | A device keeps talking but stops sending a measure its profile requires |
state_too_long | A state (a door open, an alarm) lasts longer than tolerated |
measure_value_impossible | A source writes a value that cannot be read, or outside the measure's possible range |
Where to go next
- Batching & idempotency — replay safety, revisions, partial acceptance.
- Events — the endpoint itself, field by field.