# 3D Secure using Finix.js

US only
3D Secure is currently supported for US-based merchants only. Canada support is coming soon.

Render the 3D Secure challenge inside your own checkout page.

Finix.js shows the challenge in an in-page modal overlay, so the buyer never leaves your checkout. How you raise it depends on whether you're tokenizing a card at checkout or authenticating one you already have on file.

## Prerequisites

Before you start, make sure you have the following.

### The Finix.js library

Add the library to your checkout page with a script tag:


```html
<script src="https://js.finix.com/v/2/finix.js"></script>
```

### A session endpoint on your backend

Finix.js never creates a 3D Secure Session itself. You need an endpoint on your own backend that calls POST /3d_secure_sessions with your API credentials and returns the response to your page unmodified.

Leave `redirect_url` out of the sessions you create for Finix.js. The challenge renders in your page, so there's nowhere to send the buyer.

Keep credentials and trusted values on your server
Your browser calls this backend endpoint, but it must never receive your Finix API credentials. Authenticate the checkout, derive and validate the merchant and amount on the server, and rate-limit requests to prevent BIN enumeration and forged sessions.

### Your environment and Application ID

The payment-form option requires an Application ID and environment. The standalone `Finix.ThreeDSecure` class takes only the environment. In both flows, the environment must match the credentials your backend uses.

## Option 1 - 3DS authentication when tokenizing a card

Use this option when the buyer enters card details at checkout. Finix.js tokenizes the card, calls your session endpoint, and raises the challenge in one pass.

### Step 1: Add the ThreeDSecure option to your payment form

Add a `ThreeDSecure` object to the options you already pass to [Finix.PaymentForm](/js/payment-form). Its `createSession` hook is the backend endpoint from the prerequisites.


```javascript Checkout page
async function createThreeDSecureSession(token) {
  // Your backend calls POST /3d_secure_sessions with your API credentials.
  const response = await fetch("/checkout/3d-secure-session", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      tokenId: token.id,
      amount: 92169,
      currency: "USD",
    }),
  });

  // Return the session response unmodified.
  return response.json();
}

Finix.PaymentForm("payment-form", "sandbox", "APgPDQrLD52TYvqazjHJJchM", {
  ThreeDSecure: {
    createSession: createThreeDSecureSession,
    onStatusChange: function ({ status }) {
      console.log("3DS status:", status);
    },
  },
  // ...existing options like onSubmit, etc.
});
```

Finix.js calls `createSession` after it generates a token, waits for your endpoint to resolve, then renders the challenge if the issuer asks for one. Return the session unmodified: Finix.js reads `state` from it to decide whether a challenge is possible, and hands the whole object back to you.

`onStatusChange` is optional. It reports `initialized` as an advisory, then exactly one of `succeeded` or `failed`, just before `onSubmit` runs.

### Step 2: Read the result in onSubmit

Your existing [onSubmit](/js/callbacks) callback fires exactly once per submission. It receives the token on `response.data`, plus the authentication result on `response.threeDSecure` as `{ session, status, error }`.


```javascript Checkout page
onSubmit: function (error, response) {
  // This error argument covers tokenization only. It says nothing about 3D Secure.
  if (error) {
    console.error("Tokenization failed:", error);
    return;
  }

  const token = response.data;

  // Absent on bank and Plaid submissions, which bypass the hook entirely.
  const { session, status, error: threeDSecureError } = response.threeDSecure || {};

  if (status === "failed") {
    if (threeDSecureError.code === "CARD_NOT_ENROLLED") {
      // Nothing went wrong. The card simply cannot be authenticated.
      console.log("No challenge possible:", threeDSecureError.reason);
    } else {
      console.error(`3DS failed [${threeDSecureError.code}]:`, threeDSecureError.reason);
      return;
    }
  }

  // Charge the card from your server.
  fetch("/checkout/pay", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
        tokenId: token.id,
        sessionId: status === "succeeded" ? session.id : undefined,
        status,
      }),
  });
},
```

A null error does not mean the card was authenticated
The `error` argument reports tokenization failures only. Read `response.threeDSecure.status` before you charge, or you'll charge cards that failed the challenge.

Two cases deserve their own handling:

- `CARD_NOT_ENROLLED` means no challenge was possible, not that something broke. You can still charge the card, but the payment is unauthenticated and liability stays with you. Gate on this if your integration requires 3D Secure.
- Bank and Plaid submissions never call your hook, so `response.threeDSecure` is absent. Guard for it rather than assuming it's there.


