Decide
Render a synchronous fraud verdict for a subject + action + resource.
Render a synchronous risk verdict. Always returns within the engine's p99 budget; the durable audit write is dispatched asynchronously so it never blocks the response.
Endpoint
POST /api/v1/fraud/decide
Scope: fraud:decide
Plan gate: Detection Pack addon (Pro or Enterprise)
Rate limit: 10,000 requests / hour per API key
Request body
| Field | Type | Required | Description |
|---|---|---|---|
subject.id | string | Yes | The actor performing the action (e.g. user id). |
subject.kind | string | Yes | Tenant-defined kind, e.g. user, merchant, device. |
subject.attributes | object | No | Freeform attributes evaluated by ABAC rules in Phase 6B. |
action | string | Yes | The action being attempted, e.g. transfer, withdraw, login. |
resource.id | string | No | The resource the action targets, e.g. transaction id. |
resource.kind | string | Yes | The resource type, e.g. transaction, withdrawal, account. |
resource.attributes | object | No | Freeform resource attributes (amount, recipient, etc.). |
context.ip | string | No | Client IP. Redacted before persistence — present in audit trail as [REDACTED]. |
context.deviceFingerprint | string | No | SHA-256 hash of a tenant-computed device fingerprint. |
context.userAgent | string | No | Client user-agent. Redacted before persistence. |
context.geoHint | string | No | Tenant-supplied geo hint (country code). |
context.amount | { value: number, currency: string } | No | Transactional amount, evaluated by amount-based rules. |
context.externalScore | { value: number, source: string } | No | Optional behavioural-ML score the tenant ran upstream. 0–100. |
idempotencyKey | string | No | If present and a recent decision exists for the same key on the same org, the cached verdict is returned. |
Response
{
"success": true,
"data": {
"decisionId": "dec_2j7yhq8wcs9...",
"verdict": "allow",
"score": 0,
"reasons": [
{
"kind": "default",
"code": "phase_6a_default_allow",
"weight": 0,
"detail": "No rules evaluated — Phase 6B wires the rule engine. Default allow."
}
],
"ttlSeconds": 30,
"notice": "This verdict is informational and is provided for risk assessment. The tenant remains the decision authority for any action taken. See https://docs.platformxe.com/legal/fraud-detection-terms for full terms.",
"decidedAt": "2026-05-03T09:14:21.412Z",
"shadow": false,
"latencyMs": 4
}
}
| Field | Type | Description |
|---|---|---|
decisionId | string | Unique id for this verdict; surface it to your reviewers and support tooling. |
verdict | "allow" | "review" | "step_up" | "block" | The recommended outcome. The tenant remains the decision authority. |
score | number (0–100) | Aggregated risk score. Always 0 in Phase 6A; rule-driven in Phase 6B. |
reasons | Array<{ kind, code, weight, detail }> | Structured chain explaining the verdict. Safe to surface to ops/support. No PII. |
ttlSeconds | number | Idempotency window — replaying the same idempotencyKey within this window returns the cached verdict. |
notice | string | Mandatory liability disclaimer surfaced on every response. |
decidedAt | string (ISO-8601) | When the engine rendered the verdict. |
shadow | boolean | true only for /shadow-decide responses. |
latencyMs | number | Engine latency budget for this call (excludes network). |
Verdict semantics
| Verdict | Meaning | Recommended caller behaviour |
|---|---|---|
allow | No risk signals triggered. | Proceed with the action. |
review | Suspicion score above the review threshold but below step-up. | Queue for human review or run a soft challenge. |
step_up | Stronger signal — escalate authentication. | Trigger an OTP / passwordless / MFA challenge before proceeding. |
block | High-confidence risk or hard rule (sanctions hit). | Reject the action and (optionally) open a case. |
Error responses
| HTTP | Code | Cause |
|---|---|---|
| 400 | BAD_REQUEST | Missing required fields or malformed body. |
| 401 | UNAUTHORIZED | Missing or invalid API key. |
| 402 | DETECTION_PACK_REQUIRED | The org has not enabled the Detection Pack addon. |
| 403 | FORBIDDEN | API key has no organization context, or wrong scope. |
| 429 | RATE_LIMITED | 10,000/hr ceiling exceeded. |
| 500 | INTERNAL_ERROR | Unexpected engine failure. |
Examples
curl
curl -X POST https://api.platformxe.com/api/v1/fraud/decide \
-H "Content-Type: application/json" \
-H "x-api-key: pxk_live_your_api_key_here" \
-H "x-idempotency-key: txn_8a91" \
-d '{
"subject": { "id": "usr_001", "kind": "user" },
"action": "transfer",
"resource": { "id": "txn_8a91", "kind": "transaction" },
"context": {
"amount": { "value": 50000, "currency": "NGN" }
}
}'
TypeScript SDK
import { PlatformXe } from '@caldera/platformxe-sdk';
const px = new PlatformXe({ apiKey: process.env.PLATFORMX_API_KEY! });
const verdict = await px.fraud.decide({
subject: { id: 'usr_001', kind: 'user' },
action: 'transfer',
resource: { id: 'txn_8a91', kind: 'transaction' },
context: { amount: { value: 50_000, currency: 'NGN' } },
idempotencyKey: 'txn_8a91',
});
if (verdict.data.verdict === 'block') {
// Tenant decides what blocking means in their system
console.warn('blocked:', verdict.data.decisionId, verdict.data.reasons);
}
Performance
| Percentile | Target |
|---|---|
| p50 | 15ms |
| p95 | 60ms |
| p99 | 120ms |
The hot path reads from Upstash Redis only. Durable persistence to fraud_decisions is fanned out via Inngest after the response is returned.