> ## Documentation Index
> Fetch the complete documentation index at: https://usefoil.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Checkout & payment

> Detect card testing and bot-driven checkout fraud. Feed Foil verdicts and manipulation scores into your fraud stack without slowing real purchases.

<Info>
  Checkout is a high-friction action with a real revenue cost for false positives. Treat Foil as one signal alongside your existing fraud stack (Stripe Radar, Sift, a custom rules engine) rather than a standalone blocker. The most useful field on this surface isn't the top-level verdict — it's `decision.manipulation`.
</Info>

## The threat

Payment fraud at checkout comes in a few shapes, and Foil is relevant to a subset:

* **Card testing.** Script runs a stolen card list against a cheap SKU, looking for valid cards. This is high-volume and very automation-shaped — Foil catches it well.
* **Account-based carding.** Attacker takes over an account with a saved card and checks out from a real browser. Account takeover is a login problem; see [Login & credential stuffing](/use-cases/login). Once they're logged in and paying, Foil mostly contributes `manipulation` evidence rather than a bot verdict.
* **Reshipping / mule fraud.** A real human at a real keyboard using a stolen card. Foil will return `human` and that's correct — this is a chargeback and identity problem, not a bot problem.

Integration strategy: pass Foil evidence into the same decision that already consumes your payment processor's risk score. Don't block on Foil alone unless you're seeing `decision.verdict === "bot"` on a checkout, which is specifically a card-testing signal.

## The flow

<Steps>
  <Step title="Start Foil when the cart page loads">
    Users often land on checkout directly from email or a deep link, so start as early as you can in the journey.
  </Step>

  <Step title="Best-effort waitForFingerprint()">
    Gate submission on it if you can, but don't hang the UI forever — users arriving hot should still be able to pay.
  </Step>

  <Step title="Call getSession() at payment submit">
    Right before the PaymentIntent confirm (or equivalent) fires.
  </Step>

  <Step title="Verify and extract fraud-relevant fields">
    Pull `decision.verdict`, `decision.risk_score`, and `decision.manipulation.verdict` before the payment gateway call.
  </Step>

  <Step title="Attach metadata to the payment">
    Put `sessionId` on the PaymentIntent so your fraud team can correlate Foil and processor evidence later.
  </Step>
</Steps>

## Client integration

Race `waitForFingerprint()` with a short deadline so slow or failed fingerprinting doesn't block a sale.

```html theme={"dark"}
<script type="module">
  const foilPromise = import("https://cdn.usefoil.com/t.js").then(
    (Foil) =>
      Foil.start({
        publishableKey: "pk_live_your_publishable_key",
      }),
  );

  async function waitForFingerprintWithTimeout(foil, ms) {
    return Promise.race([
      foil.waitForFingerprint(),
      new Promise((resolve) => setTimeout(resolve, ms)),
    ]);
  }

  async function submitPayment(formData) {
    const foil = await foilPromise;
    await waitForFingerprintWithTimeout(foil, 2000);

    const { sessionId, sealedToken } = await foil.getSession();

    return fetch("/api/checkout", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        ...formData,
        foil: { sessionId, sealedToken },
      }),
    });
  }
</script>
```

If the timeout fires before fingerprint freeze, `getSession()` still returns a handoff — the verified result just won't carry a durable `visitor_fingerprint`, and the score will lean more on environment and behavioral signals. That's a worse input than a fully-frozen session, but it's still useful, and it's vastly better than failing the checkout.

## Server verification and fraud-stack handoff

The example below uses Stripe. The pattern is identical with any processor: compute Foil's signal, pass it in as metadata alongside the payment, and let your fraud rules engine combine it with the processor's own risk score.