Branch on `status`, never on `session.state`. The session updates asynchronously on the server and still reads `PENDING` when your callback runs. For every `error.code` value, see [Error codes](/js/three-d-secure#error-codes).

### Step 3: Run the sale or Authorization

On your backend, convert the token from `response.data.id` to a Payment Instrument by calling `POST /payment_instruments` with `type: TOKEN` and the token ID. Use the resulting Payment Instrument ID as `source`, and reference the session as `3d_secure_session` on the Transfer or Authorization. Finix sends the authentication values to the card network with the payment.


```shell Transfer with a 3D Secure Session
curl https://finix.sandbox-payments-api.com/transfers \
    -H "Content-Type: application/json" \
    -H 'Finix-Version: 2022-02-01' \
    -u USsRhsHYZGBPnQw8CByJyEQW:8a14c2f9-d94b-4c72-8f5c-a62908e5b30e \
    -d '{
        "amount": 92169,
        "currency": "USD",
        "merchant": "MUmfEGv5bMpSJ9k5TFRUjkmm",
        "source": "PI6iQcTtJNCS8GZAVKYi5Ueb",
        "3d_secure_session": "3d_secure_session_9hK2mNp4T761RBv8qwLDYc"
    }'
```

Use `succeeded` to trigger payment submission on the client. On your server, verify the authentication outcome by fetching the session or confirming via webhook before charging — don't rely on the client-supplied status alone.

Don't send both 3D Secure fields
`3d_secure_session` and `3d_secure_authentication` are mutually exclusive. Use `3d_secure_session` when Finix runs the authentication, and `3d_secure_authentication` only when you authenticate with [your own vendor](/guides/online-payments/fraud-and-risk/3d-secure/passthrough).

## Option 2 - 3DS authentication when a card is already tokenized

Use this option when there's no Finix payment form on the page. That covers charging a Payment Instrument you already have on file, and checkouts that collect the card number in your own PCI-compliant form.

Here your backend creates the session before the challenge runs, because there's no tokenization step to trigger the hook.

### Step 1: Create the session on your server

Call your session endpoint with the Payment Instrument you're about to charge, and return the session to the page. The amount must match what you charge in Step 3.


```javascript Checkout page
const response = await fetch("/checkout/3d-secure-session", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    amount: 92169,
    currency: "USD",
    paymentInstrument: "PI6iQcTtJNCS8GZAVKYi5Ueb",
  }),
});

const session = await response.json();
```

### Step 2: Raise the challenge

Pass the whole session object to `validate`, and listen for the outcome.


```javascript Checkout page
Finix.ThreeDSecure("sandbox")
  .validate(session)
  .on("succeeded", function ({ session }) {
    // Charge the card from your server.
    fetch("/checkout/pay", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        paymentInstrument: "PI6iQcTtJNCS8GZAVKYi5Ueb",
        sessionId: session.id,
      }),
    });
  })
  .on("failed", function ({ error }) {
    console.error(`3DS failed [${error.code}]:`, error.reason);
  });
```

Pass the session through whole rather than picking fields out of it. The `environment` argument takes the same values as the rest of Finix.js, and must match the environment your API credentials belong to. Register a `failed` listener to handle a buyer who cancels the overlay or an issuer that declines the authentication.

### Step 3: Run the sale or Authorization

Reference the session the same way as in Option 1, using the saved Payment Instrument as the `source`.


```shell Transfer with a 3D Secure Session
curl https://finix.sandbox-payments-api.com/transfers \
    -H "Content-Type: application/json" \
    -H 'Finix-Version: 2022-02-01' \
    -u USsRhsHYZGBPnQw8CByJyEQW:8a14c2f9-d94b-4c72-8f5c-a62908e5b30e \
    -d '{
        "amount": 92169,
        "currency": "USD",
        "merchant": "MUmfEGv5bMpSJ9k5TFRUjkmm",
        "source": "PI6iQcTtJNCS8GZAVKYi5Ueb",
        "3d_secure_session": "3d_secure_session_9hK2mNp4T761RBv8qwLDYc"
    }'
```

The same two rules apply. Submit when the success listener fires rather than waiting for `state` to reach `SUCCEEDED`, and never send `3d_secure_session` and `3d_secure_authentication` together.

## Next steps

Finix.ThreeDSecure
Every method, option, event, and error code for 3D Secure in Finix.js.

3D Secure Authentication
How 3D Secure works, session webhooks, and best practices.