Reason codes
Every verdict carries reasons[] — the codes that explain it. The codes
are public, stable contract; the thresholds and logic that fire them are not
public and will keep evolving without notice. Some reasons carry a small
detail object of scalars (e.g. identity_cluster includes
{"linked_accounts": 4}).
You will eventually receive codes not on this page. The enum is tolerant by design — new detection signals ship as new codes without a new SDK release (the SDK types them as
KnownCode | string). Treat any unknown code as informational only: log it, display it, never crash on it, and never map “unknown” to a punitive action. The verdict field is the decision; reasons are the explanation.
Email signals
IP signals
Velocity signals
velocity_card_countriescannot fire for you yet, and there is nothing you can do to enable it. It counts distinct card-issuing countries, which it reads from apayment.card_countryfield. That field exists in the API contract; it is not in the Node SDK’sPaymenttype and there is no way to send it. So the rule is live in the engine, the counter it reads is always zero, and the code never appears.This is the deliberate rollout order — API first, fully deployed, then the SDK, then these docs — and not an oversight. A new field has to be accepted everywhere before any client starts sending it; shipping it the other way round means some fraction of calls hit a server that has never heard of the field. We would rather a signal be briefly unavailable than briefly unreliable. The row stays on this page so that when the code does start appearing you already know what it means.
If you are asking because you want this signal: it is not available, no configuration turns it on, and we will tell you when it ships.
Identity signals
Account signals (login only)
These read one account’s own history rather than volume across a shared identifier, so they need both external_user_id and device_token on the request. Send a login without either and neither can fire.
Neither fires for an account we hold no history for. A first-ever login — and every login while our cache is cold — produces no account signal rather than a false one. The absence of history is never treated as evidence.
Devices accumulating on one account gradually is seat sharing, not takeover — a commercial signal rather than an abuse one. It is reported in the dashboard and never returned as a verdict.
Payment signals
card_probe_patternneeds an amount, and a0amount is judged differently. An absentpayment.amountis unknown, not small: with no amount the probe ladder is skipped entirely and the attempt falls through tovelocity_card_attemptsinstead, which never blocks alone. Sendamount(minor units) andcurrencyon every checkout and trial-conversion event —currencyselects the probe-sized ceiling, so omitting it applies the two-decimal default to currencies where that is the wrong scale.A zero amount is probe-sized in every currency — a $0 authorization is a setup-mode
SetupIntent, which is both how everyone stores a card on file and the purest card-validation move there is. Because those two look identical, a zero-amount event is judged only on how many different cards one device presented in ten minutes: someone fumbling one card on your signup form is a frustrated customer and is not a probe, however many times they try.The $0 ladder therefore runs on its own numbers, deliberately looser than the two-and-three above: one or two cards allow, three or four
review, five or moreblock. Three is where the legitimate case ends — a person carries three or four cards, and a customer working through personal, corporate and backup after declines must get a human look rather than a block. A real card tester presents tens to hundreds; the volume is the economics.That arm reads a device counter, so a $0 event with no
device_token(fingerprint snippet) has no probe gate at all.
Your lists
SDK-synthesized codes
These codes are attached by the SDK on your side, never by the server:
they mark a verdict the SDK synthesized locally (per your failMode) because
it couldn’t get a real one. Such results also carry degraded: true, and
their id and policy_version are null — there is no server event behind
them. The *_failopen suffix is reserved for these client-side degradation
codes; only timeout_failopen appears in the frozen enum, the others arrive
via the tolerant KnownCode | string type.