Migration Guide: Payment Element → Corgi-Controlled Confirmation

Migration Guide: Payment Element → Corgi-Controlled Confirmation

Add Corgi Model fraud decisioning to your existing Stripe Payment Element integration with one backend change and no checkout UX impact.

On this page

If you process payments using Stripe Payment Element with PaymentIntent, you can start using Corgi Model or Corgi Rule Engine by migrating to Corgi-Controlled PaymentIntent Confirmation. This enables deterministic Request 3DS, Allow, Block, and Review decisions enforced by Corgi Labs before payment confirmation, without changing your checkout UX.

Changes at a high level

You will make one flow change. Instead of having Stripe's standard client confirmation flow finalize the payment, you collect payment details in your existing Payment Element, create a Stripe ConfirmationToken, send it to your backend, and let your server call Corgi Labs' confirm-payment endpoint.

Your checkout UX remains largely unchanged, but the confirmation control is moved to your server. Your customers should not experience any noticeable changes.

BeforeAfter
Client flowMount Payment Element, then call stripe.confirmPayment(...)Mount Payment Element, collect a Fingerprint event, call elements.submit(), create a ConfirmationToken, then call your backend finalize endpoint with confirmationTokenId and fpEventId
Server flowCreate PaymentIntent directly with StripeCall Corgi Labs to create the PaymentIntent, evaluate rules, and execute confirmation
Risk evaluationControlled by Stripe RadarControlled by Corgi Model and Corgi Rule Engine

Migrate to Corgi-Controlled Confirmation

Step 1 — Replace direct PaymentIntent creation with server finalization

Allow Corgi Labs to evaluate the payment during the server-finalize flow. This is the main backend change.

❌ Before (direct Stripe call)

# server.py
pi = stripe.PaymentIntent.create(
    amount=5000,
    currency="usd",
    automatic_payment_methods={"enabled": True},
    metadata={"order_id": "123"},
)

return {"client_secret": pi.client_secret}

Create a CORGI_API_TOKEN in your Corgi Web App at app.corgilabs.ai. Keep that token on your server only.

# server.py
import os
import requests

CORGI_API_TOKEN = os.environ["CORGI_API_TOKEN"]

# /api/finalize-payment
def finalize_payment(confirmation_token_id, fp_event_id):
    response = requests.post(
        "https://app.corgilabs.ai/api/v1/corgi-sdk/confirm-payment",
        headers={
            "Authorization": f"Bearer {CORGI_API_TOKEN}",
            "Content-Type": "application/json",
        },
        json={
            "confirmationTokenId": confirmation_token_id,
            "paymentIntentParams": {
                "amount": 5000,
                "currency": "usd",
                "metadata": {
                    "order_id": "123",
                    "fp_event_id": fp_event_id,
                }
            }
        },
        timeout=15,
    )
    response.raise_for_status()
    return response.json()

Optional: SDK (Node.js) example

import { CorgiSDK } from 'corgi-sdk';

const corgi = new CorgiSDK({
  token: process.env.CORGI_API_TOKEN,
});

// /api/finalize-payment
export async function finalizePayment(
  confirmationTokenId: string,
  fpEventId: string,
) {
  return await corgi.confirmPayment(confirmationTokenId, {
    amount: 5000,
    currency: 'usd',
    metadata: {
      order_id: '<order_id>',
      fp_event_id: fpEventId,
    },
  });
}

Step 2 — Change the frontend to create a Stripe ConfirmationToken

Move the client from Stripe-controlled confirmation to token creation plus server finalization. The checkout UI remains the same, but the submit path changes.

Your frontend should also embed Fingerprint.js, collect a Fingerprint event ID for each checkout attempt, and send that fpEventId to your backend. Your backend should persist that value to PaymentIntent metadata as fp_event_id during the Corgi confirm call. This lets Corgi retrieve IP address, user agent, and related request/device context.

❌ Before (client confirms directly with Stripe)

const stripe = Stripe(PUBLISHABLE_KEY);
const elements = stripe.elements({
  mode: "payment",
  currency: "usd",
  amount: 5000,
  captureMethod: 'manual',
  paymentMethodCreation: 'manual',
});

const paymentElement = elements.create("payment");
paymentElement.mount("#payment-element");

const { error } = await stripe.confirmPayment({
  elements,
  confirmParams: {
    return_url: "<your return url>",
  },
});

if (error) {
  window.alert(error.message);
}

✅ After (client creates ConfirmationToken, server finalizes)

import * as Fingerprint from '@fingerprint/agent';

const stripe = Stripe(PUBLISHABLE_KEY);
const elements = stripe.elements({
  mode: 'payment',
  currency: 'usd',
  amount: 5000,
});

const paymentElement = elements.create('payment');
paymentElement.mount('#payment-element');

