API reference

Rules

List rule versions, create a draft, backtest it, and activate it with a second key.

Rules are versioned. Each version is its own record with its own id. A new version starts as a draft, a different key activates it, and the version it replaces becomes retired. See Rules for how rules score events.

StatusMeaning
draftCreated, not yet affecting decisions
activeScoring every sync event. At most one version per rule key is active.
retiredReplaced by a newer active version. Kept for history.
An organisation with no active rules is scored by the built-in pack. Once you activate any rule of your own, only your active rules run. To keep the built-in rules and edit them, run sentinel-admin rules seed -org <org> before creating your own. It installs them as active rules, and only works on an organisation with no rule versions yet.

List rules

GET /v1/rules

Returns every version of every rule, sorted by key, newest version first.

Query parameters

status
string
Only return versions with this status: draft, active or retired.
key
string
Only return versions of this rule.

Example

Terminal
curl "$SENTINEL_URL/v1/rules?key=structuring_below_threshold" \
  -H "Authorization: Bearer $SENTINEL_KEY"
Response · 200
{
  "rules": [
    {
      "id": "6a1f0c3e-2b4d-4e5f-8a9b-0c1d2e3f4a5b",
      "key": "structuring_below_threshold",
      "version": 2,
      "expr": "amount >= 4500.0 && amount < 5000.0 && features[\"near_threshold_count_24h\"] >= 4",
      "weight": 60,
      "detail": "repeated transfers just below the reporting threshold within 24h",
      "status": "draft",
      "created_by": "3f2e1d0c-9b8a-4765-8432-10fedcba9876",
      "created_at": "2026-10-15T14:02:11.480Z"
    },
    {
      "id": "1b2c3d4e-5f6a-4b7c-8d9e-0f1a2b3c4d5e",
      "key": "structuring_below_threshold",
      "version": 1,
      "expr": "amount >= 4500.0 && amount < 5000.0 && features[\"near_threshold_count_24h\"] >= 3",
      "weight": 60,
      "detail": "repeated transfers just below the reporting threshold within 24h",
      "status": "active",
      "created_by": "sentinel-admin",
      "approved_by": "sentinel-admin (seed)",
      "created_at": "2026-10-01T08:00:00.000Z",
      "activated_at": "2026-10-01T08:00:00.000Z"
    }
  ]
}

Create a draft

POST /v1/rules

Creates the next version of a rule as a draft. If no rule with that key exists, this is version 1. The expression is compiled straight away, so a broken expression is rejected here and never reaches a live decision. Returns 201 with the new version.

Request body

key
string required
The rule's name. Lowercase letters, digits and underscores, starting with a letter, 2 to 64 characters. Reasons on decisions show this.
expr
string required
A CEL expression that returns true or false. See variables.
weight
integer required
Points added to the risk score when the rule fires. 1 to 100.
detail
string required
The plain explanation stored on each decision the rule fires on.
subject
string
source (the default) or destination: which party the rule is about. Its alerts go to that party. Use destination for rules about money arriving, such as fan-in.

Example

Terminal
curl -X POST "$SENTINEL_URL/v1/rules" \
  -H "Authorization: Bearer $SENTINEL_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "key": "large_card_payment",
    "expr": "rail == \"card\" && amount >= 10000.0",
    "weight": 40,
    "detail": "card payment of 10,000 or more"
  }'
Response · 201
{
  "id": "9e8d7c6b-5a4f-4e3d-2c1b-0a9f8e7d6c5b",
  "key": "large_card_payment",
  "version": 1,
  "expr": "rail == \"card\" && amount >= 10000.0",
  "weight": 40,
  "detail": "card payment of 10,000 or more",
  "subject": "source",
  "status": "draft",
  "created_by": "3f2e1d0c-9b8a-4765-8432-10fedcba9876",
  "created_at": "2026-10-15T14:20:45.913Z"
}

Expression variables

VariableTypeValue
typestringThe event type, for example transfer.outbound
amountdoubleamount.value, or 0 if the event has none
currencystringamount.currency, or empty
railstringAs sent, or empty
source_idstringsource.entity_id, or empty
destination_idstringdestination.entity_id, or empty
featuresmap of string to intCounters for the source
dest_featuresmap of string to intThe same counters for the destination

Each counter covers the 24 hours up to and including this event, by occurred_at, and only events that had arrived by the time this one did. Events stored in async mode count too.

CounterCounts
txn_count_24hEvents the entity sent
near_threshold_count_24hOf those, amounts from 4,500 up to but not including 5,000. Set the band with SENTINEL_NEAR_THRESHOLD_LOW and SENTINEL_NEAR_THRESHOLD_HIGH
in_count_24h, in_value_24hPayments the entity received, and their total in whole units
in_senders_24h, in_senders_1hDifferent senders that paid it, in 24 hours and in the last hour
out_count_24h, out_value_24hPayments it made, and their total in whole units
out_receivers_24hDifferent receivers it paid
dormant_daysWhole days since its previous payment in or out, 0 if none
seen_before1 if it had any earlier payment, else 0

