Back to blog

Payment Fraud Prevention Implementation: Device Evidence Before Checkout

Device evidence passes through a server checkpoint before reaching a separate checkout and payment provider

Last updated on October 29, 2026 · 13 min read

Payment fraud prevention implementation connects evidence to a specific, authorized payment action. For device intelligence, that means collecting in the browser, retrieving the result on the server and matching it to the order before starting the payment operation. A browser-supplied “low risk” value is not an approval.

The integration has three separate decisions: can this session operate on the order, is the device evidence usable for this attempt, and has the payment provider authorized the payment? Each needs its own evidence and state. Confusing them creates gaps even when every API call returns successfully.

This guide implements that boundary for an authenticated checkout. It includes code checked against published SDK packages, a reference implementation with automated tests, and a retry path that preserves the original evidence. No card data or real payment is required to run the tests.

What does checkout fraud prevention need to protect?

Start with the operation that creates financial exposure. Depending on the product, that may be creating a payment, authorizing a stored payment method, applying an offer or releasing goods. Record where fraud and legitimate-customer friction occur before selecting a control.

Checkout concernEvidence to connectUseful intervention point
Repeated card-testing attemptsPayment failures, attempt velocity and device activityBefore another payment operation
Use of a compromised accountAuthenticated session, account history and device contextBefore access to stored payment methods
Repeated introductory benefitsOffer eligibility, account links and order benefitsBefore applying the benefit
Disputed purchasesOrder, payment, access and fulfilment recordsAt investigation and dispute review

The payment gateway fraud detection guide explains the broader stack. This article focuses on the handoff into checkout. It does not assume that one new device, one proxy or one automated visit proves fraud.

Keep three trust boundaries in the implementation

The browser collects and correlates. It can send an Identification ID with the intended action. It must not provide the authoritative amount, account owner or risk decision. Keep private API keys and webhook secrets off the page.

The merchant backend binds and controls the action. Authenticate the session, enforce protection against cross-site request forgery (CSRF) where applicable, load the order from trusted storage and verify that the customer may operate on it. OWASP's transaction authorization guidance emphasizes server-side enforcement and protection against changes to transaction data. Recheck the order after an asynchronous evidence read; the cart can change while that read is in flight.

The payment service provider (PSP) handles its payment flow. A decision to continue means that the checkout may proceed to the PSP. It does not mean that funds were authorized or captured. Keep any required payment authentication, including 3-D Secure, in that flow. Stripe's Payment Intents documentation describes a payment lifecycle with distinct states and authentication requirements.

Browser collection, backend verification and payment-provider authorization occupy separate trust boundaries

Create the checkout attempt before collecting its evidence

Store an attempt with the authenticated account, a server session reference, order ID, cart version, amount, currency and creation time. Compute the User HID on the server. Return only the values the page needs, including the attempt ID, Public Key and pseudonymous User HID.

The reference targets these published versions:

npm install --save-exact @shieldlabs-ai/[email protected] @shieldlabs-ai/[email protected]

The browser SDK returns identificationId. The Node example reads the current card using entities.get(domainId, 'identifications', identificationId). The domain ID comes from the server's configuration; obtain the registered domain UUID through the History API domain catalog. Keep the method and version together when adapting the code.

The SDK does not attach a signed order nonce to this check. The reference therefore establishes an application-level association, checking domain, User HID, freshness and authorized attempt state. It does not claim cryptographic proof that an Identification ID was generated for a particular order. The ID is a correlation reference, not an authorization token.

For guest checkout, design a separate session-based association. Do not treat every anonymous identification as the same customer or replace account authentication with a browser identifier. The runnable reference deliberately covers authenticated orders.

Collect early and preserve the ID across submission retries

Start collection when the customer interacts with the checkout form. Attach the submit handler even if the agent cannot load. Avoid a top-level await load() that prevents the rest of the module from attaching handlers after a loading failure.

The browser portion of the reference follows this pattern:

import { load } from '@shieldlabs-ai/js';

let handle;
let identificationId = null;
const options = { publicKey };

load(options).then(agent => {
  handle = agent.identifyOnInteraction(form, { userId: userHid });
}).catch(() => {});

async function sendEvidence() {
  if (!identificationId) {
    const agent = await load(options);
    ({ identificationId } = await (
      handle?.take() ?? agent.identify({ userId: userHid })
    ));
    handle?.dispose();
    handle = undefined;
  }
  return fetch('/api/checkout/evidence', {
    method: 'POST', credentials: 'same-origin',
    headers: {
      'Content-Type': 'application/json',
      'X-CSRF-Token': csrfToken,
    },
    body: JSON.stringify({ attemptId, identificationId }),
  });
}

This excerpt shows collection and correlation. The complete module also handles collection failure, prevents overlapping submissions, reports errors to the host application and disposes listeners on unmount. When collection fails, it sends null as evidence; the backend returns pending.

If the POST fails after collection, retry it with the saved ID and attempt ID. Do not generate a fresh identification just because the response was lost. Keep the page alive until the backend request has been sent and its state handled, then follow the existing payment flow. Reset the integration for a genuinely new checkout attempt or changed account.