const submitResult = await elements.submit();
if (submitResult.error) {
  window.alert(submitResult.error.message);
  return;
}

const tokenResult = await stripe.createConfirmationToken({
  elements,
  params: {
    payment_method_data: {
      billing_details: {
        name: '<name>',
        email: '<email>',
      },
    },
    return_url: '<your return url>',
  },
});

if (tokenResult.error) {
  window.alert(tokenResult.error.message);
  return;
}

const fp = Fingerprint.start({
  apiKey: 'CXTnbDDa5sn49p2NSHNm',
  endpoints: "https://metrics.corgilabs.ai"
});
const fpResult = await fp.get();
const fpEventId = fpResult.event_id;

const result = await fetch('/api/finalize-payment', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    confirmationTokenId: tokenResult.confirmationToken.id,
    fpEventId,
  }),
}).then((res) => res.json());

if (result.action === 'block') {
  window.alert('Your card was declined.');
  return;
}

if (result.action === 'request_3ds' || result.status === 'requires_action') {
  const { error } = await stripe.handleNextAction({
    clientSecret: result.clientSecret,
  });

  if (error) {
    window.alert('We are unable to authenticate your payment method. Please choose a different payment method and try again');
    return;
  }
}

Expo / React Native Fingerprint event ID

For mobile checkout, keep the backend payload unchanged. The React Native SDK returns the per-identification event ID as requestId; send that value as fpEventId.

pnpm add @fingerprintjs/fingerprintjs-pro-react-native

For Expo, add the native config plugin and use a native build or custom dev client. Expo Go is not supported because the SDK uses native code.

{
  "expo": {
    "plugins": ["@fingerprintjs/fingerprintjs-pro-react-native"]
  }
}

Wrap the app with the Fingerprint provider using Corgi's public API key and metrics endpoint. In the React Native SDK, this endpoint config is endpointUrl.

import { FingerprintJsProProvider } from '@fingerprintjs/fingerprintjs-pro-react-native';

export function AppRoot() {
  return (
    <FingerprintJsProProvider
      apiKey="CXTnbDDa5sn49p2NSHNm"
      endpointUrl="https://metrics.corgilabs.ai"
    >
      <App />
    </FingerprintJsProProvider>
  );
}

In the mobile checkout submit path, collect requestId and pass it to the same finalize endpoint used by web:

import { useVisitorData } from '@fingerprintjs/fingerprintjs-pro-react-native';

const { getData } = useVisitorData();

const visitorData = await getData();
const fpEventId = visitorData.requestId;

await fetch('/api/finalize-payment', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    confirmationTokenId,
    fpEventId,
  }),
});

No backend change is needed. Continue saving the value as paymentIntentParams.metadata.fp_event_id during the Corgi confirm call.

See Fingerprint's React Native SDK docs and package README for native setup details.

Step 3 — Optional: Add metadata for better decisions

In addition to the required fp_event_id field used for device and request context, you can pass any metadata you already have. For example:

{
  "metadata": {
    "user_id": "u_456",
    "loyalty_tier": "gold"
  }
}

Corgi Labs automatically ingests the custom metadata. These fields can be used for Corgi Model evaluation and Rule Management immediately.

Step 4 — Optional: Subscribe to webhooks

We recommend subscribing to the following events:

  • payment_intent.succeeded

  • payment_intent.payment_failed

If you already listen to Stripe PaymentIntent events, no changes are required.


Handle payments with Corgi-Controlled Confirmation

For each checkout attempt routed to Corgi after the migration:

  1. The client mounts Payment Element and initializes Fingerprint.js.

  2. The client collects fpEventId and calls elements.submit().

  3. The client creates a Stripe ConfirmationToken.

  4. The client sends confirmationTokenId and fpEventId to your backend.

  5. Your backend calls Corgi's confirm-payment endpoint or SDK, creates the PaymentIntent, and saves fp_event_id in metadata.

  6. Corgi Labs creates the draft PaymentIntent, evaluates rules, and executes the confirmation flow.

  7. Corgi Labs returns a result such as allow, block, request_3ds, or review, along with Stripe status fields.

  8. If Stripe requires customer authentication, the client calls stripe.handleNextAction(...) using the returned clientSecret.


API response shape

confirmPayment(confirmationTokenId, paymentIntentParams) returns:

  • action: allow, block, none, request_3ds, or review

  • matchedRuleId: the matched rule ID, if any

  • request3ds: whether Corgi forced 3DS on the underlying PaymentIntent confirmation

  • paymentIntentId: the Stripe PaymentIntent ID created by Corgi

  • status: the latest Stripe PaymentIntent status

  • clientSecret: the Stripe client secret when Stripe returns it

Pass the intended Stripe PaymentIntentCreateParams as the second argument when using the SDK. Corgi uses those parameters to create the draft PaymentIntent before rule evaluation and confirmation.

Last updated: 2026-07-08