What is card testing?
Card testing (also called carding, card cracking, or card checking) is the rapid validation of stolen card numbers by pushing many low-value or zero-value authorizations through a checkout to see which cards are still live. Some testers push a tiny charge; others use a zero-dollar authorization that is slower to surface, so a run can stay quiet for a while. The tell is repetition without a real buyer behind it: a burst of attempts in a short window, many declines, and often a masked or rotating connection so each try looks like a different person. It clusters on low-friction, low-ticket flows — guest checkouts, digital goods, gaming top-ups, and donation forms — where a small charge looks unremarkable.How ShieldLabs surfaces it
ShieldLabs resolves each checkout session to a set of identifiers and returns a Risk Score with named anonymity signals. It identifies the session — the device behind the browser and how anonymous the connection is. The card number, the BIN, and the authorization result stay with your payment processor; ShieldLabs reads the session, so a tester swapping cards still resolves to the same device. A naive checkout keys its attempt counter on the cookie, the session, or the buyer’s IP — all three reset for free, so a tester clears cookies, opens incognito, or rotates to a fresh proxy IP and each try reads as a brand-new shopper. The durable DeviceID is derived server-side, so it survives cleared cookies, incognito, and IP rotation and recognizes the same device behind a run of attempts. That stable anchor is what makes counting possible.ShieldLabs gives you a device id that holds steady, so your code counts attempts per device, in your own datastore, against your own limit. The split is the whole point: ShieldLabs identifies the session; your code does the counting and owns the verdict.
Prevent card testing
The rule your code applies: at each payment attempt, identify the session, then read the durabledevice_id, the anonymity signals, and the Risk Score (0–100) from the webhook or History. In your own store, increment an attempt counter keyed on that device_id (and on local_ip.ip for the network view), and add friction — 3-D Secure, a hold, or a step-up — when the per-device count crosses your own limit, or when the session is masked (a datacenter IP, proxy, Tor, an anti-detect browser, or an ip_mismatch flag). The outcome is that a tester swapping cards behind cleared cookies and a fresh proxy still resolves to one device, so your code can let a clean first attempt through and clamp down once the same device starts cycling cards. ShieldLabs surfaces the device and the signals; your checkout code owns the limit and the verdict. The steps below wire it up.
Build it
Create a ShieldLabs account and get your keys
Sign up for free and get 5,000 identifications, or log in if you already have an account. Register the domain you want to identify visitors on, then open the Keys page. Use the Public Key to initialize the snippet in the browser, and keep your server-side credentials on your backend: the Private API Key authenticates the History API, and each webhook endpoint has its own
whsec_… signing secret. See Keys.Force a fresh check at the payment step
On earlier pages you may already run a check for analytics. At the payment step you want a current read, so use the For a guest checkout with no account, use
forceCheck* variant: it clears the session and runs a new identify call immediately, scoring the session as it is at payment time. Pass a hashed user id, never a raw email or account id.checkout.html
forceCheckAnonymous instead; installing the snippet covers the React, Vue, Angular, Preact, and Svelte versions of the same dynamic-import pattern.Read the device and count attempts in your own store
Pull the scored result for that Track declines the same way if your processor returns them: declines are the tell, not noise, because a tester reads each rejection reason and retries, so one device returning a string of declines against fresh cards is the classic card-testing shape. Only your code sees the payment result, so count declines per
RequestID with the shared waitForScore helper, which polls your webhook cache and falls back to the History API. Then do the counting on your side: increment a per-device_id attempt counter in your datastore and compare it to your own limit. This counter is your velocity, in your data — ShieldLabs supplies the stable anchor to count on.api/pay.js
device_id alongside attempts.Add friction to masked sessions, regardless of count
A first attempt has no history yet, so the count alone will not catch the opening move of a run. The anonymity picture catches it on the spot: card testing usually rides a masked connection. Branch on the Read the real network behind a VPN. The webhook carries two IPs.
detection_flags object — its keys are stable booleans, safer than parsing the human-readable signals labels.public_ip.country comes from the public IP, which a proxy or VPN can put anywhere; local_ip.country is the real network IP, which can expose the network behind the mask. When the two disagree, detection_flags.ip_mismatch is set to true, so your code can act on the gap. Counting per local_ip.ip as well as per device_id catches a run spread across several devices but one real network.Corroborate the run on the dashboard over time
The per-request decision above is in-the-moment. The pattern that confirms a card-testing operation builds up over hours: one device, or one local network, cycling through account after account. Patterns grade each entity Suspicious, then Dangerous, as the linked count crosses thresholds in a rolling window, and levels never downgrade once flagged.
These live on the dashboard Patterns tab only and are not in the checkout webhook. Export them as your watchlist, or reconstruct the same counts live from the History API:
Many Accounts on One Device
Many Accounts on One Device
Many distinct accounts driven from a single device. A tester running stolen cards through guest checkouts or throwaway accounts produces this shape. The grouping identity is the
device_id.Many Accounts on One Local IP
Many Accounts on One Local IP
Many accounts originating from one real network IP. Catches a run spread across several devices on the same connection. The grouping identity is
local_ip.ip.Count accounts per device
Account History reads on
account.shieldlabs.ai and webhook delivery are free; the Management History path bills 1 request per returned row. For routine watching, use the dashboard pattern export and reserve billed reads for devices you are actively investigating.Test it
Confirm the durable DeviceID holds before you wire your attempt limit to live charges. Run one checkout, note thedevice_id from the webhook, then repeat the visit in an incognito window, after clearing cookies, and from a second browser on the same machine: the cookie_id and visitor_id change each time, but the device_id stays the same — that is the anchor your per-device counter sits on, so three “fresh” tries from one tester increment one counter, not three. Then repeat through a VPN or proxy and watch the signals array gain a VPN or Proxy entry with a higher Score, the same masking your friction rule keys on.
Next steps
Card testing is the front edge of payment fraud at checkout; the same fresh-check pattern and the samedevice_id carry through from validating a stolen card to pushing the real purchase. And because a tester farming many throwaway accounts is the same durable device behind each one, the device link that powers your attempt counter also surfaces one person running many accounts.
Acting on the Risk Score
Turn the Score and
signals into allow, challenge, review, and hold logic in your app.Signals
Every signal that can appear in
signals, in plain language, with its weight.The Risk Score
How the 0 to 100 score is built, what
signals carries, and the band definitions.Payment fraud at checkout
The next step: read device and anonymity signals before you charge.