Decisions API
Ce contenu n’est pas encore disponible dans votre langue.
The Decisions API has two sides:
- Your systems ask for decisions and send feedback, with an API key.
- People read the decision log, statistics and per-rule outcomes, and label decisions, while signed in. These are the endpoints behind the Decisions page.
For a step-by-step integration guide, read Calling the decision API first.
Endpoints at a glance
Section titled “Endpoints at a glance”| Endpoint | Does | Who can call it | Success |
|---|---|---|---|
POST /api/v1/decisions |
Decide on one transaction | API key with decide |
200 |
POST /api/v1/feedback |
Send what happened to transactions | API key with decide or ingest:write |
200 |
GET /api/v1/decisions |
The decision log | Signed in, alerts.view |
200 |
GET /api/v1/decisions/{id} |
One decision in full | Signed in, alerts.view |
200 |
POST /api/v1/decisions/{id}/label |
Label a decision fraud or legitimate | Signed in, cases.work |
204 |
GET /api/v1/decisions/stats |
Volume, outcomes and latency | Signed in, alerts.view |
200 |
GET /api/v1/decisions/rule-outcomes |
How each real-time rule’s hits were labelled | Signed in, rules.view |
200 |
Decide on a transaction
Section titled “Decide on a transaction”POST /api/v1/decisions
Section titled “POST /api/v1/decisions”The body is one transaction, with the same fields and rules as an item of
POST /api/v1/ingest/transactions. The transaction is
recorded for monitoring at the same time, so don’t also send it to the ingest API. It is stored
with the status you send (default completed), except that a block answer stores it as
blocked.
curl -s "$SP/api/v1/decisions" \ -H "Authorization: Bearer $SPECULAPRO_DECIDE_KEY" \ -H 'Content-Type: application/json' \ -d '{ "external_id": "CARD-AUTH-88213409", "occurred_at": "2026-10-09T14:31:07Z", "customer_external_id": "CUST-000412", "direction": "out", "type": "payment", "channel": "card", "status": "pending", "amount_minor": 45000, "currency": "GHS", "mcc": "5732", "device_fingerprint": "a8f3c2d9-6b1e-4f7a-9c2d-3e5f7a9b1c4d" }'Response (200):
| Field | Type | Meaning |
|---|---|---|
decision_id |
UUID | This decision |
transaction_id |
UUID | SpeculaPro’s ID for the transaction, derived from your external_id |
external_id |
string | Yours, echoed |
outcome |
string | allow, step_up, review or block |
score |
number | Combined score of the rules that fired, 0 to 100 |
reasons |
array | Live rules that fired, strongest action first: rule (key), name, action (block, review, step_up, allow, tag or score_only), severity, score, explanation |
degraded |
boolean | The failure policy answered because the checks couldn’t finish in time |
note |
string, optional | Why the outcome isn’t simply the strongest rule action |
latency_ms |
number | Server-side time to decide |
decided_at |
timestamp | When |
replayed |
boolean | This is the stored answer to an earlier request with the same external_id |
How the outcome is combined from the rules, and the score thresholds, are explained in Real-time decisions.
Errors:
| Status | detail |
Meaning |
|---|---|---|
401 |
api key required |
No API key, or a browser session |
403 |
api key lacks scope decide |
The key doesn’t have the decide scope |
409 |
conflict: external_id was already used for a different transaction |
Same external_id, different body, within the idempotency window |
409 |
conflict: external_id was already used for a transaction sent to the ingest API |
The ID was already sent through the ingest API |
409 |
conflict: a request with this external_id is still being decided; retry |
Another request with the same external_id is being decided right now. Retry shortly: you get its answer. See Two requests at once |
422 |
invalid transaction |
errors lists each problem, such as occurred_at is in the future |
503 |
decision service unavailable; apply your fallback and retry |
The transaction couldn’t be recorded at all, for example because exchange rates couldn’t be looked up |
A slow or failing dependency doesn’t produce an error: the failure policy answers and degraded
is true. See When SpeculaPro is degraded.
Send feedback
Section titled “Send feedback”POST /api/v1/feedback
Section titled “POST /api/v1/feedback”Final statuses and fraud labels for up to 500 transactions, identified by external_id. Works
for transactions sent through either API. Fields you leave out keep their earlier value.
| Field | Required | Values | Meaning |
|---|---|---|---|
items[].external_id |
Yes | 1 to 128 characters | The transaction |
items[].status |
At least one of status, label, step_up_passed, note |
completed, failed, reversed, chargeback, cancelled |
Its final status |
items[].label |
See status |
fraud, legitimate, unknown |
Whether it was fraud |
items[].step_up_passed |
See status |
true, false |
After a step_up: did the customer pass? |
items[].note |
See status |
Up to 1,000 characters | Anything else |
items[].fraud_type |
No | account_takeover, card_testing, social_engineering, sim_swap, identity_theft, first_party, mule, merchant, other |
Only with label: "fraud" |
items[].reported_at |
No | Timestamp | When you learned it. Default: now |
curl -s "$SP/api/v1/feedback" \ -H "Authorization: Bearer $SPECULAPRO_DECIDE_KEY" \ -H 'Content-Type: application/json' \ -d '{"items":[{"external_id":"CARD-AUTH-88213409","status":"chargeback","label":"fraud","fraud_type":"card_testing"}]}'Response (200): results, one per item in order, each with external_id, status and, when
rejected, errors.
status |
Meaning |
|---|---|
recorded |
Stored |
not_found |
No transaction with that external_id in your workspace |
rejected |
errors says why: send at least one of status, label, step_up_passed or note, or fraud_type needs label fraud |
Every item that changes something is written to the audit log as
decision.feedback, with the feedback before and after and the API key as the actor. An item
that changes nothing writes no entry.
A key with neither scope gets 403 api key lacks scope ingest:write. More on how feedback is used:
Feedback and fraud labels.
The decision log
Section titled “The decision log”GET /api/v1/decisions
Section titled “GET /api/v1/decisions”Newest first.
| Query parameter | Meaning | Default |
|---|---|---|
outcome |
allow, review, block or step_up |
All |
degraded |
true: only decisions answered by the failure policy |
false |
labelled |
true: only decisions with a fraud label |
false |
q |
An exact transaction external_id, or a customer’s external_id if your role has entities.view (without it, customer IDs never match) |
— |
days |
How far back, 1 to 90 | 7 |
limit |
1 to 200 | 100 |
before, before_id |
Paging: the decided_at and id of the last row of the previous page |
— |
Each row:
| Field | Meaning |
|---|---|
id, decided_at, external_id |
The decision, when, and the transaction’s ID in your system |
customer_external_id, customer_id |
The customer. Left out if your role lacks entities.view |
amount_base_minor |
In your base currency’s minor units |
channel, outcome, score, degraded, latency_ms |
As in the decision |
rules |
Keys of the live rules that fired |
label |
fraud, legitimate or unknown, once reported |
How to page through the log:
- Request the first page:
GET /api/v1/decisions?days=30&limit=200. - If you got
limitrows, take the last row’sdecided_atandid. - Request the next page with
before=<decided_at>&before_id=<id>and the same filters. - Stop when a page has fewer rows than
limit.
A before_id that isn’t a UUID gets 422 before_id must be a UUID.
curl -s -b ~/.sp-cookies "$SP/api/v1/decisions?outcome=block&days=30"GET /api/v1/decisions/{id}
Section titled “GET /api/v1/decisions/{id}”Everything in a log row, plus:
| Field | Meaning |
|---|---|
transaction_id, transaction_at |
The transaction and when it happened |
customer_name |
Left out without entities.view |
note |
The policy note, if any |
reasons |
The live rules that fired, each with rule_id, rule_key, rule_version_id, name, entity_scope, action, severity, score, explanation, and the features and params it used |
shadow |
Shadow rules that would have fired, in the same form. They didn’t affect the outcome |
features |
Every feature value read, by signature entity|aggregate|direction|type|channel|window |
rule_versions |
Every real-time rule version evaluated |
model |
With a live risk model: its score for the transaction when the decision was made, as model_id, version, score (0 to 1), percentile (0 to 100) and reasons (up to three, each with feature, label, value and contribution). Absent without a live model and on a degraded decision. The answer to POST /api/v1/decisions doesn’t carry it. See Risk models API |
transaction |
direction, type, status, amount_minor, currency, country, counterparty, and the first 12 characters of the device’s keyed fingerprint as device_id. Absent for a few seconds, until the transaction is stored. counterparty is left out without entities.view |
feedback |
status, label, fraud_type, step_up_passed, note, source (api or analyst), set_by (the person’s name, for an analyst label) and updated_at |
alert_id |
The fraud alert the decision’s hits joined, if any |
404 not found if there is no such decision in your workspace.
Label a decision
Section titled “Label a decision”POST /api/v1/decisions/{id}/label
Section titled “POST /api/v1/decisions/{id}/label”| Field | Required | Values |
|---|---|---|
label |
Yes | fraud, legitimate, unknown |
fraud_type |
No | As for feedback; only with fraud |
note |
No | Up to 1,000 characters |
Recorded with source: "analyst" and your name, replacing any earlier label, and written to the
audit log as decision.feedback with the feedback before and after. Answers 204.
curl -s -b ~/.sp-cookies -H 'Content-Type: application/json' \ -d '{"label":"legitimate","note":"Customer confirmed by phone; new handset"}' \ "$SP/api/v1/decisions/01a1208b-b57c-7a8f-a1a2-0f95ed78d63c/label"Statistics
Section titled “Statistics”GET /api/v1/decisions/stats
Section titled “GET /api/v1/decisions/stats”| Query parameter | Meaning | Default |
|---|---|---|
hours |
1 to 24 | 24 |
| Field | Meaning |
|---|---|
total, by_outcome, degraded |
Counts over the period |
p50_ms, p95_ms, p99_ms |
Server-side latency percentiles. Each is the upper bound of a latency bucket (1, 2, 5, 7.5, 10, 15, 20, 30, 50, 75, 100, 150, 200, 300, 500, 1000 ms); slower than 1 second reads 2000 |
series |
One point per 5 minutes, oldest first: at, allow, step_up, review, block, degraded |
These figures are kept for about a day. For long-term trends, use the Prometheus metrics; see Monitoring.
Per-rule outcomes
Section titled “Per-rule outcomes”GET /api/v1/decisions/rule-outcomes
Section titled “GET /api/v1/decisions/rule-outcomes”| Query parameter | Meaning | Default |
|---|---|---|
days |
1 to 90 | 30 |
One row per real-time rule that fired live in that period, most hits first: rule_key, hits,
labelled_fraud and labelled_legitimate. Precision is
labelled_fraud / (labelled_fraud + labelled_legitimate).