Retrieve the card on the server and check availability

The backend loads its own order and attempt before the API read. This is the retrieval used by the reference:

import { ShieldLabs } from '@shieldlabs-ai/node';

const client = new ShieldLabs({
  apiKey: process.env.SHIELDLABS_PRIVATE_API_KEY,
  timeout: 2_000,
  maxRetries: 0,
});

const card = await client.entities.get(
  process.env.SHIELDLABS_DOMAIN_ID,
  'identifications',
  identificationId,
  { signal: AbortSignal.timeout(2_000) },
);

Two seconds is an example request budget, not a detection-latency promise. An absent record, transient failure or exhausted budget leaves this attempt pending. A credential error is an integration fault to fix and alert on. Neither case should become a fabricated score of zero.

Before using the card, check its expected domain, ID, account context and observation time. Then require the availability states needed by the operation:

const usable = card.risk.state === 'ok'
  && card.signals.state === 'ok'
  && Number.isFinite(card.risk.score)
  && card.risk.score >= 0 && card.risk.score <= 100
  && !card.signals.items.includes('check_incomplete');

An empty list with signals.state: 'ok' differs from an unavailable signal set. Keep that distinction. Also distinguish this identification's risk from account-level high-risk events or a current entity risk category; they are not interchangeable fields. See the fraud detection API guide for the wider contract.

Persist a decision without making the same payment twice

The reference uses a SQLite transaction and a unique Identification ID claim. It rechecks the authorized order snapshot after retrieval, claims the evidence and stores the resulting decision together. An order ready for the normal payment route is frozen before leaving that transaction. In another database, preserve those atomic guarantees rather than replacing them with a separate “check then insert.”

ResultReference behavior
Missing, unavailable or incomplete evidencePending; no payment-adapter call
Wrong account, session, order, cart version or observation timeReject the attempted association
Sample review or step-up routeHold for the relevant workflow; no direct payment-adapter call
Usable evidence and a continue decisionReady for PSP; payment checks still apply
Same decided attempt and ID sent againReturn the stored decision
ID claimed by another attemptReject reuse

The sample routing illustrates an authenticated checkout, including review and step-up states. It is not a universal score cutoff for all merchants. A detected AI browser or automation signal alone does not establish that the intended payment is unauthorized. Evaluate that context with the account, order and payment history.

Rate-limit the evidence endpoint and payment operations using the context appropriate to your service. One-use evidence does not stop someone from creating many different orders, and a PSP error is not permission to retry indefinitely. Track failed payment velocity alongside device activity; the card-testing guide covers that pattern.

Use the payment provider's components for payment details. The device-evidence endpoint does not need card numbers or CVV values.

A second synthetic case changes the cart version while the API read is in flight. The guard reloads the order and rejects the old attempt, even when the device result is otherwise usable. That is why the second authorization check belongs after retrieval.

A separate payment idempotency key belongs to the order. Keep it stable across transport retries and replacement checkout attempts for that order. Stripe's idempotency documentation explains that retries with the same key and parameters can reuse the first operation's result; a changed request is not an interchangeable retry.

Consider a lost response. The evidence decision was stored, and the PSP may already have created a payment object. Retrying should recover those states, using the same key and parameters. Starting a new payment because the client did not see the response can duplicate the financial operation. Freeze or reconcile a payment already started before changing its order amount.

Checkout retries converge on the same order and payment idempotency key

Verify notifications without turning evidence into payment success

For a ShieldLabs identification notification, verify the signature over the original body bytes, then check event type and domain:

import { webhooks } from '@shieldlabs-ai/node';

const event = webhooks.constructEvent(
  rawBody,
  signatureHeader, // X-Shield-Signature
  process.env.SHIELDLABS_WEBHOOK_SECRET,
);

Persist notification processing idempotently. The reference deduplicates an identical verified body and records an evidence receipt. It does not create a payment from that receipt. A later refined observation can be a different message, while a repeated delivery must not repeat an order transition. ShieldLabs webhook documentation describes the signing contract.

Keep payment notifications separate. Verify them with the payment provider's own signing mechanism and associate the provider object with the stored order, amount and currency. Confirm the appropriate successful payment state before fulfilment. Stripe's webhook guidance covers asynchronous events, signature verification, retries and duplicates; do not depend on delivery order or the customer's redirect alone.

Test the integration before enabling a checkout action

The reference code package contains the browser module, server guard, SQLite state and tests. Run it with Node.js 22.13 or later:

npm ci
npm test

The package is an executable integration reference, not a replacement storefront. Its host application must supply authenticated routes, CSRF handling, authoritative order data and the real payment adapter. The tests exercise the published SDK with synthetic API responses; they do not demonstrate a live PSP charge or measure fraud-detection accuracy.

The checked cases include successful retrieval, malformed and substituted IDs, domain/account/session mismatch, changed cart state, stale observations, unavailable risk or signals, incomplete collection, replay, concurrent submissions, bad credentials, timeouts and duplicate or altered signed notifications. The 29 October 2026 check passed 34 server tests with the installed packages. Two browser scenarios verified network retry and blocked collection; retrying a failed POST reused the captured ID rather than creating another identification.

