API reference

Alerts

List the alert queue, assign and update alerts, add notes, and close them with four-eyes approval.

A review or block decision puts the event on an alert. Alerts are grouped by source: while a source has an open or in_review alert, its later review and block decisions join that alert instead of raising new ones. occurrences counts them. Once an alert is pending closure or closed, the next decision for that source starts a new alert.

An alert starts at medium priority for a review and high for a block. A block that joins a medium alert raises it to high. decision_id and event_id point at the highest-scoring decision on the alert.

Decisions for events without a source.entity_id can't be grouped, so each gets its own alert.

A screening with a potential match raises an alert too. See Screening alerts.

Statuses

StatusMeaningHow it gets there
openWaiting for an analystNew alert, or set with PATCH
in_reviewBeing workedSet with PATCH, or a rejected closure
pending_closureSomeone proposed a disposition and it's waiting for a second personPOST .../closure
closedA second person approved the closurePOST .../closure/approve

Alerts that are pending_closure or closed can't be edited with PATCH. Every change is written to the audit log with the key that made it.

List alerts

GET /v1/alerts

Alerts a page at a time. Each carries the decision and event behind it, so a queue renders from one request. With queue=open they come soonest deadline (due_at) first; otherwise most recently changed first. Pass limit (1 to 200, default 50); if there are more, the response has a next_cursor to send back as cursor.

Query parameters

queue
string
open (open and in review), pending_closure or closed.
status
string
A single status: open, in_review, pending_closure or closed. Ignored when queue is set.
assigned_to
string
Only alerts assigned to this name.
decision
string
review or block. Screening alerts count as review.
q
string
Part of the source's id or name, or an alert number such as ALR-20481.
limit, cursor
integer, string
Paging, as above.

Counts

GET /v1/alerts/counts

How many alerts are in each queue, counted on the server, so tab counts are right however many alerts there are. Pass assigned_to to fill mine.

Response · 200
{
  "open": 38,
  "in_review": 7,
  "pending_closure": 2,
  "closed": 1204,
  "mine": 5,
  "overdue": 2,
  "near_sla": 3,
  "median_close_minutes": 252
}

open includes those in review. overdue and near_sla are open alerts past their due_at or due within the hour. median_close_minutes covers alerts closed in the last 7 days and is absent when none were.

Get an alert

GET /v1/alerts/{id}

One alert, with the same fields as the list. An unknown id is a 404 with alert not found.

Example

Terminal
curl "$SENTINEL_URL/v1/alerts?status=open" \
  -H "Authorization: Bearer $SENTINEL_KEY"
Response · 200
{
  "alerts": [
    {
      "id": "c7d4e1a2-5b6c-4d7e-8f9a-0b1c2d3e4f5a",
      "number": 20481,
      "decision_id": "a81e4f02-6c9d-4b7a-9e3f-2d1c0b9a8f7e",
      "event_id": "c4d5e6f7-0a1b-4c2d-8e9f-3a4b5c6d7e8f",
      "status": "open",
      "priority": "medium",
      "occurrences": 3,
      "last_event_at": "2026-10-15T11:21:40Z",
      "due_at": "2026-10-16T11:09:12.318Z",
      "created_at": "2026-10-15T11:09:12.318Z",
      "updated_at": "2026-10-15T11:21:40.552Z",
      "decision": "review",
      "risk_score": 60,
      "decided_in_ms": 38,
      "reasons": [
        {
          "rule": "structuring_below_threshold",
          "version": 1,
          "detail": "repeated transfers just below the reporting threshold within 24h",
          "weight": 60
        }
      ],
      "source": {
        "id": "cus_10442",
        "kind": "customer",
        "name": "Ama Mensah"
      },
      "event": {
        "type": "transfer.outbound",
        "occurred_at": "2026-10-15T11:09:12Z",
        "amount": {
          "value": 4900,
          "currency": "USD"
        },
        "rail": "mobile_money",
        "source_id": "cus_10442"
      }
    }
  ]
}

Update an alert

PATCH /v1/alerts/{id}

Changes an alert's status, its assignee, or both. Status can only move between open and in_review here. Closing goes through closure.

Path parameters

id
string, uuid required
The alert's ID.

Request body

Send either field, or both.

status
string
open or in_review.
assigned_to
string
Who is working the alert. Free text, for example a name or an email address. Must not be empty.

Example