<CodeGroup>
  ```javascript Node.js theme={"dark"}
  const Stripe = require("stripe");
  const { safeVerifyFoilToken } = require("@abxy/foil-server");
  const stripe = Stripe(process.env.STRIPE_SECRET_KEY);

  app.post("/api/checkout", async (req, res) => {
    const result = safeVerifyFoilToken(
      req.body.foil.sealedToken,
      process.env.FOIL_SECRET_KEY,
    );

    // Card testing is near-100% automation. A bot verdict here is almost
    // always right — block before touching the payment processor.
    if (result.ok && result.data.decision.verdict === "bot") {
      return res.status(403).json({ error: "Payment declined" });
    }

    // Everything else goes to the processor with Foil signal attached.
    const decision = result.ok ? result.data.decision : null;
    const intent = await stripe.paymentIntents.create({
      amount: req.body.amountCents,
      currency: "usd",
      payment_method: req.body.paymentMethodId,
      confirm: true,
      metadata: {
        foil_session_id: req.body.foil.sessionId,
        foil_verdict: decision?.verdict ?? "unavailable",
        foil_risk_score: decision ? String(decision.risk_score) : "unavailable",
        foil_manipulation: decision?.manipulation?.verdict ?? "unavailable",
      },
    });

    res.json({ intent });
  });
  ```

  ```python Python theme={"dark"}
  import stripe
  from foil_server import safe_verify_foil_token
  import os

  stripe.api_key = os.environ["STRIPE_SECRET_KEY"]

  @app.post("/api/checkout")
  def checkout(request):
      result = safe_verify_foil_token(
          request.json["foil"]["sealedToken"],
          os.environ["FOIL_SECRET_KEY"],
      )

      if result.ok and result.data.decision.verdict == "bot":
          return {"error": "Payment declined"}, 403

      decision = result.data.decision if result.ok else None
      intent = stripe.PaymentIntent.create(
          amount=request.json["amountCents"],
          currency="usd",
          payment_method=request.json["paymentMethodId"],
          confirm=True,
          metadata={
              "foil_session_id": request.json["foil"]["sessionId"],
              "foil_verdict": decision.verdict if decision else "unavailable",
              "foil_risk_score": str(decision.risk_score) if decision else "unavailable",
              "foil_manipulation": decision.manipulation.verdict if decision and decision.manipulation else "unavailable",
          },
      )
      return {"intent": intent}
  ```

  ```go Go theme={"dark"}
  import (
      foil "github.com/abxy-labs/foil-server-go"
      "github.com/stripe/stripe-go/v79"
  )

  func checkoutHandler(w http.ResponseWriter, r *http.Request) {
      var body CheckoutBody
      json.NewDecoder(r.Body).Decode(&body)

      tr := foil.SafeVerifyFoilToken(
          body.Foil.SealedToken,
          os.Getenv("FOIL_SECRET_KEY"),
      )

      if tr.OK && tr.Data.Decision.Verdict == "bot" {
          http.Error(w, "Payment declined", 403)
          return
      }

      params := &stripe.PaymentIntentParams{
          Amount:        stripe.Int64(body.AmountCents),
          Currency:      stripe.String("usd"),
          PaymentMethod: stripe.String(body.PaymentMethodID),
          Confirm:       stripe.Bool(true),
      }
      params.AddMetadata("foil_session_id", body.Foil.SessionID)
      if tr.OK {
          params.AddMetadata("foil_verdict", tr.Data.Decision.Verdict)
          params.AddMetadata("foil_risk_score", strconv.Itoa(tr.Data.Decision.RiskScore))
      }
      intent, _ := paymentintent.New(params)

      json.NewEncoder(w).Encode(map[string]any{"intent": intent})
  }
  ```

  ```ruby Ruby theme={"dark"}
  require "stripe"
  require "foil/server"

  Stripe.api_key = ENV["STRIPE_SECRET_KEY"]

  post "/api/checkout" do
    body = JSON.parse(request.body.read)
    tr = Foil::Server::SealedToken.safe_verify_foil_token(
      body.dig("foil", "sealedToken"),
    )

    decision = tr[:ok] ? tr[:data][:decision] : nil
    halt 403, { error: "Payment declined" }.to_json if decision && decision[:verdict] == "bot"

    intent = Stripe::PaymentIntent.create(
      amount: body["amountCents"],
      currency: "usd",
      payment_method: body["paymentMethodId"],
      confirm: true,
      metadata: {
        foil_session_id: body.dig("foil", "sessionId"),
        foil_verdict: decision ? decision[:verdict] : "unavailable",
        foil_risk_score: decision ? decision[:risk_score].to_s : "unavailable",
        foil_manipulation: decision && decision[:manipulation] ? decision[:manipulation][:verdict] : "unavailable",
      },
    )
    { intent: intent }.to_json
  end
  ```

  ```php PHP theme={"dark"}
  use Foil\Server\SealedToken;

  \Stripe\Stripe::setApiKey(getenv("STRIPE_SECRET_KEY"));

  $body = json_decode(file_get_contents("php://input"), true);
  $tr = SealedToken::safeVerify(
      $body["foil"]["sealedToken"],
      getenv("FOIL_SECRET_KEY"),
  );

  if ($tr->ok && $tr->data->decision["verdict"] === "bot") {
      http_response_code(403);
      echo json_encode(["error" => "Payment declined"]);
      exit;
  }

  $decision = $tr->ok ? $tr->data->decision : null;
  $intent = \Stripe\PaymentIntent::create([
      "amount"         => $body["amountCents"],
      "currency"       => "usd",
      "payment_method" => $body["paymentMethodId"],
      "confirm"        => true,
      "metadata" => [
          "foil_session_id"   => $body["foil"]["sessionId"],
          "foil_verdict"      => $decision["verdict"] ?? "unavailable",
          "foil_risk_score"   => isset($decision) ? (string) $decision["risk_score"] : "unavailable",
          "foil_manipulation" => $decision["manipulation"]["verdict"] ?? "unavailable",
      ],
  ]);
  echo json_encode(["intent" => $intent]);
  ```

  ```bash cURL theme={"dark"}
  # Durable readback — useful when reconciling chargebacks after the fact.
  curl https://api.usefoil.com/v1/sessions/sid_... \
    -H "Authorization: Bearer sk_live_..."
  ```
