Improvement
Blacklist Entries Carry Data Classifications
Every blacklist entry now carriesvalue_classification on its API
representation — the x-data-classification the entry’s field_path
resolves to across the registered schemas: any or pii. A path no schema
declares is any (an omitted marker is a positive assertion, and an entry on
such a path is inert, since undeclared keys never enter a decision payload).
The marking is stamped when the entry is written and re-resolved whenever a
schema is registered or published, so it always follows the schemas’ current
declarations. Existing rows are classified automatically; no action is
needed. In the other direction, a schema registration or publication that
would re-classify a currently-blacklisted path as pci-pan or pci-sad is
refused with 422 naming the path.The retention posture is now a documented statement: blacklist entries are a
fraud-prevention record, exempt from the decision-record retention windows.
An expired entry stops matching but its value remains in the table — a TTL
does not bound retention, and deleting the entry is the only erasure. Entry
values remain readable in clear on the Admin API and UI so the list stays
inspectable and a data subject’s entries can be found and deleted on request;
every read of a value classified as personal data is recorded as an audit
event.Improvement
Interceptors Enforce the Built-in-Binding Contract
Interceptors have always mapped onto the built-intransaction schema; that
contract is now enforced instead of degrading silently. Creating or updating
an interceptor rejects a field_mapping target the transaction schema does
not declare with 422 naming the offending field_mapping[i].target —
dynamic metadata.<key> targets remain accepted. Mappings stored before this
gate are not swept; they surface on their next edit.Creating or updating an interceptor whose context currently resolves to a
custom-bound ruleset now rejects with 422 naming the context and the bound
schema — such an interceptor could never execute, so the mistake surfaces at
authoring. A context with no active ruleset yet is accepted, since its
binding is not knowable at that point. If a context is rebound to a custom
schema after the interceptor exists, executing it refuses with 502 INTERCEPTOR_BINDING_UNSUPPORTED, naming the context and the bound schema —
previously every mapped target was silently dropped as undeclared and the
decision evaluated a near-empty payload. Passthrough interceptors (empty
mapping) never touch the binding and are unaffected.The mapping editor is now tier-aware: below SAQ-D, credential.pan.* targets
are withheld from the offered list (the served target schema carries a
withheld flag), the “Full PAN” template is hidden, and pan leaves the
credential-type selector — while stored mappings on withheld targets stay
readable with a badge rather than degrading to unknown strings.Maintenance
Ruleset Request Shape Covers All Four Rule Types
The request body forPOST /api/admin/rulesets and PATCH /api/admin/rulesets/{id}
now documents what Specter has always accepted: type is condition, backend,
backend_group, or blacklist — the specification previously listed only the
first two — and the fields each type carries are on the request shape, not only
on the response: fields, ttl_seconds, and populate_on for blacklist rules;
strategy, members, and merge for backend groups; live for backend rules.
id and name are marked required, as they have always been, and so is the
fail posture: on_error is required on backend and backend-group rules (never
defaulted to allow), and action is required on blacklist rules as on
condition rules. A backend group carries at least 2 members; a split group
requires a weight of at least 1 on every member (ignored for fallback and
parallel); and merge on a non-parallel strategy is rejected, not ignored.Two behaviors are documented for the first time. A backend or backend-group rule
may carry a condition that guards the call: when it is false or abstains,
the backend is skipped with a guard_not_met result and evaluation continues.
And backend_results[].error documents the no-call codes — insufficient_context,
guard_not_met, backend_capability_disabled, backend_disabled, and
not_simulated; invocation failures additionally surface transport-level codes
such as timeout, network, and http_error.Documentation only; no endpoint changed. Regenerate clients to pick up the
request shape.Maintenance
Disabled Async Backend Rules No Longer Affect the Verdict
A backend rule withasync: true and enabled: false no longer turns an
ALLOW into PROVISIONAL. Previously the rule’s mere presence in the ruleset
put the decision on the asynchronous path even though the rule itself never
ran. A disabled rule now has no effect of any kind on the outcome, on the
decision API and in the simulation trace alike.Maintenance
Contract Corrections: Filtered Operands, HMAC Literals, Markers
A documentation-only round that aligns the API reference with the behavior that shipped. No endpoint changed; if you generate clients from our OpenAPI document, regenerate to pick up the corrected shapes.- Filtered comparison operands use
expr. A field reference that carries a filter pipe chain is written{"expr": "$.customer.email | downcase()"}(long form{"type": "filter_expr", "expr": …}).{"path": …}takes a bare path only — a pipe inside it rejects as an undeclared path. Velocity fields, blacklist rule fields, and HMAC inputs keep taking the pipe string directly. - HMAC fingerprints are
hmc_-prefixed. A literal you compare against anhmacoperand must readhmc_followed by the 64-character digest. The simulation trace and decision evidence render the bare digest; prefix it when you copy it into a rule. HMAC inputs may also include string literals ({"type": "const", "value": …}) alongside at least one field reference. - Classification markers completed. Fourteen fields of the built-in
transaction schema that Specter treats as
piiwere shown without a marker:customer.id, thecity/country/postal_code/stateleaves ofbillingandshipping,credential.masked_pan.first_six/last_four,credential.paypal.payer_id, andfrequent_flyer_number/country_codeon airline passengers. On decision reads,customer_idispiias well. - Smaller fixes.
shipping.destination_typesis an array of strings;masked_pan.first_sixis exactly six digits; a line item needsnameorsku; phone examples are E.164;on_error: reviewis accepted when creating a rule; schema versions have no draft state — every publication creates an immutable version,activeuntil deprecated; the schema-registry list endpoints are not paginated (the documentedafter/limitparameters never existed);carry_overgoverns velocity counters only, never blacklist entries; acontextkey in a simulation payload is ignored rather than rejected; the interceptor endpoint documents its422(validation / PCI gate) and502(field-mapping fault) responses.
Improvement
Velocity Counters Honor carry_over
Publishing a schema version with carry_over: false now does what it says:
rulesets bound to the new version count from zero, while counters of earlier
versions are untouched and keep serving the rulesets still bound to them.
With the default carry_over: true, counters for paths present in both
versions continue across the version boundary. Until now the flag was
accepted and stored but had no effect on counting.Blacklist entries are global and not versioned, so they are never affected by
this flag.Improvement
Backend Results Carry the Backend’s Reference
Eachbackend_results entry now surfaces backend_reference as a top-level
field: the reference the backend itself assigned to its evaluation, the value
you quote when correlating a Specter decision with the backend’s own records.Previously the reference was only readable inside raw_response, the backend’s
verbatim body. The dedicated field is stable across backends and stays
addressable even when the raw body is not served. null when the backend
supplied no reference. Additive — existing consumers are unaffected.Improvement
Stored Payload Redaction Follows the Classification Markers
The payload copy stored on an interceptor decision is now redacted by the bound schema’s classification markers — the same markers shown on the API reference — replacing the old name-and-shape guesswork.What this protects better: everything markedpii or pci-* is redacted by
declaration — customer ids, device IP, addresses, passenger details —
whether or not the field’s name suggests it. What becomes visible: unmarked
fields now store verbatim, including short numbers the old heuristic hid on
suspicion alone, such as amounts, counts, and numeric references or metadata
values.Two protections are independent of markers and unchanged in intent: PAN-like
and IBAN-like values are removed wherever they appear — now including
space- or dash-delimited and numerically encoded forms — and a mapping row
flagged sensitive: true always redacts its target.One behavior is deliberately gone: Specter no longer guesses sensitivity
from a source field’s name. A field named cvv or csc is not special —
if a mapped source carries data that must not be stored, set
sensitive: true on that mapping row. That flag is the supported mechanism,
and the only one.Payloads stored before this change keep their original redacted form.Improvement
Regulated Data Ages Out of Decision Records
Decision records now observe a per-scope retention window, configured on your instance as a number of days per classification (pii, pci-pan,
pci-sad). Once a scope’s window has passed, every field of that scope reads
as null on GET /api/decisions/{id} — for the decision record that is the
pii set: customer_id, device_ip, device_fingerprint; no pci-pan or
pci-sad value is ever persisted on the record, so those windows bound stored
payloads only. The key stays and the rest of the record — including the
unmarked masked_credential — is untouched. The boundary is enforced on every
read, not only when the background sweep reaches the row.A scope you do not configure is retained without bound, and any-scoped
fields — amounts, references, the credential fingerprint — are never aged out,
so an old decision stays correlatable.backend_results[].raw_response is the one place no marker reaches: the body
is the backend’s own shape, so it is nulled on the first configured window to
expire. backend_reference is held outside the body and survives it.Improvement
Regulated Fields No Longer Compare Against a Literal
A rule that compares apci-pan- or pci-sad-scoped field against a literal
value now rejects at submission with 422, on every tier.The reason is the literal, not the field. Writing
credential.pan.value == "4111111111111111" or cvc == "123" stores that value
in the rule itself, where it comes back on every read of the ruleset and renders
in the rule editor — the one place regulated card data was still able to come to
rest. Card and authentication data are never persisted, and a rule is no
exception.The field stays addressable. What changes is what may sit on the other side of
the comparison:- compare it against another field reference — that stores no value;
- hash it with
hmacand compare the digest against a literal (thehmc_-prefixed fingerprint) — a digest is not regulated data; - for card matching specifically, use the credential fingerprint.
pii fields still take a literal, and velocity thresholds
and windows are numbers rather than compared values, so counters are unaffected.
Rules already stored keep evaluating; the check runs when you next save the
ruleset, so an existing rule of this shape surfaces the first time you edit it.Maintenance
backend_options Removed From the Decision Request
backend_options is gone from the decision request. The field was specified as
a passthrough for provider-specific values, but nothing ever read it: no
backend received it and no rule could reference it, so anything you sent in it
was accepted and discarded.Sending it remains harmless. Unknown fields on the decision request are
ignored, so existing integrations keep working untouched and no request starts
failing because of this change. Only the specification changes: if you generate
clients from our OpenAPI document, the field disappears on the next
regeneration.Provider-specific data belongs in a named field instead. If you were relying on
backend_options to reach a provider, talk to us and we will model the field
properly.Improvement
List and Object Fields Belong on One Side of in
An array- or object-typed field is now addressable in a single operand position:
the right side of in / not_in, the set that membership is tested against.
"visa" in credential.pan.schemes is the shape, and it is unchanged.Everywhere else the field stands for one value, which a list or an object is not
— so the submission rejects with 422 naming the declared type. That covers a
scalar comparison (already the case), and now also the left side of
in / not_in, a velocity field, and a blacklist rule field. Each of
those could only ever produce “no opinion” at decision time, so a rule written
that way never did anything.Blacklist entries follow the same rule. POST /api/admin/blacklist now
rejects an array- or object-typed field_path with 422: an entry matches
exactly on the pair of field_path and value against one resolved value, so a
list can never equal the stored string. Such an entry was stored and shown but
could never block anything. Entries carry no schema binding, so the check is
conservative across your whole registry — a path that any registered document
and version types as a list or an object cannot key an entry, which means every
container path of any registered document is unkeyable.The rule editor and the entry picker already offered these fields in the
positions that accept them, so anything built in the UI is unaffected. Rules and
entries already stored keep working as before; the check runs when you next save
the ruleset or create an entry.Maintenance
scheme Removed From Card Credentials
The singular credential.pan.scheme / credential.masked_pan.scheme
string is gone; use the schemes array, which carries the same fact and
covers co-branded cards. Requests that still send scheme are not
rejected — the field is dropped like any other undeclared key — but a rule
referencing $.credential.pan.scheme no longer submits. Rewrite such
rules as membership over the array: "visa" in $.credential.pan.schemes.
A rule already saved against the old path keeps evaluating and abstains,
because the path now resolves absent (tri-state semantics); re-submitting
that ruleset rejects until the reference is rewritten.Improvement
Classification Markers on the Decision Payload
The API reference now shows a classification marker on every field of the built-in decision payload that carries regulated data.credential.pan.value is
marked pci-pan; the personal data you send for evaluation is marked pii —
customer names, email, date of birth, billing and shipping addresses, phone
numbers, device IP, and the passenger details on an airline booking.Fields without a marker hold none of those categories. masked_credential is
the case worth knowing: 411111 •••••• 1111 identifies nobody on its own, so it
carries no marker even though it looks card-like. The
full legend
is on the API reference.Nothing about any endpoint changed — this is documentation only.Maintenance
Credential Modelled as a Discriminated Union
credential on the decision request is now specified as a discriminated
union on type, with one variant per credential kind — pan, masked_pan,
sepa, and paypal. The request shape is unchanged: the same type value
paired with the same sibling object is what you have always sent. What
changes is that the pairing is now expressed in the specification rather than
only in prose, so the reference documentation shows one tab per credential
kind instead of four always-optional objects, and generated clients get a
proper tagged union.The specification now rejects combinations that were never valid but that the
previous shape technically allowed — most notably a type with no matching
credential object. If you generate clients or validate against our OpenAPI
document, regenerate against the current version.Improvement
Rulesets Declare Their Schema
The schema binding announced on August 7 is now enforced end to end.schemais required when you create a ruleset.POST /api/admin/rulesetswithout it rejects with422; send{"schema": {"name": "transaction"}}for the built-in payload, or{"name": …, "version": N}for a custom schema. Rulesets created before this change were backfilled with thetransactionbinding and keep working.- Rule paths validate against the bound schema. A field reference the
bound schema does not declare rejects at submission with
422naming the path — a typo surfaces when you save, not as a silent abstention at decision time.$.metadata.*stays free-form. Existing rulesets are checked the next time you save them. - Velocity fields follow the schema. Any declared scalar field can be
counted, not only the four launch paths;
pci-pan- andpci-sad-scoped fields are refused, because a counter key persists the counted value. - Decisions carry their schema. Decision responses and
GET /api/decisions/{id}includeschema— the(name, version)the decision was evaluated against. Additive; existing consumers are unaffected.
New Release
See Why a Rule Fired
Decisions can now carry evidence: the request payload as it stood when the decision was made, and — for each rule that fired — the values behind it.GET /api/decisions/{id}/payload returns the stored request, and
GET /api/decisions/{id}/evidence returns per-rule evidence together with a
replay of the exact ruleset version that decision was pinned to. Both read under
the existing decisions:read scope.Nothing is kept by default. Retention is opt-in per data classification, and
what is stored is decided by each field’s classification: any keeps the value,
pii keeps the path with a redaction sentinel, pci-pan keeps only the
fingerprint, and pci-sad is never stored at all. Because the sensitive tiers
are already reduced when written, evidence works on instances below SAQ-D.New Release
Typed Schema Fields and Data-Classification Markers
Custom schema fields now declare a type from a closed vocabulary — fromemail, iban, and phone to amount, pan, and masked-pan — and
Specter validates incoming payloads against it at ingestion: Luhn checks
for card numbers, ISO 8601 for dates, check digits for IBANs, closed enums
for schemes and funding types. Each field also carries a data-classification
marker (pci-pan, pci-sad, pii, or none) that governs where its value
may flow: marked values are redacted from error bodies, logs, and audit
events, pan fields are always PCI-scoped, and sensitive authentication
data is never stored — below SAQ-D it is rejected outright, and nothing
that persists data (velocity counters, blacklist entries) may reference a
pci-sad field.New Release
Built-In Transaction Schema
The decision payload shape Specter ships with is now a first-class schema: read-only, introspectable through the schema registry like any custom schema, and evolving additively only — existing fields never change type or disappear. Rulesets that name no custom schema are backfilled to bind the built-in, so every ruleset now states what it evaluates against.New Release
BIN Attributes on Card Credentials
Card credentials now expose BIN-derived attributes —bin, schemes,
type, segment, currency_code, issuer_name, and
issuer_country_code — on both credential.pan and
credential.masked_pan, ready to use in rule conditions. All attributes
are optional and reflect the BIN data as provided: Specter does not curate,
reconcile, or guarantee consistency between them. schemes is an array —
match it with the in operator. When simulation detects diverging BIN
attributes, the trace carries a bin_attribute_divergence warning.New Release
Custom Decision Schemas
Define the shape of your own decision payloads with the new schema registry under/api/admin/schemas. Schemas are named and versioned: every
publication creates a new, immutable version, and a superseded version can
be deprecated while it keeps serving the rulesets bound to it. A ruleset
binds one (schema, version) pair — the binding validates
incoming payloads and resolves rule field paths. The identity fields of a
persisted ruleset (schema, context) no longer change on PATCH:
sending a different value returns a 422, resending the same value is a
no-op.The registry endpoints require the new admin:schemas:read and
admin:schemas:write scopes.New Release
Filter Pipes and HMAC Fingerprints in Rule Conditions
Field references in rule conditions now accept filter pipe chains —$.customer.email | trim() | downcase() — wherever a field path appears:
condition operands, velocity fields, and HMAC inputs. The v1 filter set is
closed and typed; filters compose left to right, and a filter receiving an
absent or malformed value abstains rather than failing the rule. Filtered
values inherit the strictest data-classification marker of their inputs.A new hmac operator composes an ordered list of filtered field references
into a deterministic HMAC-SHA256 fingerprint, so rules can correlate on
stable derived identifiers without exposing the underlying values.New Release
Simulate Rulesets Without Side Effects
The newPOST /api/admin/rulesets/{id}/simulate endpoint evaluates a test
payload you supply against any persisted ruleset — DRAFT, ACTIVE, or
INACTIVE — and returns a full evaluation trace: the verdict the ruleset
would reach, and for every rule whether it fired, abstained, or was skipped,
and why. Simulation is inert — no decision is recorded, no velocity counter
is incremented, and no backend is invoked. Velocity conditions read live
counter state and report the value they evaluated against alongside the
rule’s threshold and rolling window, so the trace shows how close a rule
was to firing.An abstention always names its cause: absent_fields where a field the rule
addressed was not in the payload, and absorbed_operands where one was
present but could not be used — a membership test whose set operand is not
an array, for instance, which otherwise looks like a rule that silently
never matches.Simulation requires the new admin:rulesets:simulate scope.New Release
PayPal Payment Method Type
Decision requests now accept apaypal credential alongside pan,
masked_pan, and sepa. The credential carries email, payer_id,
address_status, protection_eligibility, and payer_status — all
optional, all caller-injected from your PayPal flow, and all addressable in
rules as $.credential.paypal.*. Send whichever your flow has captured at
the point you call Specter. The email is never persisted raw; the decision
log keeps the credential fingerprint (computed from payer_id) and a masked
form. See
Credential types.Improvement
Report Fraud on Any Decision
fraud_report events are no longer tied to the payment lifecycle: they are
accepted in any payment state — including for decisions that never received a
lifecycle event — so you can feed fraud signals back to Specter even if you
only use it for screening. An accepted report sets a permanent fraud marker on
the decision (fraud_marked_at, fraud_source, fraud_reason), independent
of payment_state, and continues to fan out to backends and auto-populate the
blacklist. See
lifecycle events.Improvement
Smarter Condition Fields in the Rule Editor
The rule editor’s condition field suggestions are now served by the backend, so the type-ahead always matches the exact request shape your rules evaluate against — including nested credential fields and your$.metadata.* paths.
Fields that are unavailable on your instance, such as raw PAN paths below
SAQ-D, are omitted automatically.New ReleaseImprovement
Run Specter Within SAQ A and SAQ A-EP Scope
Specter now runs on instances assessed at SAQ A or SAQ A-EP — you no longer need a SAQ-D environment to use it. Below SAQ-D, Specter actively enforces the boundary that keeps your assessment scope small: request bodies carrying a raw card number (PAN) outside the designated credential field are rejected with a422 validation error, and Interceptor payloads are scanned so raw
card data never reaches your instance.The platform test-card policy applies on every instance: on the Test tier,
decision requests accept only known test cards; on the Production tier, test
cards are rejected. Violations return a 403 with a clear classifier.New Release