Skip to main content
The payment step is where anonymity matters most: a masked session at checkout is a stronger signal than the same session browsing a catalog. This pattern gets a fresh Risk Score (0–100) right before the charge, then branches on the Score and its band so your checkout code can decide what to do.

What is payment fraud at checkout?

Payment fraud at checkout is the use of stolen cards, stolen accounts, or coordinated fake identities to push a charge through the payment step before it can be caught. The tell is concealment: the buyer hides behind a VPN, proxy, Tor, a datacenter IP, or an anti-detect browser so the session cannot be traced back to a single person or device.

How ShieldLabs surfaces it

ShieldLabs resolves the buyer to a set of identifiers and returns a Risk Score with named anonymity signals. A naive checkout trusts the cookie, the session, or the buyer’s IP — all three are trivial to reset, so a fraudster clears cookies, opens incognito, or rotates to a fresh proxy IP, and a cookie- or IP-keyed rule treats each try as a brand-new buyer. The durable DeviceID is derived server-side, so it survives cleared cookies, incognito, and IP rotation and recognizes the same device behind a string of “new” checkout attempts. ShieldLabs surfaces the Score and the reasons; your checkout code owns the verdict.

Prevent payment fraud at checkout

Read the fresh Risk Score and the durable DeviceID the moment the buyer reaches the payment step. The rule your code applies: gate the charge on the Score and its band, drawn tighter at payment than on a low-stakes page, and step up or hold when masking signals like Tor, Anti-detect Browser, or Abuser Flag fire, or when the DeviceID matches a device tied to past abuse. The outcome is that a buyer hiding behind a fresh proxy IP or cleared cookies still resolves to the same device, so your code can allow the clean charge, ask for 3DS or OTP on the borderline one, and route the high-band one to review.

Build it

1

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.
2

Force a fresh check at the payment step

On page load you may already run checkAuthenticatedUser for analytics. At the payment step you want a current read, so use the forceCheck* variant: it clears the session and runs a new identify call immediately, scoring the session as it is at payment time, not a stale score from page load. Pass a hashed or pseudonymous user id, never a raw email or account id.
checkout.html
<script type="module">
  const mod = await import(
    'https://cdn.shieldlabs.ai/snippet.js?publicKey=YOUR_PUBLIC_KEY'
  );
  // The browser does NOT compute the Risk Score. The callback's first arg
  // is the client IP the server saw (not a score). Keep the requestID:
  // it is your join key to the webhook you receive server-side.
  mod.forceCheckAuthenticatedUser('a1b2c3d4hasheduserid', (ip, requestID) => {
    document.getElementById('shield-request-id').value = requestID;
  });
</script>

<form id="checkout-form" method="POST" action="/api/checkout">
  <input type="hidden" id="shield-request-id" name="shieldRequestId" />
  <!-- payment fields -->
  <button type="submit">Pay</button>
</form>
The snippet POSTs the signals to rest.shieldlabs.ai automatically; installing the snippet covers the React, Vue, Angular, Preact, and Svelte versions of the same dynamic-import pattern.
3

Receive the webhook and gate the charge on Score plus band

ShieldLabs POSTs one signed webhook per scored identification. Verify X-Shield-Signature on the raw body, then cache the result keyed by request_id so the checkout request can look it up — the shared waitForScore helper (defined once for every tutorial) does this read, polling the cache and falling back to the History API by request_id. Delivery is at-most-once with no retries, so the History fallback covers a dropped webhook; each returned row bills 1 request while the webhook is free.Branch on the Score and its band, which already fold in the anonymity signals. At the payment step, draw the band lines tighter than elsewhere.
checkout.js
app.post('/api/checkout', async (req, res) => {
  const { shieldRequestId, paymentData, userId } = req.body;

  // Wait up to ~2s for the webhook; falls back to the History API.
  const shield = await waitForScore(shieldRequestId, 2000);

  // No result yet is not the same as "clean". Hold for review rather than
  // silently letting a payment through on missing data.
  if (!shield) {
    return res.status(202).json({ status: 'review', reason: 'verifying' });
  }

  const score = shield.score;

  // High band: your code decides to block or hard-challenge.
  if (score >= 60) {
    await flagForReview(userId, shield);
    return res.status(202).json({
      status: 'verify',
      reason: 'Extra verification is required to complete this payment.',
    });
  }

  // Medium band: step up to 3DS / OTP before the charge.
  if (score >= 30) {
    return res.status(202).json({ status: 'step_up', method: '3ds' });
  }

  // Clean / Low band: proceed with the charge in your own flow.
  return processPayment(paymentData, userId, res);
});
The block, step-up, and review actions run in your application. ShieldLabs returns the Score and its signals; your checkout code owns the verdict.
4