txn_count_24h and near_threshold_count_24h are only computed for the source; in dest_features they read 0. Any counter that doesn't apply reads 0. Use amount >= 4500.0, not amount >= 4500: CEL won't compare a double with an int.

A fan-in rule written against the destination:

{
  "key": "fan_in_unrelated_senders",
  "expr": "dest_features[\"in_senders_1h\"] >= 10",
  "weight": 50,
  "detail": "10 or more different senders paid this account within an hour",
  "subject": "destination"
}

Activate a draft

POST /v1/rules/{id}/activate

Approves a draft version. It becomes active, and any other active version of the same rule becomes retired. No body is needed.

This is four-eyes: the key that created the draft can't activate it. It takes effect at once on the instance that handled the request, and within 30 seconds on other instances.

Terminal
curl -X POST "$SENTINEL_URL/v1/rules/9e8d7c6b-5a4f-4e3d-2c1b-0a9f8e7d6c5b/activate" \
  -H "Authorization: Bearer $SENTINEL_REVIEWER_KEY"

The response is the rule with status set to active, plus approved_by and activated_at.

Backtest a version

POST /v1/rules/{id}/backtest

Replays stored events through one rule version and reports what it would have flagged. Nothing changes: no decisions, no alerts. Each event sees the same 24-hour counters it saw live. Any version can be backtested, whatever its status.

If the version isn't active and another version of the same rule is, the result also compares the two, so you can see what a change would add and remove.

Request body

All fields are optional. Send no body for the last 7 days.

since
string, date-time
Start of the window, by event occurred_at. Defaults to 7 days before until.
until
string, date-time
End of the window. Defaults to now. The window can be at most 31 days.
max_events
integer
The most events to replay, oldest first. 1 to 100,000. Defaults to 50,000.

Example

Terminal
curl -X POST "$SENTINEL_URL/v1/rules/6a1f0c3e-2b4d-4e5f-8a9b-0c1d2e3f4a5b/backtest" \
  -H "Authorization: Bearer $SENTINEL_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "since": "2026-10-01T00:00:00Z",
    "until": "2026-10-15T00:00:00Z"
  }'
Response · 200
{
  "rule_id": "6a1f0c3e-2b4d-4e5f-8a9b-0c1d2e3f4a5b",
  "key": "structuring_below_threshold",
  "version": 2,
  "since": "2026-10-01T00:00:00Z",
  "until": "2026-10-15T00:00:00Z",
  "events_evaluated": 18342,
  "truncated": false,
  "would_flag": 41,
  "eval_errors": 0,
  "baseline_version": 1,
  "baseline_would_flag": 67,
  "newly_flagged": 0,
  "no_longer_flagged": 26,
  "sample_event_ids": []
}

Result fields

FieldMeaning
events_evaluatedEvents replayed
truncatedtrue when max_events cut the window short
would_flagEvents this version would have fired on
eval_errorsEvents where the expression failed at run time
baseline_versionThe active version compared against. Absent when there isn't one.
baseline_would_flagEvents the active version fires on
newly_flaggedFired by this version but not the active one
no_longer_flaggedFired by the active version but not this one
sample_event_idsUp to 20 event IDs: newly flagged ones when there's a baseline, otherwise flagged ones

Rule fields

FieldTypeMeaning
idstring, uuidThis version's ID
keystringThe rule's name, shared by all its versions
versionintegerStarts at 1 and goes up by one per draft
exprstringThe CEL expression
weightintegerPoints added when it fires
detailstringThe explanation stored on decisions
subjectstringsource or destination: whose alert a match goes on
statusstringdraft, active or retired
created_bystringThe key ID that created it, or sentinel-admin for seeded rules
approved_bystringThe key ID that activated it
created_atstring, date-timeWhen the draft was created
activated_atstring, date-timeWhen it was activated

Errors

EndpointCodeerror
All401missing or invalid API key
POST /v1/rules400key must be lowercase letters, digits and underscores, starting with a letter
POST /v1/rules400weight must be between 1 and 100
POST /v1/rules400detail is required
POST /v1/rules400The compile error, for example rule large_card_payment: expression must return bool
.../activate403the author of a rule cannot approve it
.../activate409only a draft can be activated
.../backtest400since must be before until
.../backtest400window is capped at 31 days
.../backtest400max_events must be between 1 and 100000
.../activate, .../backtest404rule not found
Sentinel is a Somu Labs product. © 2026 Somu Labs Ltd.