Skip to main content
The rule engine evaluates the rules in your active ruleset against the decision payload and produces the outcome. Rules run in the order they appear in the ruleset; a BLOCK short-circuits evaluation, while REVIEW outcomes accumulate. If no rule matches, the decision is ALLOW. Rules address the payload through field paths on the ruleset’s bound schema — the built-in transaction schema unless the ruleset binds a custom one. The examples on this page use transaction paths; the grammar is the same over any schema.

Rule types

Each condition and blacklist rule carries an action (REVIEW or BLOCK); backend rules derive theirs from the backend’s response. Every rule has an enabled flag. Rules are evaluated in the order they appear in the ruleset’s rules array.
The exact ruleset JSON schema is documented in the API reference. This page describes the concepts; the schema is the source of truth for field names.

Conditions

A condition rule evaluates a boolean expression tree over the decision payload, addressed with JSONPath ($.transaction.amount, $.metadata.channel, …). Expressions compose:
  • Comparisoneq, neq, lt, lte, gt, gte; either side may be a field reference or a literal, so field-to-field comparison works
  • Set membership — value in a list: a literal set, or an array-typed field such as $.credential.pan.schemes
  • Boolean compositionand, or, not
  • Velocity — a rolling-window count (see below)
Every field the bound schema declares is addressable, including the PayPal credential fields ($.credential.paypal.email, payer_id, address_status, protection_eligibility, payer_status):
The PayPal fields are optional and become available at different stages of the PayPal flow, so predicate only on ones your integration actually sends. protection_eligibility in particular is returned by PayPal only after authorization, so a rule using it will not match a pre-authorization decision — it resolves to an absent path, and the predicate abstains: it never fires and never raises.
A path the bound schema does not declare rejects when you save the ruleset (422 naming the path). A declared path that a payload does not carry makes the predicate abstain: the rule never fires on that decision, and the abstention absorbs through and / or / not — a composite with any absent operand abstains as a whole, and not over an absent path abstains rather than firing. JSON null counts as absent; an empty string or array counts as present. Simulation shows which rules abstain and why.

Field references and filters

A field reference is a JSONPath starting with $., optionally followed by a chain of filters that transform the value before it is compared:
Filters compose left to right and are type-checked against the bound schema when you save the ruleset: domain() on a field that is not declared as email rejects with 422. A filter that receives an absent or malformed value emits absent, and the predicate abstains. A filtered value inherits the strictest classification of its inputs. In a comparison the filtered reference goes in expr; a bare path goes in path:
Velocity fields, blacklist rule fields, and HMAC inputs take the pipe string directly.

Fingerprints

hmac composes one or more field references into a single deterministic fingerprint — an HMAC-SHA256 of the ordered inputs under a secret dedicated to your instance — so you can match an identity without storing or comparing the raw values:
  • Inputs are filtered field references or string literals ({"type": "const", "value": "…"}); at least one must be a field reference, and there are at most 16.
  • The fingerprint reads hmc_ followed by 64 hex characters. The simulation trace and decision evidence render the bare hex — prefix it with hmc_ when you copy it into a rule.
  • Identical inputs and filters always produce the same fingerprint; the operator applies no normalization of its own, so express case-folding and trimming per input.
  • An absent input makes the whole composition absent, and the predicate abstains.
  • pci-sad fields cannot be inputs. A fingerprint is any-scoped, so it can be compared against a literal even when one of its inputs could not be.

Regulated fields and literals

The classification markers gate what a rule may do with a field:
  • A pci-pan or pci-sad field cannot be compared against a literal — the literal would persist in the rule. Compare it against another field reference, or hash it with hmac and compare the fingerprint. Card matching normally uses $.credential_fingerprint.
  • A pci-pan or pci-sad field cannot be a velocity field or a blacklist field.
  • Rules referencing pci-pan or pci-sad fields submit only on instances at PCI level SAQ_D or ROC — there, hmac over the field is the way to pin a rule to a specific card.
  • Array- and object-typed fields are usable in one position: the right side of in / not_in. Anywhere a single value is expected they reject with 422.
  • pii fields are unrestricted.

Velocity

Velocity conditions count how many decisions matched a given identity within a rolling time window — for example, “more than 5 attempts on this card in the last hour”. Counters are maintained atomically and fail open: if the counter store is unavailable, the velocity check never blocks a transaction.
Any scalar field the bound schema declares can be counted, optionally through a filter chain ($.customer.email | downcase()), except pci-pan and pci-sad fields — a counter key persists the counted value. The identities most rulesets count on the transaction schema: Counters are keyed by field path and value, not by rule or ruleset: two rules, or two rulesets, counting $.device.ip read the same counter. When a custom schema publishes a new version, carry_over decides whether counters continue across the version boundary — see Schemas.

Backend rules

A backend rule delegates the decision to a backend exposed by a Link integration. The rule names the integration slug and the backend_id to invoke; Specter calls Link to assess the transaction and maps the outcome to a Specter decision. Before any call, Specter checks that the payload contains the backend’s required fields. If any are missing, it skips the backend with an insufficient_context result and makes no call. A backend or backend-group rule may carry a condition of its own as a guard: the backend is called only when the condition holds. When it is false or abstains, the call is skipped with a guard_not_met result and evaluation continues — useful to reserve a paid risk check for high-value payments. Each backend and backend-group rule declares an on_error setting — it is required, never defaulted — that governs what happens when the backend errors or times out:

Simulation

Before you activate a ruleset, run it against a test payload:
The response is an evaluation trace: one entry per rule, in evaluation order, with its outcome — fired, not_fired, abstained (naming the absent field or absorbing operand), not_simulated for a backend rule, not_evaluated for a rule never reached — and the derivations behind it: resolved values (redacted per classification), each filter step’s output, the HMAC fingerprint, the velocity counter and its current reading. The run is inert: no decision record, counter increment, blacklist entry, webhook, or backend call. Counters are read live, so two runs may differ. Backend rules are never invoked, and because a backend could change everything downstream, a ruleset that reaches a live, synchronous backend rule simulates to no verdictverdict_suppressed names the rule that stopped it. An async backend rule suppresses only a verdict that would otherwise be ALLOW; shadow rules (live: false) never suppress. Rulesets without backend rules simulate to a full verdict. The endpoint works on a ruleset in any lifecycle state and needs the dedicated admin:rulesets:simulate scope. See the API reference for the trace contract. After the fact, the same trace shape answers “what made this rule fire” for a recorded decision — see decision evidence.

Evaluation order

Rulesets

Version and activate collections of rules.

Backends & connections

Connect external risk engines.

Schemas

The field paths, types, and markers rules are written against.