Event Counters
Some fraud signals are not transactions. Login attempts, failed verifications, session pings, channel switches: high-volume activity that says a lot about an entity right now and very little a week later. Event counters turn those streams into windowed per-entity values (a count in the last hour, a sum over 24 hours, a last-seen timestamp) that your rules read at decision time like any other field.
You register what to count once. Then you post events as they happen, and Loci maintains the rolling values and keeps them available to rule evaluation. No feature pipeline, no batch jobs, no schema migrations on your side.
Event counters are available to every organization. They do not require any optional module.
Counters extend windowed signals to event streams; they are not the platform's aggregation engine. Transaction history already gets full stateful aggregations natively inside FLM rules: counts, distinct counts, sums, averages, minimums and maximums, standard deviation, variance, most common value, and percentiles, computed live at evaluation over rolling per-entity windows. Use those for behavioral baselines and distributed-fraud patterns over transactions; use counters when the activity you need to count is not a transaction.
How It Works
- Register counters with a PUT to the counter configuration endpoint: what event types to count, over what windows, with what filters.
- Post events to the events endpoint as they happen, singly or in batches of up to 500.
- Loci maintains the values and writes them to an
entity_counterstable, provisioned automatically for your organization. Rolling windows advance continuously; values refresh on a debounced interval (default 60 seconds), and any event that crosses a threshold you define flushes immediately. - Rules read the values through the table-qualified reference. A rule that says "decline when
entity_counters.failed_logins_1his greater than 5" needs nothing beyond the counter's existence.
The values also appear as live signals in the entity realtime risk endpoint, alongside the entity's other risk context.
Registering Counters
Read Current Configuration
/v1/counters/{org_id}Read Counter Configuration Returns the current configuration and its version. Data is null when never configured.
curl "https://api.runloci.com/v1/counters/your_loci_org_id" \
-H "x-org-id: your_loci_org_id" \
-H "x-api-key: your_loci_api_key"
Create or Replace Configuration
/v1/counters/{org_id}Create or Replace Counter Configuration Submits the full counter configuration. Concurrency-safe via the version field.
curl -X PUT "https://api.runloci.com/v1/counters/your_loci_org_id" \
-H "x-org-id: your_loci_org_id" \
-H "x-api-key: your_loci_api_key" \
-H "Content-Type: application/json" \
-d '{
"enabled": true,
"flush_interval_seconds": 60,
"attributes": [
{ "name": "channel", "type": "text" },
{ "name": "outcome", "type": "text" }
],
"counters": [
{
"name": "failed_logins_1h",
"event_type": "login_attempt",
"aggregate": "count",
"window": { "amount": 1, "unit": "hours" },
"attribute_filters": { "outcome": "failed" },
"alert_thresholds": [5]
},
{
"name": "ivr_spend_24h",
"event_type": "ivr_transfer",
"aggregate": "sum",
"window": { "amount": 24, "unit": "hours" }
},
{
"name": "last_device_change",
"event_type": "device_registered",
"aggregate": "last_seen"
}
],
"version": 3
}'
Response: 200 OK with the saved configuration and its new version. Response: 409 Conflict when the version you sent is stale; the response carries the current version, so read, merge, and resubmit.
Configuration fields:
| Field | Description |
|---|---|
enabled |
Master switch for event counting |
table_name |
The table counters are written to. Defaults to entity_counters when omitted |
flush_interval_seconds |
How often refreshed values are written out. Default 60 |
attributes |
The attributes your events may carry and your filters may reference. Text type in v1 |
counters |
The counter definitions. Each becomes a column in the entity_counters table |
version |
Optimistic concurrency. Send the version you read; omit only when no configuration exists yet |
Counter definition fields:
| Field | Description |
|---|---|
name |
Column name your rules will reference. Lowercase letters, digits, underscores |
event_type |
The event type this counter consumes |
aggregate |
count, sum, or last_seen |
window |
Rolling window for count and sum, up to 90 days. Not used for last_seen |
attribute_filters |
Only events whose attributes equal these values are counted |
alert_thresholds |
Crossing any of these values triggers an immediate write instead of waiting for the flush interval |
active |
Set false to stop counting without losing the column. Re-activate any time |
The configuration is submitted whole: to add a counter, GET the current configuration, append the new counter, and PUT the result back with the version you read. The version check protects concurrent edits by returning 409 Conflict when another user saved a newer version first.
Posting Events
Single Event
/v1/events/{org_id}Ingest One Event Accepts a single event. Returns 202 on acceptance.
curl -X POST "https://api.runloci.com/v1/events/your_loci_org_id" \
-H "x-org-id: your_loci_org_id" \
-H "x-api-key: your_loci_api_key" \
-H "Content-Type: application/json" \
-d '{
"event_id": "evt_login_20260724_1401_88321",
"entity_id": "cust_88321",
"event_type": "login_attempt",
"occurred_at": "2026-07-24T14:01:09Z",
"attributes": { "channel": "mobile", "outcome": "failed" }
}'
Response: 202 Accepted
{
"status": "ok",
"data": { "accepted": 1, "duplicates": 0, "rejected": [] }
}
Batch Events
/v1/events/{org_id}/batchIngest Event Batch Accepts an array of up to 500 events in one request.
curl -X POST "https://api.runloci.com/v1/events/your_loci_org_id/batch" \
-H "x-org-id: your_loci_org_id" \
-H "x-api-key: your_loci_api_key" \
-H "Content-Type: application/json" \
-d '{
"events": [
{
"event_id": "evt_ivr_0001",
"entity_id": "cust_88321",
"event_type": "ivr_transfer",
"occurred_at": "2026-07-24T13:58:40Z",
"value": 15000
},
{
"event_id": "evt_ivr_0002",
"entity_id": "cust_88321",
"event_type": "ivr_transfer",
"occurred_at": "2026-07-24T14:00:12Z",
"value": 40000
}
]
}'
The batch body is an object with an events array, not a bare array. Rejected events are reported per item with an index and reason; the rest of the batch is still accepted.
Event fields:
| Field | Required | Description |
|---|---|---|
event_id |
Yes | Your idempotency key. Re-sending the same id is counted once and reported as a duplicate |
entity_id |
Yes | The entity the event belongs to |
event_type |
Yes | Matches counters by their event_type |
occurred_at |
Yes | RFC 3339 timestamp. Events more than 5 minutes in the future are rejected |
value |
For sum |
The amount to add. Ignored by count and last_seen |
attributes |
No | Declared attributes only. Used by attribute_filters |
Send events as they happen rather than batching on a long timer. Counters are most valuable when the value at decision time reflects the last few minutes, not the last hour's batch.
Using Counter Values in Rules
Every counter is a column in your counter table (entity_counters unless you configured a different table_name), keyed by entity_id. Rules reference them with the table-qualified form, entity_counters.<counter_name>:
entity_counters.failed_logins_1h > 5
entity_counters.ivr_spend_24h >= 500000
The qualifier matters: a bare field name in a rule resolves against the transaction itself, so failed_logins_1h alone will not find the counter. Always include the entity_counters. prefix.
A transaction from an entity whose recent activity crossed your thresholds is evaluated against those live values with no extra integration work. The counter values are also returned as live signals by the entity realtime risk endpoint, so investigation views and risk dashboards see the same numbers your rules do.
Integration Guide: From Event Stream To Rule
- Choose the short-lived signal your rule needs, such as failed login attempts, card verification failures, support-channel switches, or device changes.
- Register a counter for that event type. The counter name becomes the field your rule references.
- Send events as they happen to
POST /v1/events/{org_id}orPOST /v1/events/{org_id}/batch. - Author the rule against the table-qualified counter field:
entity_counters.failed_logins_1h > 5
For example, a high-risk transaction rule can combine transaction facts with event-counter context:
amount > 500000 AND entity_counters.failed_logins_1h > 5
That pattern keeps the transaction decision path simple. Your application sends lightweight event facts when they occur, and the rule reads the latest windowed value at decision time.
Counter names share the entity_counters table across your organization. Choose names that read clearly in a rule (failed_logins_1h, not flc1), because analysts and auditors will encounter them inside rule logic and investigation views.
Idempotency and Windows
- Duplicate suppression. Each
event_idis remembered for the length of your longest counter window plus a safety margin. Within that lifetime, a re-sent event counts once and is reported as a duplicate. - Rolling windows. Windows advance continuously. A 1-hour count at 14:05 covers 13:05 to 14:05, not a calendar hour.
- Deactivation. Setting a counter's
activeflag to false stops counting and updates while preserving the column and its history. Removing a counter from the configuration retires it; existing stored values may remain for audit and continuity.
Machine-Readable Spec
/v1/counter-stream/openapi.jsonOpenAPI Specification The full OpenAPI 3.0 document for the counter endpoints.
curl "https://api.runloci.com/v1/counter-stream/openapi.json"
Use it to generate clients or validate integrations against the active API contract.
Support
- Email: support@runloci.com
- Related: Webhook Events, Transaction Monitoring API Reference, Rules API