Alerts
An alert is "something looked off." It's created automatically by screening hits, transaction-evaluate hits, and rule fires — and can be created manually from the dashboard.
Alerts have a short lifecycle. They either get dismissed (false positive), escalated into a case (worth investigating), or marked for SAR filing (clearly suspicious).
Lifecycle
created → open → ┐
├→ resolved (dismissed by analyst)
├→ escalated (promoted to case)
└→ sar_filed (case + SAR submitted)List alerts
/api/alertsFilters:
| Param | Example | Meaning |
|---|---|---|
lane | onboarding · ongoing · transaction | Restrict to one lane |
status | open · escalated · sar_filed · resolved | Comma-separated for multi-status |
triggerType | screening_hit · rule_fire · threshold · manual | What created the alert |
customerId | cus_01HXY... | Restrict to a single customer |
summary | true | Adds aggregate counts to the response |
page | 1 | 1-indexed pagination |
limit | 20 | Default 20, max 1000 |
Response
{
"ok": true,
"data": [
{
"id": "alt_01HXY...",
"customerId": "cus_01HXY...",
"status": "open",
"lane": "transaction",
"triggerType": "rule_fire",
"riskScore": 65,
"amountIdr": "550000000",
"createdAt": "...",
"customer": { "id": "cus_01HXY...", "fullName": "Umar Patek", "riskRating": "high" },
"cases": []
}
],
"total": 47,
"page": 1,
"limit": 20,
"meta": { "pages": 3 },
"summary": { "open": 12, "escalated": 8, "sarFiled": 3, "resolved": 24 }
}amountIdr is string because IDR amounts can exceed Number.MAX_SAFE_INTEGER over very large transactions — parse with BigInt().
Read a single alert
GET /api/alerts/{id} returns the full alert with hits[] populated:
{
"ok": true,
"data": {
"id": "alt_01HXY...",
"customerId": "cus_01HXY...",
"status": "open",
"lane": "transaction",
"triggerType": "rule_fire",
"riskScore": 65,
"createdAt": "...",
"hits": [
{
"ruleId": "rul_01HXY...",
"ruleName": "High Transaction Amount",
"severity": "medium",
"scoreContribution": 35
}
],
"customer": { ... },
"cases": [],
"screeningId": null,
"transactionId": "txn_01HXY..."
}
}Dismiss
/api/alerts/{id}/dismiss{
"reason": "False positive — counterparty is a known business partner with KYC done."
}Sets status: resolved. The reason field is required (min 20 chars) and audit-logged. Dismissals are reversible by an admin from the dashboard if needed.
Escalate to case
/api/alerts/{id}/escalateIf the customer has an open case, the alert is linked to it. Otherwise a new case is created. Response:
{
"ok": true,
"data": {
"alert": { "id": "alt_01HXY...", "status": "escalated" },
"case": { "id": "cas_01HXY...", "title": "Case · rule_fire", "status": "open", "newlyCreated": true }
}
}You can pass { "caseId": "cas_01HXZ..." } to link to a specific existing case instead of the auto-pick.
Mark for SAR filing
/api/alerts/{id}/sarDoesn't actually file the SAR — that's a separate dashboard workflow. This just bumps status: sar_filed and surfaces the alert in the MLRO's SAR queue.
Listing strategies
Operational dashboards. Use ?summary=true with no filters to power KPI tiles. The summary block returns counts cheaply without scanning all rows.
Analyst inbox. Filter ?status=open&triggerType=screening_hit&page=1&limit=50 for the "new screening hits to review" queue.
Compliance audit. Use cursor pagination via webhook delivery logs rather than scanning /api/alerts?from=...&to=... — list endpoints are not optimized for full-history exports. For full exports, use the dashboard's data export feature.
Don't over-poll
Polling /api/alerts?status=open&since=... every few seconds works at low volume but burns rate limit at scale. Subscribe to alert.created and alert.escalated webhooks instead — see Webhooks.