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

  1. Register counters with a PUT to the counter configuration endpoint: what event types to count, over what windows, with what filters.
  2. Post events to the events endpoint as they happen, singly or in batches of up to 500.
  3. Loci maintains the values and writes them to an entity_counters table, 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.
  4. Rules read the values through the table-qualified reference. A rule that says "decline when entity_counters.failed_logins_1h is 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

GET/v1/counters/{org_id}

Read Counter Configuration Returns the current configuration and its version. Data is null when never configured.

bash
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

PUT/v1/counters/{org_id}

Create or Replace Counter Configuration Submits the full counter configuration. Concurrency-safe via the version field.

bash
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

POST/v1/events/{org_id}

Ingest One Event Accepts a single event. Returns 202 on acceptance.

bash
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

json
{
  "status": "ok",
  "data": { "accepted": 1, "duplicates": 0, "rejected": [] }
}

Batch Events

POST/v1/events/{org_id}/batch

Ingest Event Batch Accepts an array of up to 500 events in one request.

bash
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>:

text
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

  1. Choose the short-lived signal your rule needs, such as failed login attempts, card verification failures, support-channel switches, or device changes.
  2. Register a counter for that event type. The counter name becomes the field your rule references.
  3. Send events as they happen to POST /v1/events/{org_id} or POST /v1/events/{org_id}/batch.
  4. Author the rule against the table-qualified counter field:
text
entity_counters.failed_logins_1h > 5

For example, a high-risk transaction rule can combine transaction facts with event-counter context:

text
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_id is 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 active flag 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

GET/v1/counter-stream/openapi.json

OpenAPI Specification The full OpenAPI 3.0 document for the counter endpoints.

bash
curl "https://api.runloci.com/v1/counter-stream/openapi.json"

Use it to generate clients or validate integrations against the active API contract.


Support