Skip to main content
Improvement

Blacklist Entries Carry Data Classifications

Every blacklist entry now carries value_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-in transaction 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 for POST /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 with async: 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 an hmac operand must read hmc_ 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 pii were shown without a marker: customer.id, the city / country / postal_code / state leaves of billing and shipping, credential.masked_pan.first_six / last_four, credential.paypal.payer_id, and frequent_flyer_number / country_code on airline passengers. On decision reads, customer_id is pii as well.
  • Smaller fixes. shipping.destination_types is an array of strings; masked_pan.first_six is exactly six digits; a line item needs name or sku; phone examples are E.164; on_error: review is accepted when creating a rule; schema versions have no draft state — every publication creates an immutable version, active until deprecated; the schema-registry list endpoints are not paginated (the documented after / limit parameters never existed); carry_over governs velocity counters only, never blacklist entries; a context key in a simulation payload is ignored rather than rejected; the interceptor endpoint documents its 422 (validation / PCI gate) and 502 (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

Each backend_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 marked pii 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 a pci-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 hmac and compare the digest against a literal (the hmc_-prefixed fingerprint) — a digest is not regulated data;
  • for card matching specifically, use the credential fingerprint.
Nothing else moved. 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.
  • schema is required when you create a ruleset. POST /api/admin/rulesets without it rejects with 422; 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 the transaction binding and keep working.
  • Rule paths validate against the bound schema. A field reference the bound schema does not declare rejects at submission with 422 naming 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- and pci-sad-scoped fields are refused, because a counter key persists the counted value.
  • Decisions carry their schema. Decision responses and GET /api/decisions/{id} include schema — 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 — from email, 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 new POST /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 a paypal 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 a 422 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

Specter Is Live

Specter — the risk and fraud decisioning component of the Composable Payment Architecture — is now generally available on the Test and Production tiers. Evaluate payments against your own rulesets and get an allow, block, or review decision in real time. Integrate through the Decision API, or drop the Interceptor into your existing PSP traffic without changing your payment flow. External risk engines plug in through Link backends.Start with the overview and plans and access.