Rule
The rule studio: draft → backtest → activate with two approvers → live alerts.
A bank authors rules as data over the same features the scorer uses. A rule never changes a Skoor: it opens an alert (detector rule:<id>) that always waits for a person. Activation needs a backtest against the tenant's own last 90 days that is younger than 24 hours, a second approver, and writes a versioned rules policy on the record chain.
Rule
| Field | Type | Notes |
|---|
| id | string | Stable id, lowercase. |
| name | string | |
| scope | "transfer" | "entity" | "counterparty" | "program" | Default transfer; only transfer-scoped rules fire today. |
| programId | string | null (uuid) | null = every program on the tenant. |
| when | RuleCondition[] | AND of conditions; at least one. |
| action | "alert" | "hold" | "review" | "log" | |
| severity | "low" | "medium" | "high" | |
| status | "draft" | "testing" | "active" | "paused" | "archived" | |
| note (optional) | string | |
RuleCondition
| Field | Type | Notes |
|---|
| field | "amountCents" | "rail" | "direction" | "type" | "status" | "counterpartyCountry" | "cardDecisionReason" | "entity.verificationStatus" | "entity.pepStatus" | "entity.isHighRisk" | "entity.requiresScreening" | "entity.screeningAgeDays" | "entity.count30d" | "velocity24hCount" | "velocity24hSumCents" | "entity.priorTransfers" | "entity.priorReturns" | "entity.achDebits60d" | "entity.unauthorizedReturns60d" | "structuringWindowCount" | "counterparty.firstTime" | "counterparty.n" | "counterparty.priorReturns" | "counterparty.priorUnauthorizedReturns" | "program.n90d" | "program.medianCents" | "program.p95Cents" | "program.dailyAvgCents30d" | "program.declaredMonthlyVolumeCents" | "program.unauthorizedReturnRate60d" | "declared.countries" | "typology.roundTrip" | "typology.layeringHops" | "typology.dormantDays" | "typology.justBelowThreshold" | A fixed vocabulary over the same features the scorer uses. |
| op | "gt" | "gte" | "lt" | "lte" | "eq" | "neq" | "in" | "not_in" | "between" | "is_null" | "not_null" | |
| value (optional) | string | number | boolean | null | string | number | boolean | null[] | Scalar (string, number, boolean, null) or an array of scalars for in / not_in / between; omitted for is_null / not_null. |
Backtest
| Field | Type | Notes |
|---|
| key | string | |
| ruleId | string | |
| at | string (date-time) | |
| days | integer | |
| scanned | integer | Transfers replayed (capped at 1000). |
| sampled | boolean | True when the cap truncated the window. |
| matched | integer | |
| matchedShare | number | null | |
| wouldHold | integer | |
| wouldAlert | integer | |
| overlapWithExistingAlerts | integer | |
| byBand | Record<string, integer> | |
| sampleTransferIds | string[] | |
RulesVersion
| Field | Type | Notes |
|---|
| id | string | |
| version | string | |
| rules | Rule[] | |
| createdAt | string (date-time) | |
| active | boolean | |
| note (optional) | string | |
Endpoints
get/api/rules
List rules with their last backtest
| Auth | Bearer + X-Operator |
|---|
| Operation | Rule · GET /api/rules |
|---|
Parameters
| Name | In | Type | Notes |
|---|
| tenant (optional) | query | string | Tenant slug. Optional: the bearer token already proves the tenant. Pass it to be explicit. |
Responses
200 — Every rule on the tenant, each with its latest backtest, plus the field vocabulary and operators the DSL accepts. object
| Field | Type | Notes |
|---|
| rules | Rule & object[] | |
| fields | string[] | |
| operators | string[] | |
401 — Bearer token missing or unknown. Error
| Field | Type | Notes |
|---|
| error | string | What went wrong, in one sentence. |
| requestId (optional) | string | Present once the gateway is live: the X-Request-Id of the failed request. |
Example
curl -s "https://skoor.ai/risk/api/rules?tenant=demo" \
-H "Authorization: Bearer test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "X-Operator: you@bank.example"
post/api/rules
Create or update a draft rule
| Auth | Bearer + X-Operator |
|---|
| Operation | Rule · POST /api/rules |
|---|
Parameters
| Name | In | Type | Notes |
|---|
| tenant (optional) | query | string | Tenant slug. Optional: the bearer token already proves the tenant. Pass it to be explicit. |
| Idempotency-Key (optional) | header | string | 1–255 characters. A replay with the same body returns the stored status and body with Idempotent-Replayed: true; a different body under the same key is 409. |
Request body (application/json, required) · RuleDraft
| Field | Type | Notes |
|---|
| id | string | |
| name | string | |
| scope (optional) | "transfer" | "entity" | "counterparty" | "program" | |
| programId (optional) | string | null (uuid) | |
| when | RuleCondition[] | |
| action (optional) | "alert" | "hold" | "review" | "log" | |
| severity (optional) | "low" | "medium" | "high" | |
| note (optional) | string | |
Responses
201 — The rule as stored (status draft). Rule
| Field | Type | Notes |
|---|
| id | string | Stable id, lowercase. |
| name | string | |
| scope | "transfer" | "entity" | "counterparty" | "program" | Default transfer; only transfer-scoped rules fire today. |
| programId | string | null (uuid) | null = every program on the tenant. |
| when | RuleCondition[] | AND of conditions; at least one. |
| action | "alert" | "hold" | "review" | "log" | |
| severity | "low" | "medium" | "high" | |
| status | "draft" | "testing" | "active" | "paused" | "archived" | |
| note (optional) | string | |
400 — Invalid rule; issues lists the Zod problems. Error
| Field | Type | Notes |
|---|
| error | string | What went wrong, in one sentence. |
| requestId (optional) | string | Present once the gateway is live: the X-Request-Id of the failed request. |
401 — Bearer token missing or unknown. Error
| Field | Type | Notes |
|---|
| error | string | What went wrong, in one sentence. |
| requestId (optional) | string | Present once the gateway is live: the X-Request-Id of the failed request. |
409 — The rule is active; pause it before editing. Error
| Field | Type | Notes |
|---|
| error | string | What went wrong, in one sentence. |
| requestId (optional) | string | Present once the gateway is live: the X-Request-Id of the failed request. |
Example
curl -s -X POST "https://skoor.ai/risk/api/rules?tenant=demo" \
-H "Authorization: Bearer test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "X-Operator: you@bank.example" \
-H "Content-Type: application/json" \
-d '{"id":"large-first-time-wire","name":"Large wire to a first-time counterparty","when":[{"field":"rail","op":"eq","value":"wire"},{"field":"counterparty.firstTime","op":"eq","value":true},{"field":"amountCents","op":"gte","value":1000000}],"action":"alert","severity":"high"}'
post/api/rules/{id}/backtest
Backtest a rule against the tenant's own transfers
| Auth | Bearer + X-Operator |
|---|
| Operation | Rule · POST /api/rules/{id}/backtest |
|---|
Parameters
| Name | In | Type | Notes |
|---|
| id | path | string | Rule id. |
| tenant (optional) | query | string | Tenant slug. Optional: the bearer token already proves the tenant. Pass it to be explicit. |
Request body (application/json) · object
| Field | Type | Notes |
|---|
| days (optional) | integer | Window in days (default 90). |
Responses
200 — What the rule would have matched: scanned (capped at 1000, then sampled), matched, share, would-hold, overlap with existing alerts, by band, and sample transfer ids. Backtest
| Field | Type | Notes |
|---|
| key | string | |
| ruleId | string | |
| at | string (date-time) | |
| days | integer | |
| scanned | integer | Transfers replayed (capped at 1000). |
| sampled | boolean | True when the cap truncated the window. |
| matched | integer | |
| matchedShare | number | null | |
| wouldHold | integer | |
| wouldAlert | integer | |
| overlapWithExistingAlerts | integer | |
| byBand | Record<string, integer> | |
| sampleTransferIds | string[] | |
401 — Bearer token missing or unknown. Error
| Field | Type | Notes |
|---|
| error | string | What went wrong, in one sentence. |
| requestId (optional) | string | Present once the gateway is live: the X-Request-Id of the failed request. |
404 — Rule not found. Error
| Field | Type | Notes |
|---|
| error | string | What went wrong, in one sentence. |
| requestId (optional) | string | Present once the gateway is live: the X-Request-Id of the failed request. |
Example
curl -s -X POST "https://skoor.ai/risk/api/rules/large-first-time-wire/backtest?tenant=demo" \
-H "Authorization: Bearer test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "X-Operator: you@bank.example" \
-H "Content-Type: application/json" \
-d '{"days":90}'
post/api/rules/{id}/activate
Activate a rule with a second approver
Needs a backtest younger than 24 hours and an approver distinct from X-Operator. Writes a new rules version and chains a periodic_review action (target rule, <id>@<version>) on the rule's program, else the tenant's first program.
| Auth | Bearer + X-Operator |
|---|
| Operation | Rule · POST /api/rules/{id}/activate |
|---|
Parameters
| Name | In | Type | Notes |
|---|
| id | path | string | Rule id. |
| tenant (optional) | query | string | Tenant slug. Optional: the bearer token already proves the tenant. Pass it to be explicit. |
| Idempotency-Key (optional) | header | string | 1–255 characters. A replay with the same body returns the stored status and body with Idempotent-Replayed: true; a different body under the same key is 409. |
Request body (application/json, required) · object
| Field | Type | Notes |
|---|
| approverId | string | |
Responses
200 — The active rule, the rules version written, and the action id recorded on the chain (null when none). object
| Field | Type | Notes |
|---|
| rule | Rule | |
| version | RulesVersion | |
| actionId | string | null | |
401 — Bearer token missing or unknown. Error
| Field | Type | Notes |
|---|
| error | string | What went wrong, in one sentence. |
| requestId (optional) | string | Present once the gateway is live: the X-Request-Id of the failed request. |
404 — Rule not found. Error
| Field | Type | Notes |
|---|
| error | string | What went wrong, in one sentence. |
| requestId (optional) | string | Present once the gateway is live: the X-Request-Id of the failed request. |
409 — No fresh backtest, same approver, or the rule is archived. Error
| Field | Type | Notes |
|---|
| error | string | What went wrong, in one sentence. |
| requestId (optional) | string | Present once the gateway is live: the X-Request-Id of the failed request. |
Example
curl -s -X POST "https://skoor.ai/risk/api/rules/large-first-time-wire/activate?tenant=demo" \
-H "Authorization: Bearer test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "X-Operator: you@bank.example" \
-H "Content-Type: application/json" \
-d '{"approverId":"second.approver@bank.example"}'
post/api/rules/{id}/pause
Pause an active rule
| Auth | Bearer + X-Operator |
|---|
| Operation | Rule · POST /api/rules/{id}/pause |
|---|
Parameters
| Name | In | Type | Notes |
|---|
| id | path | string | Rule id. |
| tenant (optional) | query | string | Tenant slug. Optional: the bearer token already proves the tenant. Pass it to be explicit. |
Responses
200 — The rule with status paused. Rule
| Field | Type | Notes |
|---|
| id | string | Stable id, lowercase. |
| name | string | |
| scope | "transfer" | "entity" | "counterparty" | "program" | Default transfer; only transfer-scoped rules fire today. |
| programId | string | null (uuid) | null = every program on the tenant. |
| when | RuleCondition[] | AND of conditions; at least one. |
| action | "alert" | "hold" | "review" | "log" | |
| severity | "low" | "medium" | "high" | |
| status | "draft" | "testing" | "active" | "paused" | "archived" | |
| note (optional) | string | |
401 — Bearer token missing or unknown. Error
| Field | Type | Notes |
|---|
| error | string | What went wrong, in one sentence. |
| requestId (optional) | string | Present once the gateway is live: the X-Request-Id of the failed request. |
404 — Rule not found. Error
| Field | Type | Notes |
|---|
| error | string | What went wrong, in one sentence. |
| requestId (optional) | string | Present once the gateway is live: the X-Request-Id of the failed request. |
Example
curl -s -X POST "https://skoor.ai/risk/api/rules/large-first-time-wire/pause?tenant=demo" \
-H "Authorization: Bearer test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "X-Operator: you@bank.example"