---
title: "Migration Guide: Payment Element → Corgi-Controlled Confirmation"
description: "Add Corgi Model fraud decisioning to your existing Stripe Payment Element integration with one backend change and no checkout UX impact."
url: https://www.corgilabs.ai/docs/corgi-model/migration-guide-payment-element-corgi-controlled-confirmation
updated: 2026-07-08
---

# Migration Guide: Payment Element → Corgi-Controlled Confirmation

If you process payments using Stripe [**Payment Element**](https://docs.stripe.com/payments/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.

|  | Before | After |
|---|---|---|
| Client flow | Mount 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 flow | Create PaymentIntent directly with Stripe | Call Corgi Labs to create the PaymentIntent, evaluate rules, and execute confirmation |
| Risk evaluation | Controlled by Stripe Radar | Controlled 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)

```python
# 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}
```

---

### ✅ After (HTTP API example, recommended)

Create a `CORGI_API_TOKEN` in your Corgi Web App at [app.corgilabs.ai](https://app.corgilabs.ai/). Keep that token on your server only.

```python
# 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

```typescript
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)

```javascript
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)

```javascript
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`.

```bash
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.

```json
{
  "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`.

```typescript
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:

```typescript
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](https://docs.fingerprint.com/docs/fingerprintjs-pro-react-native) and [package README](https://www.npmjs.com/package/@fingerprintjs/fingerprintjs-pro-react-native) 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:

```json
{
  "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.

1. The client collects `fpEventId` and calls `elements.submit()`.

1. The client creates a Stripe `ConfirmationToken`.

1. The client sends `confirmationTokenId` and `fpEventId` to your backend.

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

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

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

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