PlatformXeDocs
Get API Key

Rules

Author tenant-scoped fraud rules with the ABAC-shape DSL and roll them out via the draft → shadow → published flow.

Tenant-defined rules drive the verdict for every /v1/fraud/decide call. Rules are authored as a small JSON DSL, validated with shadow-decide and shadow reports, and only then promoted to production.

Lifecycle

draft  → shadow → published → archived (terminal)
   \      \
    \      → draft (revert)
     → archived (cancel)
StatusEffect on production verdictEffect on shadow-decide
draftNone — rule is parked.None.
shadowNone. Outcome recorded in ruleVersionsEvaluated for the shadow report.Counts toward the would-be verdict.
publishedTriggered weight contributes to score; verdict bands apply; verdict overrides apply.Counts.
archivedIgnored.Ignored.

Rules transition through POST /api/v1/fraud/rules/:id/transition.

Endpoints

MethodPathDescription
GET/api/v1/fraud/rulesList rules (filter by ?status=)
POST/api/v1/fraud/rulesCreate a rule (always status=draft)
GET/api/v1/fraud/rules/:idFetch one rule
PATCH/api/v1/fraud/rules/:idUpdate a draft or shadow rule
DELETE/api/v1/fraud/rules/:idSoft-archive (transition to archived)
POST/api/v1/fraud/rules/:id/transitionState-machine transition
GET/api/v1/fraud/rules/:id/shadow-reportTrigger-rate metrics

Scope: fraud:manage (CRUD + transitions + report) Plan gate: Detection Pack addon Rate limit: 500 requests / hour per API key

PATCH on a published or archived rule returns 409 CONFLICT. To change a published rule, archive it and create a new draft.

Authoring a rule

Rules are JSON. The condition AST mirrors the Authorization Engine's ABAC operators (13 operators + all / any / not), with two fraud-specific extensions:

  • Dot-path lookups for nested fields: subject.kycTier, amount.value, context.deviceFingerprint.
  • $count.<windowName> references that resolve from the velocity counter snapshot computed before the rule fires.

Minimal example

{
  "name": "high-value-transfer",
  "weight": 30,
  "appliesTo": { "actions": ["transfer"], "resourceKinds": ["transaction"] },
  "condition": {
    "amount.value": { "gt": 100000 }
  }
}

Velocity rule with windows

{
  "name": "velocity-failed-logins",
  "weight": 50,
  "appliesTo": { "actions": ["login"] },
  "windows": [
    {
      "name": "failedLogins24h",
      "aggregation": "count",
      "duration": "P1D",
      "bucketBy": "subject.id"
    }
  ],
  "condition": {
    "$count.failedLogins24h": { "gt": 5 }
  }
}

Hard verdict override

A rule can bypass score-band logic with verdictOverride. The most-severe override across triggered rules wins (severity: block > step_up > review > allow).

{
  "name": "sanctions-hit",
  "weight": 100,
  "verdictOverride": "block",
  "appliesTo": { "actions": ["*"] },
  "condition": {
    "context.sanctionsListId": { "exists": true }
  }
}

Combinators (all / any / not)

{
  "name": "high-risk-transfer",
  "weight": 60,
  "appliesTo": { "actions": ["transfer"] },
  "condition": {
    "all": [
      { "context.geoHint": { "in": ["NG", "GH", "KE"] } },
      {
        "any": [
          { "$count.failedLogins24h": { "gt": 5 } },
          { "$count.distinctRecipients24h": { "gt": 10 } }
        ]
      },
      { "not": { "subject.attributes.kycTier": { "gte": 3 } } }
    ]
  }
}

Operators

OperatorNotes
equals / notEqualsStrict equality.
gt / gte / lt / lteBoth operands must be numeric; otherwise the predicate is false.
in / notInexpected must be an array.
contains / startsWith / endsWithString-only; non-string operand → false.
existsexpected: true matches if path is non-null/undefined; false is the inverse.
matchesRegex. Invalid regex never matches and never throws — it's safe to author.

Windows (velocity counters)

Each window adds one Redis sorted-set behind the scenes, keyed by (orgId, ruleId, windowName, bucketValue).

FieldDescription
nameIdentifier referenced from the AST as $count.<name>. Unique within the rule.
aggregationcount, sum, or distinctCount.
fieldRequired for sum / distinctCount. Dot-path within the request context — e.g. context.amount.value for sum, context.recipient for distinctCount.
durationISO-8601 duration. Supported: P1D, PT5M, PT1H, P7D, P1DT12H, etc.
bucketByDot-path resolved against the request context to produce the bucket key (e.g. subject.id).

Rolling windows are pruned at read time via ZREMRANGEBYSCORE. Each event is added to the sorted set with score = now (ms).

Verdict bands

ScoreVerdict
0 – 24allow
25 – 49review
50 – 74step_up
75 – 100block

Score is the sum of triggered-rule weights, capped at 100. Hard verdictOverride values from triggered rules can promote the verdict (never demote it).

Shadow report

GET /api/v1/fraud/rules/:id/shadow-report?from=...&to=...

Aggregates fraud_decisions to surface trigger-rate metrics for a single rule across an arbitrary window. Default window is the last 7 days.

{
  "success": true,
  "data": {
    "ruleId": "frl_2j7yhq8wcs9",
    "ruleName": "velocity-failed-logins",
    "ruleVersion": 1,
    "status": "shadow",
    "windowFrom": "2026-04-26T09:14:00Z",
    "windowTo": "2026-05-03T09:14:00Z",
    "totalDecisions": 12_403,
    "triggeredCount": 187,
    "triggerRate": 0.01508
  }
}

Use the report to decide when to promote a rule from shadow to published:

  • Trigger rate too high → tighten the condition before publishing.
  • Trigger rate near zero → either the rule is solid or the condition never matches; cross-check with shadow-decide.
  • Steady-state on a published rule → drift signal; investigate if the rate suddenly jumps.

Rule-rollout playbook

  1. Author as draft. Iterate freely, validate with /shadow-decide calls.
  2. Promote to shadow. The rule now runs ride-along on every production decide call but never affects verdicts.
  3. Watch the shadow report. Aim for a stable trigger rate that matches your design intent.
  4. Promote to published. The rule's weight starts contributing to verdicts.
  5. Monitor via /decisions audit trail and the shadow report (still works post-publish for drift detection).
  6. Archive when retired. There is no hard-delete path — every historical decision can be reproduced from ruleVersionsEvaluated.

Errors

HTTPCodeCause
400BAD_REQUESTValidation failure (missing field, bad shape, invalid duration).
401UNAUTHORIZEDMissing or invalid API key.
402DETECTION_PACK_REQUIREDDetection Pack addon not enabled.
403FORBIDDENAPI key has no organization context, or wrong scope.
404NOT_FOUNDRule unknown or belongs to a different org.
409CONFLICTName collision, immutable (published/archived) rule, or invalid state transition.
429RATE_LIMITED500/hr ceiling exceeded.
500INTERNAL_ERRORUnexpected service failure.