# Payment fraud

> Device context for card and account payments: what it adds at checkout, and what it does not replace.

Source: https://whiskerjs.com/use-cases/payment-fraud/ (last updated 2026-10-26)

> **In plain words:**
>
> At checkout you already ask whether a payment is likely genuine. Your payment provider runs its own checks, and in some regions the bank asks the customer to confirm. WhiskerJS adds one more view: does the device paying now look like a device this customer used before, and does anything about it look automated or altered? It complements those checks; it does not replace them.

## The problem

Payment fraud includes stolen card details used on a fresh account, a taken-over account spending stored cards or balances, card testing (many small payments to find working cards, usually scripted) and refund or chargeback abuse. Your server sees the order, the account and the network. The device is a further, independent view.

## What WhiskerJS contributes

Collect when the customer submits the payment, bound to a nonce your server issued for this checkout, and send the result with the order:

```ts
const binding = await (await fetch('/api/checkout/nonce', { method: 'POST' })).json();
const device = await agent.collect({ timeoutMs: 3000, binding });
await fetch('/api/checkout', {
  method: 'POST',
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify({ orderId, paymentToken, device }),
});
```

The result is about 4 KB of JSON. If you verify the binding, send it unchanged: the digest covers every field. See [Sending the result](https://whiskerjs.com/docs/integration/#sending-the-result).

## Signals and flags that matter

- **Known device for the account**: an exact `visitorId` match with earlier orders or logins.
- **Many cards, one device**: the same `visitorId` paying with many different cards in a short time is a classic pattern your rules can count.
- **Locale against card and network**: [`time_zone`](https://whiskerjs.com/signals/time-zone/), [`languages`](https://whiskerjs.com/signals/languages/) and [`intl_locale`](https://whiskerjs.com/signals/intl-locale/) compared with the IP region and the billing country. Travellers and expatriates differ honestly, so weigh, do not block.
- **Automation on checkout**: [`automation_webdriver`](https://whiskerjs.com/docs/integrity/#automation_webdriver), [`automation_globals`](https://whiskerjs.com/docs/integrity/#automation_globals), [`headless_user_agent`](https://whiskerjs.com/docs/integrity/#headless_user_agent), [`software_renderer`](https://whiskerjs.com/docs/integrity/#software_renderer) are typical of scripted card testing, and also of your own tests and of some real users (no GPU acceleration).
- **Altered values**: [`iframe_mismatch`](https://whiskerjs.com/docs/integrity/#iframe_mismatch), [`prototype_lie`](https://whiskerjs.com/docs/integrity/#prototype_lie), [`native_tampering`](https://whiskerjs.com/docs/integrity/#native_tampering). Privacy extensions cause these too.

## Using structuredId similarity

A returning customer’s device drifts: a browser update changes the `browser` group, a new monitor the `device` group. Comparing group by group lets a rule treat “one group changed” differently from “almost everything changed”:

```ts
import { ID_GROUP_ORDER } from 'whiskerjs';

/** Number of groups that match the closest known device of this customer. */
function bestMatch(current: string, known: string[]): number {
  const cur = current.split('_');
  let best = 0;
  for (const k of known) {
    const parts = k.split('_');
    if (parts[0] !== cur[0]) continue;               // same idVersion only
    const n = ID_GROUP_ORDER.filter((_, i) => parts[i + 1] === cur[i + 1]).length;
    best = Math.max(best, n);
  }
  return best; // 0 to 7; what counts as "close enough" is yours to tune
}
```

## Anti-replay

A recorded result from a genuine customer’s browser should not approve a fraudster’s payment. With the binding, your server accepts each result once, only with the nonce it issued for this checkout, and only while it is recent. See [Anti-replay binding](https://whiskerjs.com/docs/integration/#anti-replay-binding) and the server guides for [Node](https://whiskerjs.com/guides/verify-node/), [Go](https://whiskerjs.com/guides/verify-go/), [PHP](https://whiskerjs.com/guides/verify-php/) and [Python](https://whiskerjs.com/guides/verify-python/).

## What must happen on your server

1. Verify the result’s shape, versions and binding before using it.
2. Combine the device view with your other evidence: order value and contents, account age and history, address and card details, IP reputation, velocity per device, card and account.
3. Keep your payment provider’s fraud checks and any strong customer authentication that applies to the payment. A device signal is never a substitute for them.
4. Decide proportionately: approve, step up, hold for review or decline. Never let the device result alone approve a payment or release money.
5. Store the ids, versions, flags and the components your rules use with the order, for the retention your purpose and obligations require, and say so in your [privacy notice](https://whiskerjs.com/privacy/privacy-notice/).

## Limits

- A fraudster with a clean, ordinary browser produces an ordinary result. The device view then only says “unknown device”.
- Shared devices and identical machines can link innocent customers.
- Privacy browsers exclude some components by design, so those devices are less distinct.
- No figures for fraud caught or false declines have been measured for WhiskerJS; test on your own orders before you change thresholds.

## Questions and answers

### Does WhiskerJS replace my payment provider's fraud checks?

No. It adds device context to your own decision. Keep your provider's checks and strong customer authentication where it applies.

### Can I send the WhiskerJS result to my payment provider?

Only if your provider accepts such data and your privacy notice covers it. WhiskerJS itself sends nothing anywhere; any sharing is your decision and your request.

### When should I collect at checkout?

When the customer submits the payment, with a fresh nonce for that checkout. Collecting earlier risks a result that is no longer bound to the payment.

## Related pages

- [Fraud prevention](https://whiskerjs.com/use-cases/fraud-prevention/): The general pattern for device signals.
- [Account takeover](https://whiskerjs.com/use-cases/account-takeover/): Payments from a taken-over account.
- [Verify the binding in Python](https://whiskerjs.com/guides/verify-python/): Recompute the digest on your server.
- [CCPA and CPRA](https://whiskerjs.com/privacy/ccpa-cpra/): Device identifiers and notice at collection.