Before rollout, repeat the path on a registered test domain with its own keys and your provider's test environment. Check controlled recovery when collection is blocked, and confirm that pending, review and step-up states cannot call the payment adapter. Measure legitimate payment completion, false-positive interventions, evidence availability and time spent waiting alongside confirmed fraud outcomes. Avoid counting every held attempt as fraud prevented.

Add ShieldLabs device evidence before checkout

ShieldLabs combines visitor and device identification, named risk signals, bot and AI traffic detection, risk scoring and account-level high-risk events. Checkout integration connects those detections to the authorized order and existing payment process.

Keep the private credential on the backend, retrieve a domain-scoped result, retain availability states and connect the evidence to the specific action. Use the card-testing guide for repeated payment attempts and the fraud decisioning comparison for the distinction between evidence, intervention and outcome.

Start Free with 5,000 identifications.

Sources

  1. ShieldLabs JavaScript SDK and Node.js SDK, with package versions pinned in the example.
  2. ShieldLabs History API and webhook contract.
  3. OWASP: Transaction Authorization Cheat Sheet.
  4. Stripe: Payment Intents, idempotent requests and webhooks.

Frequently asked questions

Where should device intelligence run in a checkout flow?
Collect browser evidence while the customer prepares the checkout, then read its result through an authenticated server API before starting the protected payment operation. Keep the order amount, currency, ownership and payment state on the backend. The browser supplies an Identification ID to correlate the check, not a trusted approval decision.
Is an Identification ID proof that a payment is safe?
No. It identifies a check to retrieve. The backend must confirm the expected domain, account context, observation time and available result, then associate it with the authorized checkout attempt. A usable device result does not establish card ownership, successful authentication, payment authorization or the absence of fraud.
What should checkout do when the device result is unavailable?
Keep unavailable evidence explicit. The reference implementation returns a pending state and does not call the payment adapter. It permits a controlled retry using the same checkout attempt and Identification ID. A production flow should define its timeout, recovery and escalation behavior for the operation rather than substituting a zero risk score.
Can device intelligence replace 3-D Secure or payment-processor checks?
No. Device evidence contributes context about the environment and related activity. The payment provider and issuer still perform their payment and authentication responsibilities. Use the existing PSP integration for authorization and any required challenge, and use verified payment state to determine whether the order may be fulfilled.
How do I prevent a checkout retry from creating another payment?
Persist the authorized order and its payment idempotency key, reuse the same key and parameters for a retry, and reconcile an uncertain provider response. The example also stores a decision per checkout attempt and atomically claims an Identification ID. These controls solve different problems: evidence replay, repeated submissions and duplicate payment operations.
Should a signed fraud webhook mark an order as paid?
No. A verified identification webhook authenticates an evidence notification. It is not a payment-success event. Verify the original body, deduplicate processing and match its domain and identification context. Fulfilment must follow the payment provider's authenticated state and an idempotent order transition, including handling delayed or repeated payment notifications.
Which ShieldLabs SDK versions does the example use?
The reference uses @shieldlabs-ai/js 1.1.0 and @shieldlabs-ai/node 1.2.0. The browser receives identificationId. The server uses entities.get(domainId, 'identifications', identificationId) to retrieve a domain-scoped card and checks its risk and signals availability states explicitly. Keep these method names and package versions together when adapting the example.

Collect browser evidence while the customer prepares the checkout, then read its result through an authenticated server API before starting the protected payment operation. Keep the order amount, currency, ownership and payment state on the backend. The browser supplies an Identification ID to correlate the check, not a trusted approval decision.

No. It identifies a check to retrieve. The backend must confirm the expected domain, account context, observation time and available result, then associate it with the authorized checkout attempt. A usable device result does not establish card ownership, successful authentication, payment authorization or the absence of fraud.

Keep unavailable evidence explicit. The reference implementation returns a pending state and does not call the payment adapter. It permits a controlled retry using the same checkout attempt and Identification ID. A production flow should define its timeout, recovery and escalation behavior for the operation rather than substituting a zero risk score.

No. Device evidence contributes context about the environment and related activity. The payment provider and issuer still perform their payment and authentication responsibilities. Use the existing PSP integration for authorization and any required challenge, and use verified payment state to determine whether the order may be fulfilled.

Persist the authorized order and its payment idempotency key, reuse the same key and parameters for a retry, and reconcile an uncertain provider response. The example also stores a decision per checkout attempt and atomically claims an Identification ID. These controls solve different problems: evidence replay, repeated submissions and duplicate payment operations.

No. A verified identification webhook authenticates an evidence notification. It is not a payment-success event. Verify the original body, deduplicate processing and match its domain and identification context. Fulfilment must follow the payment provider's authenticated state and an idempotent order transition, including handling delayed or repeated payment notifications.

The reference uses @shieldlabs-ai/js 1.1.0 and @shieldlabs-ai/node 1.2.0. The browser receives identificationId. The server uses entities.get(domainId, 'identifications', identificationId) to retrieve a domain-scoped card and checks its risk and signals availability states explicitly. Keep these method names and package versions together when adapting the example.

Related articles