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:
- Comparison —
eq,neq,lt,lte,gt,gte; either side may be a field reference or a literal, so field-to-field comparison works - Set membership — value
ina list: a literal set, or an array-typed field such as$.credential.pan.schemes - Boolean composition —
and,or,not - Velocity — a rolling-window count (see below)
$.credential.paypal.email, payer_id, address_status, protection_eligibility, payer_status):
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.
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:
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 withhmc_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-sadfields cannot be inputs. A fingerprint isany-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-panorpci-sadfield cannot be compared against a literal — the literal would persist in the rule. Compare it against another field reference, or hash it withhmacand compare the fingerprint. Card matching normally uses$.credential_fingerprint. - A
pci-panorpci-sadfield cannot be a velocity field or a blacklist field. - Rules referencing
pci-panorpci-sadfields submit only on instances at PCI levelSAQ_DorROC— there,hmacover 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 with422. piifields 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.$.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 integrationslug 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: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 verdict — verdict_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
Related
Rulesets
Version and activate collections of rules.
Backends & connections
Connect external risk engines.
Schemas
The field paths, types, and markers rules are written against.