Aller au contenu

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.

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

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.

Terminal window
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.

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
Terminal window
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.

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:

  1. Request the first page: GET /api/v1/decisions?days=30&limit=200.
  2. If you got limit rows, take the last row’s decided_at and id.
  3. Request the next page with before=<decided_at>&before_id=<id> and the same filters.
  4. 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.

Terminal window
curl -s -b ~/.sp-cookies "$SP/api/v1/decisions?outcome=block&days=30"

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.

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.

Terminal window
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"
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.

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).