API reference

Events

POST /v1/events submits an event for monitoring, in sync or async mode.
POST /v1/events

Submits an event for monitoring. In sync mode Sentinel scores it before responding and returns the decision. In async mode, the default, it stores the event and returns straight away.

Async mode only stores the event today. Nothing scores it later, so it raises no alert. Stored events still count toward the velocity features of later events from the same source. Use sync for anything you want decided.

Replaying an idempotency_key returns the original event and decision. See Idempotency.

Query parameters

mode
string
sync or async. Defaults to async.

Request body

type
string required
What happened, for example transfer.outbound, card.authorization, cash_out or login.
idempotency_key
string required
Your unique ID for this event.
occurred_at
string, date-time required
When it happened on your side, RFC 3339.
amount
object
For money events. value is a non-negative number and currency a three-letter ISO 4217 code. Both are required when amount is present.
rail
string
Which network the money moves on, such as mobile_money, card, bank_transfer, instant_payment or stablecoin. Rules can use it.
source
object
The sender: entity_id and account_id. Counters and alert grouping key on entity_id, so send it on every money event.
destination
object
The receiver. Send entity_id when you know who received the money: the money-in counters (fan-in, dormancy, pass-through) and the funds-flow chart only see payments that name a destination entity. Other keys, such as account_id, are stored as sent.
device
object
Device details, for example id. id is recorded as a uses_device link from the sender, so the alert panel can show other wallets on the same device. Rules can't count devices yet.

Sync example

Terminal
curl -X POST "$SENTINEL_URL/v1/events?mode=sync" \
  -H "Authorization: Bearer $SENTINEL_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "transfer.outbound",
    "idempotency_key": "txn_8f2a91",
    "occurred_at": "2026-10-15T11:02:41Z",
    "amount": {
      "value": 4950,
      "currency": "USD"
    },
    "rail": "mobile_money",
    "source": {
      "entity_id": "cus_10442",
      "account_id": "wal_8812"
    },
    "destination": {
      "account_id": "wal_6603"
    },
    "device": {
      "id": "dev_a3f19c"
    }
  }'

Returns 200 with a decision:

Response · 200
{
  "id": "5f0c2b7e-3a41-4c1d-9b6e-1f2a3b4c5d6e",
  "event_id": "9b21d4c8-7e5f-4a3b-8c2d-6e7f8a9b0c1d",
  "decision": "review",
  "risk_score": 60,
  "alert_id": "c7d4e1a2-5b6c-4d7e-8f9a-0b1c2d3e4f5a",
  "decided_in_ms": 38,
  "reasons": [
    {
      "rule": "structuring_below_threshold",
      "version": 1,
      "detail": "repeated transfers just below the reporting threshold within 24h"
    }
  ],
  "created_at": "2026-10-15T11:02:41.041Z"
}

Async example

Terminal
curl -X POST "$SENTINEL_URL/v1/events" \
  -H "Authorization: Bearer $SENTINEL_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "cash_out",
    "idempotency_key": "txn_8f2a92",
    "occurred_at": "2026-10-15T11:20:05Z",
    "amount": {
      "value": 9800,
      "currency": "USD"
    },
    "rail": "mobile_money",
    "source": {
      "entity_id": "cus_10442",
      "account_id": "wal_8812"
    },
    "destination": {
      "agent_id": "agt_0442"
    }
  }'

Returns 202 with the stored event's ID:

Response · 202
{
  "id": "e3b0c442-98fc-4c14-9afb-f4c8996fb924",
  "received_at": "2026-10-15T11:20:05.208Z"
}

Response fields

Sync (200) returns a decision. See Decisions.

Async (202):

FieldTypeMeaning
idstring, uuidSentinel's ID for the stored event
received_atstring, date-timeWhen Sentinel accepted it

Errors

Codeerror
400type and idempotency_key are required
400amount needs a non-negative value and a 3-letter currency
401missing or invalid API key

List events

GET /v1/events

Stored events, newest first by occurred_at, each with its decision if it was scored. Lists come a page at a time, newest first. Pass limit (1 to 200, default 50); if there are more, the response has a next_cursor. Send it back as cursor for the next page. Cursors are opaque and keep their place even while new rows arrive.

ParameterMeaning
typeOnly this event type
entityEvents where this entity is the source or the destination
decisionallow, review or block, or unscored for events stored in async mode
since, untilOnly events with occurred_at after since and up to until
limit, cursorPaging, as above
Terminal
curl "$SENTINEL_URL/v1/events?decision=review&limit=2"   -H "Authorization: Bearer $SENTINEL_KEY"
Response · 200
{
  "events": [
    {
      "id": "c4d5e6f7-0a1b-4c2d-8e9f-3a4b5c6d7e8f",
      "idempotency_key": "txn_88123",
      "type": "transfer.outbound",
      "occurred_at": "2026-10-16T09:12:44Z",
      "received_at": "2026-10-16T09:12:44.318Z",
      "amount": {
        "value": 4900,
        "currency": "GHS"
      },
      "rail": "mobile_money",
      "source_id": "wal_8812",
      "destination_id": "wal_2290",
      "decision": {
        "id": "a81e4f02-6c9d-4b7a-9e3f-2d1c0b9a8f7e",
        "decision": "review",
        "risk_score": 60
      }
    }
  ],
  "next_cursor": "MTc5MjE2MjQ3NTkxMzAwMDAwMHxjNGQ1ZTZmNw"
}

Get an event

GET /v1/events/{id}

One event with the same fields as the list, plus payload (the event exactly as it was sent), reasons from its decision (empty if it wasn't scored or nothing fired) and decided_in_ms. An unknown id is a 404 with event not found.

Sentinel is a Somu Labs product. © 2026 Somu Labs Ltd.