Terminal
curl -X PATCH "$SENTINEL_URL/v1/alerts/c7d4e1a2-5b6c-4d7e-8f9a-0b1c2d3e4f5a" \
  -H "Authorization: Bearer $SENTINEL_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "status": "in_review",
    "assigned_to": "analyst@example.com"
  }'
Response · 200
{
  "id": "c7d4e1a2-5b6c-4d7e-8f9a-0b1c2d3e4f5a",
  "decision_id": "a81e4f02-6c9d-4b7a-9e3f-2d1c0b9a8f7e",
  "event_id": "c4d5e6f7-0a1b-4c2d-8e9f-3a4b5c6d7e8f",
  "status": "in_review",
  "priority": "medium",
  "assigned_to": "analyst@example.com",
  "occurrences": 3,
  "last_event_at": "2026-10-15T11:21:40Z",
  "created_at": "2026-10-15T11:09:12.318Z",
  "updated_at": "2026-10-15T11:25:02.877Z"
}

Notes

GET /v1/alerts/{id}/notesPOST /v1/alerts/{id}/notes

GET returns the alert's notes, oldest first. POST adds one and returns 201. Notes can be added at any status, including after the alert is closed. Each note records the key that wrote it as author.

body
string required
The note. 1 to 5,000 characters.
Terminal
curl -X POST "$SENTINEL_URL/v1/alerts/c7d4e1a2-5b6c-4d7e-8f9a-0b1c2d3e4f5a/notes" \
  -H "Authorization: Bearer $SENTINEL_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "body": "Customer confirmed these are rent payments split across three days. Lease on file."
  }'
Response · 201
{
  "id": "0d9c8b7a-6f5e-4d3c-2b1a-0f9e8d7c6b5a",
  "alert_id": "c7d4e1a2-5b6c-4d7e-8f9a-0b1c2d3e4f5a",
  "author": "3f2e1d0c-9b8a-4765-8432-10fedcba9876",
  "body": "Customer confirmed these are rent payments split across three days. Lease on file.",
  "created_at": "2026-10-15T11:31:07.204Z"
}

GET returns { "notes": [...] } with the same fields.

Closing an alert

Closing takes two people. One key proposes a disposition, and a different key approves it.

DispositionMeaning
false_positiveNothing suspicious
escalatedPassed on for a deeper investigation
reportedReported to your FIU. Sentinel records the disposition but doesn't file anything; reporting isn't built yet.

Request closure

POST /v1/alerts/{id}/closure

Moves an open or in_review alert to pending_closure and records who asked.

disposition
string required
false_positive, escalated or reported.
Terminal
curl -X POST "$SENTINEL_URL/v1/alerts/c7d4e1a2-5b6c-4d7e-8f9a-0b1c2d3e4f5a/closure" \
  -H "Authorization: Bearer $SENTINEL_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "disposition": "false_positive"
  }'

The response is the alert, now with status set to pending_closure, plus disposition and closure_requested_by.

Approve closure

POST /v1/alerts/{id}/closure/approve

Closes a pending_closure alert. It must be called with a different key from the one that requested closure, or it returns 403. The response is the alert with status set to closed and closed_by set. No body is needed.

Reject closure

POST /v1/alerts/{id}/closure/reject

Sends a pending_closure alert back to in_review and clears the disposition. Any key can reject, including the one that asked, so it also works as "withdraw my request". No body is needed.

Alert fields

FieldTypeMeaning
idstring, uuidThe alert's ID
numberintegerA short reference. The dashboard shows it as ALR-20481.
decision_idstring, uuidThe highest-scoring decision on the alert. Absent on screening alerts.
event_idstring, uuidThe event behind that decision. Absent on screening alerts.
statusstringopen, in_review, pending_closure or closed
prioritystringmedium or high
occurrencesintegerHow many review or block decisions are on this alert
last_event_atstring, date-timeWhen the latest of those events happened
due_atstring, date-timeWhen it should be decided by: created_at plus the SLA for its priority, set when the alert is raised. A block decision that makes it high priority brings it forward. Changing SENTINEL_SLA_HIGH or SENTINEL_SLA_MEDIUM affects new alerts only. See Installation.
assigned_tostringWho is working it. Absent until assigned.
dispositionstringSet once closure is requested
closure_requested_bystringThe key ID that requested closure
closed_bystringThe key ID that approved it
created_atstring, date-timeWhen it was raised
updated_atstring, date-timeWhen it last changed

The list adds these from the decision and event behind the alert:

FieldTypeMeaning
decisionstringreview or block
risk_scoreinteger0 to 100
decided_in_msintegerHow long the engine took. Absent on decisions made before this was recorded.
reasonsarrayThe rules that fired. Each has rule, version, detail and weight, the points it added. weight is absent on older decisions.
sourceobjectThe entity the alert is about: id, plus kind and name if the entity was registered with POST /v1/entities and has a name attribute
eventobjecttype, occurred_at, amount, rail and source_id
screening_idstring, uuidThe screening that raised the alert. Only on screening alerts.

Screening alerts

A screening that comes back potential_match raises an alert in the same transaction, so there's never one without the other. It's high priority if any match is on a sanctions list, and medium otherwise. Its source is the screening's reference.

In the list it reads like any other alert, with these differences:

FieldOn a screening alert
screening_idThe screening's ID. Fetch it with GET /v1/screenings/{id}.
decision_id, event_idAbsent. There's no event behind it.
decisionAlways review
risk_scoreThe best match's score
reasonsOne per match. rule is screening_ plus the list category, for example screening_sanctions, and detail names the entry, list and score. There's no weight.
sourceid is the screening's reference. With no reference, it's the name that was checked. kind and name come from the registered entity if there is one, otherwise from the screening.
eventtype is screening and occurred_at is when the screening ran. There's no amount or rail.
A screening alert in the list
{
  "id": "e2f1a0b9-8c7d-4e6f-9a5b-4c3d2e1f0a9b",
  "number": 20502,
  "screening_id": "4c3b2a19-0f8e-4d7c-9b6a-5e4d3c2b1a09",
  "status": "open",
  "priority": "high",
  "occurrences": 1,
  "last_event_at": "2026-10-16T10:41:27.630Z",
  "due_at": "2026-10-16T14:41:27.630Z",
  "created_at": "2026-10-16T10:41:27.630Z",
  "updated_at": "2026-10-16T10:41:27.630Z",
  "decision": "review",
  "risk_score": 100,
  "reasons": [
    {
      "rule": "screening_sanctions",
      "version": 1,
      "detail": "Close to Mohammed Sani Al-Rashid (SMP-0003) on sample, score 100"
    }
  ],
  "source": {
    "id": "cus_1043",
    "kind": "person",
    "name": "Muhammad Sani Rashid"
  },
  "event": {
    "type": "screening",
    "occurred_at": "2026-10-16T10:41:27.630Z",
    "source_id": "cus_1043"
  }
}

Work the matches through match review. When every match is cleared and a second person has approved each one, the alert closes by itself as false_positive, and the audit log records alert.closed. A confirmed true match leaves the alert open, so someone decides what happens next. You can also close a screening alert through closure like any other.

Funds flow

GET /v1/alerts/{id}/flow

Money in and out of the alert's source, per hour, for the 24 hours up to its latest event. Money out is events whose source.entity_id is the source. Money in is events whose destination.entity_id is the source, so send destination on your events if you want this filled. Only hours with money moving are listed.

Terminal
curl "$SENTINEL_URL/v1/alerts/c7d4e1a2-5b6c-4d7e-8f9a-0b1c2d3e4f5a/flow"   -H "Authorization: Bearer $SENTINEL_KEY"
Response · 200
{
  "entity_id": "cus_10442",
  "currency": "GHS",
  "since": "2026-10-14T11:21:40Z",
  "until": "2026-10-15T11:21:40Z",
  "hours": [
    {
      "hour": "2026-10-15T08:00:00Z",
      "in": 41200,
      "out": 0
    },
    {
      "hour": "2026-10-15T09:00:00Z",
      "in": 0,
      "out": 38750
    }
  ]
}

currency is taken from the events and assumes one currency per source. If the event behind the alert named no source, entity_id is absent and hours is empty.

Errors

EndpointCodeerror
All401missing or invalid API key
All with {id}404alert not found
PATCH400unknown status
PATCH400status can only be open or in_review here; close alerts through /closure
PATCH400assigned_to cannot be empty
PATCH409alert is pending closure or closed
POST .../notes400body must be 1 to 5000 characters
POST .../closure400unknown disposition
POST .../closure409only open or in_review alerts can be put up for closure
POST .../closure/approve403the person who requested closure cannot approve it
POST .../closure/approve, .../reject409alert is not pending closure
Sentinel is a Somu Labs product. © 2026 Somu Labs Ltd.