</CodeGroup>

## The manipulation score

`decision.manipulation` is distinct from the top-level verdict and often more relevant at checkout.

| Field                           | Type                                            | Meaning                                                                                                                                                   |
| ------------------------------- | ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `decision.manipulation.score`   | `integer \| null`                               | `0`–`100` measure of active environment tampering (spoofed user agent, patched navigator properties, canvas/audio noise injection, anti-detect browsers). |
| `decision.manipulation.verdict` | `"none" \| "low" \| "medium" \| "high" \| null` | Bucketed version of the score.                                                                                                                            |

A real customer with a legitimate card has `manipulation.verdict === "none"`. A sophisticated fraudster using an anti-detect browser (Multilogin, GoLogin) to make the same account look like a new device on every checkout will score `medium` or `high` — even when they pass `decision.verdict === "human"` because the behavioral signals look real.

Feed both into your rules engine. A reasonable starter policy:

* `decision.verdict === "bot"` → decline.
* `decision.manipulation.verdict === "high"` → always send to manual review.
* `decision.manipulation.verdict === "medium"` + processor risk elevated → decline or step up to 3-D Secure.
* Otherwise → follow the processor's decision.

## Correlating later

Putting `foil_session_id` (and the verdict snapshot) on the PaymentIntent pays off when you investigate a chargeback weeks later. Stripe's metadata surfaces in the dashboard, and you can pull the durable Foil session back via `GET /v1/sessions/:sessionId` with your secret key to recover the full fingerprint, attribution, and signal list. See [Server verification](/server-verification) for the readback shape.

## What's next

<CardGroup cols={2}>
  <Card title="Login protection" icon="key-round" href="/use-cases/login">
    Stop credential stuffing before it reaches checkout.
  </Card>

  <Card title="Signup protection" icon="user-plus" href="/use-cases/signup">
    Block account-based carding at the account-creation step.
  </Card>

  <Card title="Verdicts & scoring" icon="gauge" href="/verdicts-and-scoring">
    Deep-dive on `risk_score` and `manipulation`.
  </Card>

  <Card title="Going to production" icon="rocket" href="/going-to-production">
    Rollout plan for enforcement on a revenue-critical surface.
  </Card>
</CardGroup>
