Rules API
Ce contenu n’est pas encore disponible dans votre langue.
These endpoints manage the rules described in Managing rules. The web app’s Rules screens use them. Most people never call them directly; this page is for teams that script rule changes, export rule definitions for review, or check behaviour precisely.
Before you start
Section titled “Before you start”Who can call them. Rule management is for signed-in people, not API keys. Every change is
attributed to a named user, checked against their permissions and the four-eyes rule, and written
to the audit log. An API key gets 401 sign in required.
Signing in with curl. Sign in once and keep the session cookie in a file:
export SP=https://speculapro.example.com
curl -s -c ~/.sp-cookies -H 'Content-Type: application/json' \ -d '{"workspace":"acme-pay","email":"[email protected]","password":"<your password>","totp_code":"123456"}' \ "$SP/api/v1/auth/login"Then add -b ~/.sp-cookies to every request below. The session follows the usual
rules: it ends after 30 minutes without activity, or 12 hours at most.
Conventions. IDs are UUIDs. Times are RFC 3339 in UTC. Bodies are JSON with
Content-Type: application/json. Amounts inside rule parameters are in major units of your
base currency (50000 is GHS 50,000), because that is how rules compare them.
Endpoints at a glance
Section titled “Endpoints at a glance”| Endpoint | Does | Permission | Success |
|---|---|---|---|
GET /api/v1/rule-packs |
List the packs available to your installation and which are installed | rules.view |
200 |
POST /api/v1/rule-packs/{key}/install |
Install a pack; its rules arrive stopped | rules.approve |
200 {"added": n} |
POST /api/v1/rule-packs/{key}/upgrade |
Move an installed pack to the newest available version: new rules arrive stopped, changed rules get a new version for approval | rules.approve |
200 with the upgrade report |
POST /api/v1/rule-packs/bundles |
Load a signed pack file, through the same upgrade path as /upgrade |
rules.approve (forcing a downgrade also needs rules.packs_downgrade) |
200 |
GET /api/v1/rules/catalog |
Every field a rule can read, the aggregates it can compute and the actions per mode | rules.view |
200 |
POST /api/v1/rules/test |
Try a definition on a sample transaction, without saving | rules.view |
200 |
GET /api/v1/rules |
List rules with their live and shadow versions and 30-day hit counts | rules.view |
200 |
GET /api/v1/rules/{id} |
One rule with its full version history | rules.view |
200 |
POST /api/v1/rules |
Create a custom rule, as draft version 1 | rules.author |
201 {"id": …} |
POST /api/v1/rules/validate |
Check a definition without saving it | rules.author |
200 {"valid": …, "problems": […]} |
POST /api/v1/rules/{id}/versions |
Draft a new version | rules.author |
201 {"id": …} |
POST /api/v1/rule-versions/{id}/submit |
Send a draft for approval | rules.author |
204 |
POST /api/v1/rule-versions/{id}/approve |
Approve (never your own version) | rules.approve |
204 |
POST /api/v1/rule-versions/{id}/reject |
Reject | rules.approve |
204 |
POST /api/v1/rules/{id}/deploy |
Put an approved version in the active (live) or shadow slot |
rules.approve |
204 |
DELETE /api/v1/rules/{id}/shadow |
Stop the shadow test | rules.approve |
204 |
POST /api/v1/rules/{id}/enable |
Start running the rule | rules.approve |
204 |
POST /api/v1/rules/{id}/disable |
Stop running the rule | rules.approve |
204 |
GET /api/v1/rules/{id}/runs |
A scheduled rule’s schedule, next run and latest runs | rules.view |
200 |
PUT /api/v1/rules/{id}/schedule |
Change when a scheduled rule runs | rules.approve |
204 |
Errors
Section titled “Errors”Errors use Problem Details: a JSON object with
status, title and detail, plus an errors list for validation problems.
| Status | Meaning | Examples |
|---|---|---|
401 |
Not signed in, or called with an API key | sign in required |
403 |
Missing permission, approving your own version, or (self-hosted) installing, upgrading or loading a pack, or creating a rule or version, while the licence is restricted | a version must be approved by someone other than its author; only an administrator can force a downgrade |
404 |
No such rule, version or pack in your workspace | unknown rule pack "…" |
409 |
The action doesn’t fit the current state | pack gh-psp is not installed (upgrade); pack core 1.2.0 is installed; the newest available is 1.1.0 (upgrade); only draft versions can be submitted; only versions pending approval can be decided; only approved versions can be deployed (this one is draft); version belongs to another rule; rule not found or has no approved active version |
422 |
The definition or request is invalid. Every problem is listed, not just the first | features.out_7d is used but not declared; explanation {{= no_such_var }}: ERROR: <input>:1:1: undeclared reference to 'no_such_var' …; params.threshold is used but has no value; action "block" isn't available to post-event rules (use alert, tag, score_only) |
A refused request changes nothing.
GET /api/v1/rule-packs
Section titled “GET /api/v1/rule-packs”curl -s -b ~/.sp-cookies "$SP/api/v1/rule-packs"[ { "key": "core", "version": "1.1.0", "name": "Core AML & fraud", "description": "Market-neutral rules for every institution. Thresholds are starting points: calibrate with backtesting before go-live.", "jurisdiction": null, "rules": 24, "installed_version": "1.0.2", "source": "embedded" }, { "key": "gh-bank", "version": "1.0.0", "name": "Ghana banks", "description": "Typologies for Ghanaian universal and rural banks: branch cash and structuring across branches, …", "jurisdiction": "GH", "rules": 10, "installed_version": null, "source": "embedded" }]| Field | Meaning |
|---|---|
version |
The newest version available to your installation: the one built into your SpeculaPro, or a newer one from a signed pack file |
installed_version |
The version your workspace has, or null if the pack isn’t installed. When it differs from version, as for core above, an upgrade is available |
rules |
How many rules that version has |
source |
embedded (built into your SpeculaPro) or bundle (from a signed pack file) |
amounts_currency, workspace_currency |
The currency the pack’s default amounts are written in (empty when the pack states none, as for core and fraud), and your workspace’s base currency |
amounts_for_base_currency |
true when the pack has default amounts for your base currency; install and upgrade use them |
currency_mismatch |
true when installing would bring in amounts meant for another currency: set every amount before enabling the pack’s rules. See Packs written in another currency |
POST /api/v1/rule-packs/{key}/install
Section titled “POST /api/v1/rule-packs/{key}/install”curl -s -b ~/.sp-cookies -X POST "$SP/api/v1/rule-packs/core/install"{ "added": 16 }Each rule arrives as version 1, approved, in the live slot, but stopped. Nothing runs until someone enables it.
- Installing again is harmless. If the pack is already installed, nothing changes and
addedis0. - Your rules are never overwritten. If a rule with the same key already exists in your workspace, the pack skips it.
Audit: rule_pack.install, with the pack, its version and the rules added.
POST /api/v1/rule-packs/{key}/upgrade
Section titled “POST /api/v1/rule-packs/{key}/upgrade”curl -s -b ~/.sp-cookies -X POST "$SP/api/v1/rule-packs/core/upgrade"{ "pack": "core", "from": "1.0.2", "to": "1.1.0", "added": 8, "items": [ { "rule_key": "core.large_transaction", "library_key": "A1", "name": "Large single transaction", "rule_id": "0192f1c4-…", "outcome": "updated", "version_id": "0193a7e2-…", "kept_params": ["threshold"], "detail": "A new version awaits approval by someone else; your current version keeps running until it is deployed." }, { "rule_key": "core.count_spike", "library_key": "C1", "name": "Transaction count spike against the account's own history", "outcome": "added" }, { "rule_key": "core.structuring", "library_key": "B1", "name": "Repeated deposits just below a threshold", "rule_id": "0192f1c4-…", "outcome": "customised", "detail": "Your workspace changed this rule's logic, so the pack's new definition wasn't applied. Compare them and update it by hand if you want it." } ]}Moves an installed pack to the newest version available, and reports what happened to every
rule in it, in items:
outcome |
Meaning |
|---|---|
added |
New in this version: added like an install, version 1, approved, live slot, stopped |
updated |
The packaged definition changed. A new version, version_id, was drafted from it with your tuning carried over (kept_params lists the parameters, and severity or score, whose values you had changed and which were kept) and sent for approval. The upgrading person is its author, so someone else must approve and deploy it. Your current version keeps running |
needs_attention |
The definition changed, but a value you tuned doesn’t validate against the new one, or the pack moved the rule to another mode. Nothing was created; detail says why |
customised |
You changed this rule’s expression or features, so the new definition wasn’t applied |
unchanged |
Nothing to do. Also used, with a detail, for a rule with the same key that belongs to another pack or that you wrote |
- If the pack is already at the newest version,
fromandtoare equal and nothing changes. - If the installed version is newer than the newest available, you get
409pack … … is installed; the newest available is …. Packs never go backwards through this endpoint. - If the pack isn’t installed, you get
409pack … is not installed. Install it instead.
See How to upgrade a pack for how tuning is carried over.
Audit: rule_pack.upgrade, with the old version, the new version, rules_added and report: every
item except the unchanged ones.
POST /api/v1/rule-packs/bundles
Section titled “POST /api/v1/rule-packs/bundles”Loads a signed pack file (.sppack) from
SpeculaPro. Send the file’s contents as text:
jq -Rs '{bundle: .}' gh-bank-1.1.0.sppack | \ curl -s -b ~/.sp-cookies -H 'Content-Type: application/json' -d @- "$SP/api/v1/rule-packs/bundles"{ "key": "gh-bank", "version": "1.1.0", "name": "Ghana banks", "key_id": "pack-2026-1", "previous_version": "1.0.0", "action": "upgraded", "rules_added": 0, "report": { "…": "the same per-rule report as /upgrade" }}| Field | Meaning |
|---|---|
bundle |
The file’s contents (request) |
force |
true to install a version older than the installed one (request, optional). Needs rules.packs_downgrade as well as rules.approve; otherwise 403 forcing a pack downgrade needs the rules.packs_downgrade permission (the MLRO role has it) |
action |
installed (the pack wasn’t installed), upgraded, downgraded (forced) or unchanged (same version) |
report |
For upgraded and downgraded: what happened to each rule, as for /upgrade |
key_id |
The SpeculaPro key that signed the file |
The signature is checked before anything in the file is read. The file is stored for the whole
installation, and other workspaces can then upgrade the pack with
/upgrade. In this workspace, loading the file is an upgrade
like /upgrade: new rules are added, stopped; rules whose definition changed get new versions with
your tuning, awaiting someone else’s approval; rules you changed are left alone and reported.
| Status | detail |
Why |
|---|---|---|
422 |
rule-pack bundle rejected: rule-pack bundle is not valid: signature does not match (or signed with an unknown key …, signed for a different purpose …, not a signed SpeculaPro document …) |
The file is damaged, altered, or not a pack file |
409 |
conflict: pack … differs from the built-in pack of the same version |
Same version as the pack built into SpeculaPro, different rules |
409 |
conflict: pack … is already present with different content |
A different file with this pack and version was loaded before |
409 |
conflict: … is older than the installed version …; someone allowed to downgrade packs can force it |
Send "force": true (with rules.packs_downgrade) to go back |
Audit: rule_pack.install, rule_pack.upgrade or rule_pack.downgrade, with source: bundle,
key_id, the versions, the rules added and, for an upgrade or downgrade, the per-rule report. A
downgrade also records forced: true.
Catalog and test
Section titled “Catalog and test”The rule builder is built
from these two endpoints. Both only need rules.view, and neither saves anything.
GET /api/v1/rules/catalog
Section titled “GET /api/v1/rules/catalog”curl -s -b ~/.sp-cookies "$SP/api/v1/rules/catalog"{ "fields": [ { "path": "txn.amount", "type": "number", "label": "Amount", "doc": "In the workspace's base currency, major units (e.g. cedis)", "modes": ["realtime", "post_event"] }, { "path": "txn.channel", "type": "string", "label": "Channel", "doc": "", "enum": ["momo", "bank_transfer", "…"], "modes": ["realtime", "post_event"] }, { "path": "customer.hours_since", "type": "map", "label": "Hours since each account event", "doc": "customer.hours_since.sim_swap; check presence with \"sim_swap\" in customer.hours_since", "enum": ["login", "login_failed", "…"], "modes": ["realtime", "post_event"] } ], "features": { "entities": ["customer", "account", "counterparty", "device"], "aggs": ["count", "sum", "max", "avg", "distinct_counterparties", "distinct_customers"], "directions": ["any", "in", "out", "internal", "event"], "types": ["transfer", "payment", "…"], "event_types": ["login", "login_failed", "…"], "channels": ["momo", "bank_transfer", "…"], "windows": ["5m", "10m", "15m", "30m", "1h", "2h", "6h", "24h", "48h", "168h", "720h", "2160h", "4320h"] }, "actions": { "realtime": ["block", "review", "step_up", "allow", "tag", "score_only"], "post_event": ["alert", "tag", "score_only"] }}| Part | What it holds |
|---|---|
fields |
Every variable an expression can read: its path, type (number, integer, string, bool, list or map), a label and doc, the allowed values in enum where there is a fixed set, and the modes it carries data in. It matches the CEL reference |
features |
What a feature can be built from. windows are suggestions: any duration from 1m to 4320h works |
actions |
The actions each mode allows |
POST /api/v1/rules/test
Section titled “POST /api/v1/rules/test”Compiles a definition and evaluates it once against a sample you describe. Nothing is saved and no real data is read.
curl -s -b ~/.sp-cookies -H 'Content-Type: application/json' -d '{ "mode": "realtime", "definition": { "expression": "txn.direction == '\''out'\'' && device.is_new && features.payouts_10m >= double(params.max_payouts)", "features": { "payouts_10m": { "entity": "customer", "agg": "count", "direction": "out", "window": "10m" } }, "params": { "max_payouts": 3 }, "severity": "high", "score": 70, "action": "review", "explanation": "{{features.payouts_10m}} payouts in 10 minutes from a new device." }, "sample": { "txn": { "direction": "out", "amount": 2500 }, "device": { "known": true, "is_new": true }, "features": { "payouts_10m": 4 } } }' "$SP/api/v1/rules/test"{ "valid": true, "hit": true, "explanation": "4 payouts in 10 minutes from a new device." }| Field | Required | Meaning |
|---|---|---|
mode |
Yes | realtime or post_event. The action must be one the mode allows |
definition |
Yes | As for validate |
sample |
Yes (may be {}) |
txn, customer, account, counterparty and device objects with the fields you want to set; features by the names the definition declares; lists as {"key": ["value", …]} |
Fields you leave out take the same neutral defaults as live evaluation (for example -1 for
unknown ages and distances, false for the new-device and new-country signals). The sample’s
transaction defaults to an outgoing transfer on mobile_app at 12:00 on a Wednesday. A list you
don’t put in sample.lists reads as empty. Features are not worked out from history: they take
the value you give, or 0.
| Response field | Meaning |
|---|---|
valid |
false if the definition doesn’t compile or the action doesn’t fit the mode; problems then lists why |
hit |
Whether the rule fires on this sample |
explanation |
The explanation, filled in, when it fires |
error |
The sample couldn’t be evaluated, for example a field with the wrong type; hit is then false |
GET /api/v1/rules
Section titled “GET /api/v1/rules”Lists every rule in your workspace. Each rule looks like this (shortened):
{ "id": "0192f1c4-7d1e-7a3b-9c55-2f8e1a6b0d41", "key": "core.structuring", "pack_key": "core", "library_key": "B1", "name": "Repeated deposits just below a threshold", "category": "structuring", "typology": "Structuring / smurfing", "mode": "post_event", "entity_scope": "customer", "enabled": false, "active": { "id": "0192f1c4-7d1f-7c02-8e11-5b0f3e2a9c77", "version": 1, "state": "approved", "expression": "txn.type == 'cash_in' && txn.amount >= params.threshold * (1.0 - params.margin) && …", "features": { "cash_in_count_48h": { "entity": "account", "agg": "count", "direction": "in", "type": "cash_in", "window": "48h" } }, "params": { "threshold": 10000, "margin": 0.1, "min_count": 3 }, "params_schema": { "threshold": { "type": "number", "title": "Threshold being avoided", "minimum": 0 } }, "severity": "high", "score": 70, "action": "alert", "explanation": "{{features.cash_in_count_48h}} cash-ins totalling {{txn.base_currency}} {{features.cash_in_sum_48h}} in 48h; …", "change_note": "Installed from pack core 1.0.0", "created_at": "2026-10-01T08:12:40Z" }, "pending_versions": 0, "hits_30d": 0, "shadow_hits_30d": 0}| Field | Meaning |
|---|---|
key |
The rule’s unique name in your workspace. Pack rules start with the pack’s prefix (core., gh.); custom rules with custom. |
library_key |
Its code in the rule library, for example B1 |
entity_scope |
Who its alerts are about: customer, account or counterparty, or agent for rules about an agent (the pack rules G1 and ng.agent_daily_cash_out, or a custom rule created through the API): their alerts are about the agent as a counterparty (for the scheduled G1, the agent’s account when it has one) |
mode |
post_event, realtime or scheduled. Real-time rules run only in real-time decisions; scheduled rules on their timetable |
schedule |
Scheduled rules only: when it runs, such as daily 02:00, in your workspace’s time zone |
enabled |
true while the rule is running |
active |
The live version, or absent if none has been deployed |
shadow |
The version being shadow-tested, or absent |
pending_versions |
How many versions are waiting for approval |
hits_30d, shadow_hits_30d |
Live and shadow hits in the last 30 days. For a real-time rule, the decisions it fired in |
Each version carries params (the numbers), params_schema (what each parameter means: its
title, type and allowed range), severity, score, action, explanation, and the logic
(expression, features). A version saved from the rule builder also carries builder: the
builder’s own description of the conditions, so it reopens there. It is informational; the
expression is what runs. It also carries change_note, created_by and, once decided,
decided_by and decided_at. created_by and decided_by are display names; versions installed
from a pack have no created_by.
GET /api/v1/rules/{id}
Section titled “GET /api/v1/rules/{id}”The same object, plus versions: every version of the rule, newest first, in any state.
Versions
Section titled “Versions”POST /api/v1/rules/{id}/versions
Section titled “POST /api/v1/rules/{id}/versions”Drafts a new version. Send only what changes: any field you leave out (expression,
features, params, severity, score, action, explanation) is copied from the latest
version of the rule.
RULE=0192f1c4-8a20-7b51-a3e9-6c1d0f4e2b18 # id of core.large_transaction
curl -s -b ~/.sp-cookies -H 'Content-Type: application/json' \ -d '{ "params": { "threshold": 75000, "channels": ["bank_transfer", "instant_payment"] }, "change_note": "Week-1 review: 41 of 44 hits were routine supplier payments under GHS 75k. Raise threshold, limit to bank channels. Ticket CMP-112." }' \ "$SP/api/v1/rules/$RULE/versions"{ "id": "0192f1d0-11aa-7e4c-9b20-4a7d3c8e5f01" }The response is 201 Created; the id is the new version’s id. The version is a draft: it
does nothing until it is submitted, approved and deployed.
| Field | Required | Meaning |
|---|---|---|
change_note |
Yes, 3 to 500 characters | Why the change. The approver reads it first, and it stays in the audit trail |
params |
No | The complete set of parameters (see below) |
severity |
No | low, medium, high or critical |
score |
No | 0 to 100 |
action |
No | One the rule’s mode allows: alert, tag or score_only for post-event rules; block, review, step_up, allow, tag or score_only for real-time rules. See what a rule produces |
explanation |
No | Up to 500 characters. Plain placeholders {{txn.amount}} and computed ones {{= <CEL> }}; a computed one that doesn’t compile is refused with explanation {{= … }}: …. See Explanations |
expression, features |
No | The logic. See CEL reference and Features reference |
params_schema |
No | Replaces the parameter descriptions (title, type, range), for example when you add or remove a parameter |
builder |
No | The rule builder’s model. If you send expression without builder, the previous builder model is dropped, so the rule next opens as code |
The draft is checked when you save it. If anything is wrong you get 422 with every problem:
{ "title": "Unprocessable Entity", "status": 422, "detail": "rule is not valid", "errors": [ { "message": "features.out_7d is used but not declared" }, { "message": "params.threshold is used but has no value" } ]}Audit: rule_version.create, with the previous version and parameters and the new draft.
POST /api/v1/rule-versions/{id}/submit
Section titled “POST /api/v1/rule-versions/{id}/submit”curl -s -b ~/.sp-cookies -X POST "$SP/api/v1/rule-versions/0192f1d0-11aa-7e4c-9b20-4a7d3c8e5f01/submit"204 means the version is now pending_approval. Only a draft can be submitted; anything else
returns 409 only draft versions can be submitted. Audit: rule_version.submit.
POST /api/v1/rule-versions/{id}/approve and /reject
Section titled “POST /api/v1/rule-versions/{id}/approve and /reject”Called by someone with rules.approve who is not the version’s author. The note is optional
(up to 500 characters) but strongly recommended, especially when rejecting:
curl -s -b ~/.sp-cookies -H 'Content-Type: application/json' \ -d '{ "note": "Agreed. Revisit after month-end volumes." }' \ "$SP/api/v1/rule-versions/0192f1d0-11aa-7e4c-9b20-4a7d3c8e5f01/approve"| Response | Meaning |
|---|---|
204 |
Decided |
403 a version must be approved by someone other than its author |
You drafted this version. The database refuses it too, so no script can get around it |
409 only versions pending approval can be decided |
It hasn’t been submitted, or it was already decided |
Once approved, a version’s logic is frozen: its expression, features, parameters, severity,
score and action can never change. A rejected version can’t be resubmitted; draft a new one.
Audit: rule_version.approve or rule_version.reject, with the note.
Deployment and switching on and off
Section titled “Deployment and switching on and off”POST /api/v1/rules/{id}/deploy
Section titled “POST /api/v1/rules/{id}/deploy”Puts an approved version in one of the rule’s two slots:
slot |
Effect |
|---|---|
active |
The version goes live: its hits raise alerts. If it was the shadow version, the shadow slot empties |
shadow |
The version runs on live traffic next to the live one and records hits, but never raises alerts. It replaces any version already in shadow |
curl -s -b ~/.sp-cookies -H 'Content-Type: application/json' \ -d '{ "version_id": "0192f1d0-11aa-7e4c-9b20-4a7d3c8e5f01", "slot": "shadow" }' \ "$SP/api/v1/rules/$RULE/deploy"The running system picks the change up within about 15 seconds. To roll back, deploy an
older approved version to active; no new approval is needed, because it was already approved.
| Response | Meaning |
|---|---|
204 |
Deployed |
409 only approved versions can be deployed (this one is draft) |
Approve it first |
409 version belongs to another rule |
The version id and rule id don’t match |
Audit: rule.deploy, with what was in each slot before.
DELETE /api/v1/rules/{id}/shadow
Section titled “DELETE /api/v1/rules/{id}/shadow”Stops the shadow test by emptying the shadow slot. The version stays approved and its hits stay on
record. 204 on success, also when no test was running. 404 if there is no such rule in your
workspace. Audit: rule.clear_shadow.
POST /api/v1/rules/{id}/enable and /disable
Section titled “POST /api/v1/rules/{id}/enable and /disable”curl -s -b ~/.sp-cookies -X POST "$SP/api/v1/rules/$RULE/enable"204 on success. A rule starts running on new transactions within about 15 seconds; it does
not go back over transactions that arrived earlier. Disabling stops both the live and the shadow
version; hits and alerts already created stay.
409 rule not found or has no approved active version means the id is wrong, or nothing has
been deployed to the live slot yet (for example a new custom rule). Audit: rule.enable or
rule.disable.
Scheduled rules
Section titled “Scheduled rules”A scheduled rule’s versions store "expression": "scenario:<key>"
(for example scenario:fan_in_fan_out) and no features: the query is built into SpeculaPro. Draft
new versions with POST /api/v1/rules/{id}/versions as for any rule,
changing only params, severity, score, action and explanation. Parameters are checked
against the query’s hard limits, for example params.window_hours must be at most 720, and a
computed placeholder in the explanation is refused. You can’t create a scheduled rule.
GET /api/v1/rules/{id}/runs
Section titled “GET /api/v1/rules/{id}/runs”| Query parameter | Meaning |
|---|---|
limit |
How many runs, newest first: 1 to 200, default 30 |
curl -s -b ~/.sp-cookies "$SP/api/v1/rules/0192f1c4-…/runs?limit=2"{ "schedule": "daily 02:00", "timezone": "Africa/Accra", "enabled": true, "next_run_at": "2026-10-10T02:00:00Z", "runs": [ { "id": "0193b2d0-…", "version_id": "0192f1c4-…", "version": 1, "shadow": false, "scenario": "fan_in_fan_out", "period_start": "2026-10-08T02:00:00Z", "period_end": "2026-10-09T02:00:00Z", "data_from": "2026-10-06T02:00:00Z", "catch_up": false, "truncated": false, "status": "succeeded", "attempts": 1, "started_at": "2026-10-09T02:00:41Z", "finished_at": "2026-10-09T02:00:42Z", "duration_ms": 812, "candidates": 3, "hits": 2, "suppressed": 0 } ], "backoff": []}| Field | Meaning |
|---|---|
next_run_at |
The next slot, while the rule is running; absent when it is stopped |
period_end |
The slot this run was for |
period_start |
Where the previous successful run ended (or one interval before, for the first) |
data_from |
The earliest transaction time the run read |
catch_up |
true when the run covered several missed slots |
truncated |
true when the missed span was longer than 31 days and was cut |
status |
running, succeeded or failed, with error when it failed |
attempts |
How many times this window has been tried |
candidates |
Subjects the query matched |
hits |
Hits written, after leaving out subjects flagged within the last rule window. suppressed counts those a suppression swallowed |
backoff |
Versions whose runs keep failing and are waiting before the next try: version_id, failures (consecutive failed runs), retry_at and last_error. Empty while all is well. See Scheduled rules: missed runs |
A rule that isn’t scheduled, or isn’t in your workspace, returns 404 no scheduled rule with this id.
PUT /api/v1/rules/{id}/schedule
Section titled “PUT /api/v1/rules/{id}/schedule”curl -s -b ~/.sp-cookies -X PUT -H 'Content-Type: application/json' \ -d '{"schedule": "weekly mon 03:30"}' "$SP/api/v1/rules/0192f1c4-…/schedule"Returns 204. schedule is hourly, hourly :MM, daily HH:MM or weekly DAY HH:MM (DAY is
mon to sun), in your workspace’s time zone. The change takes effect at the next slot, and the
next run still covers everything since the last one. It isn’t a new version and needs no second
approval.
| Status | detail |
|---|---|
422 |
rule is not valid, with schedule "…": use "hourly", "hourly :MM", "daily HH:MM" or "weekly DAY HH:MM" (DAY = mon … sun) |
404 |
not found: no scheduled rule with this id |
Audit: rule.schedule, with the schedule before and after.
Custom rules
Section titled “Custom rules”POST /api/v1/rules/validate
Section titled “POST /api/v1/rules/validate”Checks a definition (expression, features, params, severity, score, action,
explanation) without saving anything. It always answers 200:
{ "valid": false, "problems": ["features.out_7d is used but not declared"] }It checks that the definition compiles, not that it catches what you want, and it doesn’t check the action against a mode. To try it on an example, use test; see Writing a custom rule.
POST /api/v1/rules
Section titled “POST /api/v1/rules”Creates a custom rule as draft version 1. Its key is stored with a custom. prefix.
| Field | Required | Meaning |
|---|---|---|
key |
Yes | 3 to 64 characters: lowercase letters, digits, _, . and - |
name |
Yes | 3 to 120 characters |
category |
Yes | 2 to 40 characters, for example cash or structuring. The Rules screen groups rules by it |
entity_scope |
Yes | customer, account, counterparty or agent: who its alerts are about. agent is the agent on the other side of the customer’s cash-in or cash-out, that is the transaction’s counterparty; when the counterparty isn’t known, the alert is about the customer |
mode |
No | post_event (default) or realtime. It can’t be changed later, and the definition’s action must fit it |
definition |
Yes | The same fields as validate, plus an optional builder |
params_schema |
No | A description per parameter (type, title, minimum, maximum), so people can tune values on screen |
description, typology |
No | Shown on the rule’s page |
change_note |
No | Up to 500 characters |
Response: 201 {"id": "<rule id>"}. A key that already exists returns 409
a rule with key custom.… already exists. Then submit, approve and deploy version 1 like any other
version. The full walkthrough is in Writing a custom rule.
Audit: rule.create.
Related
Section titled “Related”- Managing rules, for the Rules screens
- Approving a rule change
- Rules and versions
- Authentication
- Audit log