Read the real network behind a VPN

For a stolen-card buyer who hides their location, the webhook carries two IPs. public_ip.country comes from the public IP, which a VPN can put anywhere; local_ip.country is the real network IP, which can expose the network behind the VPN. When the two disagree, detection_flags.ip_mismatch is set to true — a buyer pretending to shop from one country while their real connection sits in another.
// Inside your /api/checkout handler, after you have `shield`.
const claimed = shield.public_ip?.country;
const real    = shield.local_ip?.country;
if (real && claimed && real !== claimed) {
  // ip_mismatch is set too; treat this as a step-up trigger.
  return res.status(202).json({ status: 'step_up', method: '3ds' });
}
local_ip.ip is empty when the follow-up network check could not complete; the Stun not checked signal may fire instead (the stun_not_checked flag), so you do not silently lose the comparison.For a hard rule that does not depend on the band, branch on the detection_flags object: its keys (vpn, tor, proxy, datacenter_ip, abuser, os_mismatch, ip_mismatch, anti_detect_browser, …) are stable booleans, safer than the human-readable signals labels.
if (shield.detection_flags.tor || shield.detection_flags.abuser) {
  // Always step up, regardless of the numeric band.
  return res.status(202).json({ status: 'verify' });
}
5

Re-check on later sensitive actions

For a high-value order or a follow-up withdrawal, run another forceCheck* at that moment. Each sensitive action deserves its own fresh check rather than a reused score.
mod.forceCheckAuthenticatedUser('a1b2c3d4hasheduserid', (ip, requestID) => {
  fetch('/api/post-purchase', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ requestID, action: 'high_value_order' }),
  });
});

Reading the Risk Score at checkout

The four bands and their ranges are defined in Risk Scoring; the full action playbook is in Acting on the Risk Score. The payment step is a good place to draw the same band tighter than you would elsewhere:
BandAt a low-stakes pageAt checkout
Clean (0–9)Pass throughAllow, charge
Low (10–29)Allow, logAllow, log the signals
Medium (30–59)Second lookStep up to 3DS or OTP before the charge
High (60–100)Review or challengeHold for review or require verification, decided by your code
The Risk Score already folds in the masking signals, so your code branches on the Score and its band rather than the label text. The names below are the human-readable labels in signals for visibility and logging; treat them as illustrative. The Signals reference lists the full set with weights.
Signal in signalsWhy it matters at payment
TorConnection exits through the Tor network. Rare for legitimate buyers. Usually a hard challenge or block decided by you.
Anti-detect BrowserFingerprint-spoofing indicators. Common in coordinated payment abuse.
ProxyIP flagged as a proxy. One signal among several; weigh with the rest.
Datacenter IPIP is in a hosting range. Unusual for a real shopper on a personal device.
Abuser FlagIP or device appears on an abuse reputation list. Corroborating, not conclusive on its own.
OS MismatchThe OS the browser claims does not match other evidence. A spoofing indicator.
Proxy, Datacenter IP, and Abuser Flag are each low weight and stack: a buyer on a flagged datacenter proxy with abuser reputation reaches the Medium band from these three together, where any one alone would not. Tor, JavaScript Disabled, Anti-detect Browser, and OS Mismatch are the high-weight single signals that push straight into High.
A legitimate buyer can score high. Corporate VPNs, privacy browsers, and iCloud Private Relay all raise the Risk Score for real customers. Decide on Score plus signals plus the action context, never the number alone, and tune your thresholds gradually. A VPN or Privacy Relay signal alone is weaker evidence than Tor or Abuser Flag.

Test it

Confirm the durable DeviceID holds before you wire thresholds to live charges. Run a checkout, note the device_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 identifier that links one buyer across “fresh” sessions. To see the Score react, repeat the checkout through a VPN or proxy and watch the signals array gain a VPN or Proxy entry with a higher Score. Patterns like Many Accounts on One Device live on the dashboard only. They grade an entity (Suspicious or Dangerous over a rolling window, typically 30 days) and are not part of the checkout webhook. Use the per-request score, signals, and detection_flags for the in-the-moment charge decision; use Patterns (exported as CSV or JSON) for the offline review of repeat offenders. The two join on device_id.

Next steps

When a disputed charge lands weeks later, the same DeviceID you scored here becomes chargeback-dispute evidence. Upstream, the same fresh-check pattern guards a suspicious login with step-up authentication and a new account at signup, and the durable device link surfaces one buyer running many accounts.

Acting on the Risk Score

Turn the Score and signals into allow, challenge, review, and block 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.

Chargeback evidence

The after-the-sale half: reconstruct the buyer’s device history into a dispute package.