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

CodeMeaningTypical action
disposable_emailEmail domain is a known disposable/burner provider.Usually arrives with review. If trials are being farmed, consider requiring a non-disposable email at product level.
invalid_mxEmail’s domain can’t receive mail (no valid MX).Treat the address as unverifiable; hold trial credits behind email verification.
young_domainEmail’s domain was registered very recently.Weak signal on its own — meaningful in combination. No action alone.
malformed_emailThe engine couldn’t parse/normalize the address.If real users trigger this, check what your form is sending us.

IP signals

CodeMeaningTypical action
proxy_ipIP is a known commercial proxy/VPN exit.Don’t punish alone — plenty of real users run VPNs. Strengthens other signals.
tor_exitIP is a Tor exit node.Same: combined-signal, not a standalone conviction.
datacenter_ipIP belongs to a hosting/datacenter ASN, not a consumer network.Characteristic of scripted traffic; expect it alongside velocity codes during attacks.

Velocity signals

CodeMeaningTypical action
velocity_ip_signupsAbnormal signup rate from this IP.Attack-wave indicator; watch the dashboard for the spike.
velocity_device_signupsAbnormal signup rate from this device.Stronger than IP velocity (devices rotate slower than IPs). Requires a verified device_token — see device fingerprinting.
velocity_domain_signupsBurst of signups sharing one email domain.Scripted signup wave or a company rollout — the review queue will make it obvious which.
velocity_card_attemptsMany checkout attempts on the same card fingerprint.Usually a real buyer having a bad time — a 3DS loop, a mistyped CVC, dunning retries. It reviews; it never blocks on its own. Consider manual capture while it lasts.
velocity_ip_card_attemptsMany checkout attempts from one IP. Counts attempts, not distinct cards — a busy office NAT produces it too.Corroborating only. Look for velocity_distinct_cards on the same events to tell a card-tester from a busy shared IP.
velocity_distinct_cardsMany different card fingerprints seen from one IP (1h) or one device (24h).The card-testing signature. A single device presenting six different cards in a day has no ordinary explanation; the same count from one IP may be a coworking space or a family, so the IP form never blocks on its own. Requires payment.card_fingerprint — an event sent without one never enters the distinct-card sets, so this code can never fire for you.
velocity_card_countriesThe linked accounts behind this user have presented cards issued in three or more different countries in 30 days.Behavioural, not demographic — a customer living abroad has two issuing countries and never triggers this. It reviews and never blocks on its own, because two people sharing a device can be linked into one identity and pool their cards. Detail carries {cluster_card_countries_30d, threshold}. Not reachable yet — see below.
velocity_cluster_trialsOne linked identity repeatedly starting trials.Trial multiplexing. Deny in review unless you recognize the cluster as legitimate (agencies, teams).

velocity_card_countries cannot fire for you yet, and there is nothing you can do to enable it. It counts distinct card-issuing countries, which it reads from a payment.card_country field. That field exists in the API contract; it is not in the Node SDK’s Payment type 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

CodeMeaningTypical action
identity_clusterThis identity links to existing accounts (detail.linked_accounts).Open the cluster in the dashboard. Households, teams, and agencies are legitimate multi-account clusters — that’s exactly what review is for.
cluster_confirmed_abuseThe linked cluster contains confirmed abuse (from your feedback or a review denial).Block-strength. Deny fast in review; the graph is doing its job.
cluster_trustedThe cluster was marked trusted (by you or by us).Informational; pushes toward allow.

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.

CodeMeaningTypical action
ato_new_deviceThis account signed in from a device it has not used before, on an account we do have history for (detail.known_devices_30d).Usually a new laptop or phone. Pair with recommended_action: "step_up" and ask for a second factor rather than refusing.
ato_account_device_burstMany separate devices signed in to this one account in quick succession (detail.devices_10m).A shared or stuffed credential. Step up, and consider ending the account’s other sessions.

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

CodeMeaningTypical action
prepaid_cardCard funding type is prepaid.Common in trial abuse and card testing, but many legitimate customers use prepaid cards — it’s a combined-signal, and your feedback calibrates it.
card_probe_patternAttempt shape matches card-validation probes: small amounts, rapid, from one source — whether the same card is retried or a fresh card is used each time.Two such attempts in ten minutes review; three or more block on this signal alone. It is the one card signal that convicts without corroboration — a run of tiny authorizations has no legitimate retry story. Requires payment.amount (see below). If you sell at sub-1.00 price points, email hello@portreeve.com so the probe ceiling fits your prices. Refund any probes that settled before detection, before they become disputes.

card_probe_pattern needs an amount, and a 0 amount is judged differently. An absent payment.amount is unknown, not small: with no amount the probe ladder is skipped entirely and the attempt falls through to velocity_card_attempts instead, which never blocks alone. Send amount (minor units) and currency on every checkout and trial-conversion event — currency selects 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 more block. 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

CodeMeaningTypical action
allowlistedMatched your allowlist — verdict forced to allow.None. Manage lists in dashboard Settings.
blocklistedMatched your blocklist — verdict forced to block.None. Manage lists in dashboard Settings.

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.

CodeMeaningTypical action
timeout_failopenThe verdict call exceeded timeoutMs; failMode produced this verdict.Should be ~zero. If you see these regularly, tell us — it’s our latency problem, not your integration.
network_failopenThe SDK couldn’t reach the API at all (DNS, connect, TLS).Check your egress/firewall first, then tell us.
upstream_failopenThe API answered with a 5xx.Our problem — tell us if it persists beyond a blip.
invalid_response_failopenThe API response didn’t parse as a verdict.Almost certainly our bug — report it with a timestamp.

Meta

CodeMeaningTypical action
partial_enrichmentThe verdict was decided within its latency budget before some slower lookups finished.Informational. The stored event is upgraded asynchronously; future decisions about the same actors use the completed data.