Events and decisions
Send an event, get a decision. In sync mode the answer comes back on the same request, fast enough to wait for before you move money.
Events
An event is something that happened to a customer: a transfer, a card authorisation, a cash-in or cash-out at an agent, a payout, a login, a change of phone number.
Events are immutable. Each one carries an idempotency_key that's unique within your organisation, so a retried request returns the original event and decision instead of creating a second one.
| Field | Required | Meaning |
|---|---|---|
type | Yes | What happened, for example transfer.outbound, card.authorization, cash_out, login |
idempotency_key | Yes | Your unique ID for this event. Replays return the original result. |
occurred_at | Yes | When it happened on your side, RFC 3339 |
amount | For money events | value as a number and currency as a three-letter ISO 4217 code |
rail | No | Which network the money moves on, such as mobile_money, card or bank_transfer. Rules can use it. |
source | No | entity_id and account_id of the sender. Features and alert grouping use entity_id. |
destination | No | Who and which account received it |
device | No | Device details. Stored with the event, not used by rules yet. |
{
"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"
}
}
The request path
Here is what happens on a synchronous request, POST /v1/events?mode=sync:
| Step | Where | What happens |
|---|---|---|
| 1 | Your payment service → API | You send the event |
| 2 | API | Checks the key and finds your organisation |
| 3 | PostgreSQL | Inserts the event; a repeated idempotency key returns the stored one |
| 4 | Redis or PostgreSQL | Reads 24-hour features for the source |
| 5 | Rules engine | Evaluates every active rule |
| 6 | Rules engine | Adds up the score and picks the decision and reasons |
| 7 | PostgreSQL | Writes the decision, with its reasons and feature values, and puts a review or block on an alert |
| 8 | API → your payment service | Returns 200 with the decision |
Steps 3 to 7 hit the database a fixed number of times, whatever the size of the customer's history. The target is a p99 under 300 ms. With SENTINEL_REDIS_ADDR set, the counters come from Redis, and Sentinel falls back to PostgreSQL if Redis is slow or down.
The decision
Every rule that matches adds its weight to a risk score from 0 to 100 and records a reason. The score becomes one of three decisions:
| Score | Decision | What your system should do |
|---|---|---|
| Under 50 | allow | Let the payment through |
| 50 to 89 | review | Let it through or hold it (you choose). The decision goes on an alert for an analyst. |
| 90 and above | block | Stop the payment. The decision goes on an alert. |
The score is capped at 100. The thresholds are fixed at 50 and 90 in the current release.
Take a transfer of 4,950 where both built-in rules fire:
| Rule | Why it fired | Points |
|---|---|---|
structuring_below_threshold | Third transfer just under 5,000 in 24 hours | +60 |
high_velocity_24h | 20th transaction from this source in 24 hours | +30 |
The total is 90, so the answer is block. Reasons are sorted by rule key:
{
"id": "5f0c2b7e-3a41-4c1d-9b6e-1f2a3b4c5d6e",
"event_id": "9b21d4c8-7e5f-4a3b-8c2d-6e7f8a9b0c1d",
"decision": "block",
"risk_score": 90,
"alert_id": "c7d4e1a2-5b6c-4d7e-8f9a-0b1c2d3e4f5a",
"reasons": [
{
"rule": "high_velocity_24h",
"version": 1,
"detail": "20 or more transactions from the same source within 24h"
},
{
"rule": "structuring_below_threshold",
"version": 1,
"detail": "repeated transfers just below the reporting threshold within 24h"
}
],
"created_at": "2026-10-15T11:02:41.041Z"
}
alert_id is set only when the decision is review or block. While a source already has an open alert, its new review and block decisions join that alert, so alert_id can point to an alert raised earlier. See Alerts.
Your payment service blocks the transfer and shows the customer a generic error. Sentinel never moves money itself.
Sync or async
| Mode | Behaviour | Use when |
|---|---|---|
sync | Scores before responding. Returns 200 with the decision. | You need the answer before moving money |
async (default) | Stores the event and returns 202 with its ID. Nothing scores it yet. | Loading history, so velocity features count it |
sync for anything you want monitored.If Sentinel can't be reached
Decide your fallback before go-live. Most teams let low-value payments through and send the event again with mode=sync once Sentinel is back, and hold high-value ones. This is called failing open or failing closed, and you can choose per amount band.
Sentinel stores every event it accepts, so nothing goes missing from the record.
Where events come from
Today your engineers call the API directly, as above. Later, connectors will pull events from a rail for you, such as a mobile money operator's API, a bank switch or a card processor. They aren't built yet.