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

# 3D Secure

> Add an extra layer of protection to card payments

## What is 3D Secure?

3D Secure adds an extra layer of protection to card payments. Like two-factor authentication for your bank account, it helps prevent fraud and protects your customers.

3DS (3D Secure) verifies that your customer is the legitimate cardholder. When triggered, customers verify their identity using:

<CardGroup cols={2}>
  <Card title="SMS Verification" icon="message">
    One-time code sent via text message
  </Card>

  <Card title="Biometric Auth" icon="fingerprint">
    Face ID, fingerprint, or other biometrics
  </Card>

  <Card title="Banking App" icon="mobile-screen">
    Confirmation through bank's mobile app
  </Card>

  <Card title="Security Questions" icon="question">
    Personal security questions or PINs
  </Card>
</CardGroup>

<Note>
  The authentication method is determined by the issuing bank (and region). It isn’t selected by Cheqpay or the merchant, and it may vary per transaction.
</Note>

<Tip>
  Most payments (60-80%) complete automatically without showing a challenge to your customer. Cheqpay handles the verification in the background.
</Tip>

## When Does 3DS Trigger?

3DS is automatically activated for:

* **High-risk transactions** - Based on fraud scoring
* **Large payment amounts** - Above certain thresholds
* **International cards** - Cards issued outside Mexico
* **Bank requirements** - Issuing bank policies
* **Regulatory compliance** - PSD2 in Europe and similar regulations

<Note>
  You don't need to decide when to use 3DS - we handle it automatically based on risk assessment and regulations.
</Note>

## How 3D Secure Works

<Steps>
  <Step title="Create Order">
    Send your payment request as usual to [`POST /v2/payment-orders`](/features/payment-orders)
  </Step>

  <Step title="Check Order Response">
    If 3DS is required, status will be `PAYER_AUTHENTICATION_DEVICE_DATA_REQUIRED`.
  </Step>

  <Step title="Collect and Submit Device Data">
    Display an invisible iframe to collect device data and send the data to `POST /v2/payment-orders/{id}/payer-authentication`
  </Step>

  <Step title="Check Authentication Response">
    If further challenge is needed, status will be `PAYER_AUTHENTICATION_CHALLENGE_REQUIRED`.
  </Step>

  <Step title="Display Challenge">
    Display an iframe for the customer to complete the 3DS challenge.
  </Step>

  <Step title="Validate">
    Once the customer completes challenge, call `POST /v2/payment-orders/{id}/payer-authentication/validate` to complete the payment.
  </Step>
</Steps>

## Implementation

### 1. Create Order

Send a normal payment order request to `POST /v2/payment-orders`.

<Accordion title="Create Payment Order Request Example" icon="terminal">
  ```bash theme={null}
  curl -X POST https://api.sandbox.cheqpay.mx/pos/v2/payment-orders \
    -H "x-api-key: YOUR_API_KEY" \
    -H "x-merchant-id: YOUR_MERCHANT_ID" \
    -H "Content-Type: application/json" \
    -d '{
      "externalId": 'order-123',
      "customer": {
        "email": "ratchet@example.com",
        "firstName": "Ratchet Lombax"
      },
      "paymentMethod": {
        "type": "card",
        "cardDetails": {
          "number": "4000000000002503",
          "expiryMonth": "05",
          "expiryYear": "28",
          "cvc": "110"
        },
        "persist": true
      },
      "amount": 100000,
      "currency": "MXN",
      "description": "Payment order example",
      "billingAddress": {
        "address": "Calle Monte de Piedad 11-Local A y B",
        "city": "Mexico City",
        "state": "CDMX",
        "postalCode": "06000",
        "country": "MX"
      }
    }'
  ```
</Accordion>

<Warning>
  `amount` unit is the smallest currency unit (e.g., cents). So for MXN, 100 MXN = 10000 cents
</Warning>

### 2. Check Order Response

If device data is required, you'll receive a response with status `PAYER_AUTHENTICATION_DEVICE_DATA_REQUIRED` and `payerAuthentication` field that will be used in the next step.

<Accordion title="Create Payment Order Response Example" icon="brackets-curly">
  ```json theme={null}
  {
    "id": "01278468-f2ef-4a46-bb41-c03188e12783",
    "orderNumber": "C2510314",
    "externalId": "order012",
    "amount": 1000000,
    "currency": "MXN",
    "status": "PAYER_AUTHENTICATION_DEVICE_DATA_REQUIRED",
    "description": "Test payment order with card",
    "createdAt": "2025-10-31T15:01:58.485Z",
    "paymentMethod": {
        "type": "CARD",
        "id": "b6ebfaa9-c8ae-4629-af14-7206971bd625",
        "cardDetails": {
            "id": "ae524690-3055-4ee0-93bc-650b9c55158e",
            "bin": "400000",
            "last4": "2503",
            "brand": "VISA",
            "type": "CREDIT",
            "country": "UNITED STATES",
            "issuerBank": "INTL HDQTRS-CENTER OWNED",
            "expiryMonth": "05",
            "expiryYear": "28"
        }
    },
    "customer": {
        "id": "192e4cbf-63d3-4d87-9bca-56eabf2cea3c",
        "firstName": "Ratchat Lombax",
        "email": "ratchet@example.com"
    },
    "payerAuthentication": {
        "id": "a0c76393-0505-4721-81de-f605180e0eb8",
        "url": "https://centinelapistag.cardinalcommerce.com/V1/Cruise/Collect",
        "jwt": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiI2NTU3ZmI2NS0zZDExLTRiYWMtOTZmMS05MjZjNWFkNjY1MzkiLCJpYXQiOjE3NjE5MjI5MTksImlzcyI6IjVkZDgzYmYwMGU0MjNkMTQ5OGRjYmFjYSIsImV4cCI6MTc2MTkyNjUxOSwiT3JnVW5pdElkIjoiNjg3NmRlZDA1YWExYzYxNWI3MTg5ZGUyIiwiUmVmZXJlbmNlSWQiOiJhMGM3NjM5My0wNTA1LTQ3MjEtODFkZS1mNjA1MTgwZTBlYjgifQ.7udCNTQ2XIpvbCrFvr6XF64nBTJOLI5S9oBcGD7WkEo"
    }
  }
  ```
</Accordion>

<Tip>When 3DS is not required, the status will be `COMPLETED`</Tip>

### 3 Collect and Submit Device Data

#### 3.1 Collect Device Data

Using the `payerAuthentication.url` and `payerAuthentication.jwt` from the response, create an invisible iframe to collect device data.

When the data collection is complete, you'll receive a callback event. `event.data` is a JSON containing the `SessionId` needed for the next step.

Here are examples using plain HTML and JavaScript, as well as a React component version.

<AccordionGroup>
  <Accordion title="Collect Device Data HTML Example" icon="code">
    ```html index.html theme={null}
      <!-- Hidden iframe for device data collection -->
      <iframe
        id="cardinal_collection_iframe"
        name="collectionIframe"
        height="10"
        width="10"
        style="display: none;">
      </iframe>

      <!-- Form to submit JWT to Cardinal Commerce -->
      <form
        id="cardinal_collection_form"
        method="POST"
        target="collectionIframe">
        <input type="hidden" name="JWT" value="{jwt-from-response}" />
      </form>

      <script>
        // Set the action URL from the API response
        document.getElementById('cardinal_collection_form').action = '{url-from-response}';

        // Submit the form on page load to begin device data collection
        window.onload = function() {
          var form = document.querySelector('#cardinal_collection_form');
          if (form) {
            form.submit();
          }
        };

        // Listen for completion callback from Cardinal Commerce
        window.addEventListener("message", function(event) {
          // Verify the message is from Cardinal Commerce
          if (
              event.origin === "https://centinelapistag.cardinalcommerce.com" ||
              event.origin === "https://centinelapi.cardinalcommerce.com"
          ) {
            console.log(
              "Device data collection completed. SessionId:",
              JSON.parse(event.data).SessionId
            );
            // Proceed with next step: POST /v2/payment-orders/{id}/payer-authentication
          }
        }, false);
      </script>
    ```
  </Accordion>

  <Accordion title="Collect Device Data React Example" icon="react">
    ```jsx DeviceDataCollector.jsx theme={null}
      import { useEffect, useRef } from 'react';

      function DeviceDataCollector({ jwt, url, onComplete }) {
        const formRef = useRef(null);

        useEffect(() => {
          // Submit form when JWT and URL are available
          if (formRef.current && jwt && url) {
            formRef.current.action = url;
            formRef.current.submit();
          }

          // Listen for completion callback from Cardinal Commerce
          const handleMessage = (event) => {
            // Verify the message is from Cardinal Commerce
            if (event.origin === "https://centinelapistag.cardinalcommerce.com") {
              console.log("Device data collection completed:", event.data);
              if (onComplete) {
                onComplete(JSON.parse(event.data));
              }
            }
          };

          window.addEventListener("message", handleMessage);

          // Cleanup listener on unmount
          return () => {
            window.removeEventListener("message", handleMessage);
          };
        }, [jwt, url, onComplete]);

        return (
          <>
            {/* Hidden iframe for device data collection */}
            <iframe
              id="cardinal_collection_iframe"
              name="collectionIframe"
              height="10"
              width="10"
              style={{ display: 'none' }}
            />
            {/* Form to submit JWT to Cardinal Commerce */}
            <form
              ref={formRef}
              id="cardinal_collection_form"
              method="POST"
              target="collectionIframe"
            >
              <input type="hidden" name="JWT" value={jwt} />
            </form>
          </>
        );
      }

      export default DeviceDataCollector;
    ```
  </Accordion>
</AccordionGroup>

#### 3.2 Submit Device Data

Once you receive the `SessionId` from the iframe callback, submit it to Cheqpay API passing it through `collectionReferenceId` field via `POST /v2/payment-orders/:id/payer-authentication`.

<Accordion title="Submit Device Data Reequest Example" icon="terminal">
  ```bash theme={null}
    curl --location '{HOST}/pos/v2/payment-orders/3737a6ec-1068-4948-94b8-cf10246de080/payer-authentication' \
    --header 'Content-Type: application/json' \
    --header 'x-api-key: {API_KEY}' \
    --header 'x-merchant-id: {MERCHANT_ID}' \
    --data '{
        "deviceInformation": {
            "ipAddress": "185.189.25.120",
            "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36",
            "userAgentBrowserValue": "Chrome",
            "httpAcceptBrowserValue": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
            "httpBrowserLanguage": "en-US,en;q=0.9",
            "httpBrowserJavaScriptEnabled": true,
            "httpBrowserScreenWidth": "1920",
            "httpBrowserScreenHeight": "1080"
        },
        "collectionReferenceId": "75df3df1-06cd-4a7d-a2ce-5e0232081105",
        "returnUrl": "https://your-website.com/3ds-return"
    }'
  ```
</Accordion>

### 4. check Authentication Response

Check the status in the response. If it's `PAYER_AUTHENTICATION_CHALLENGE_REQUIRED`, proceed to display the challenge.

<Accordion title="Submit Device Data Response Example" icon="brackets-curly">
  ```json theme={null}
    {
      "id": "01278468-f2ef-4a46-bb41-c03188e12783",
      "orderNumber": "C2510314",
      "externalId": "order012",
      "customerId": "192e4cbf-63d3-4d87-9bca-56eabf2cea3c",
      "merchantId": "merchant_4324",
      "paymentMethodId": "b6ebfaa9-c8ae-4629-af14-7206971bd625",
      "amount": 1000000,
      "currency": "MXN",
      "status": "PAYER_AUTHENTICATION_CHALLENGE_REQUIRED",
      "description": "Test payment order with card",
      "merchantReference": "order012",
      "createdAt": "2025-10-31T15:01:58.485Z",
      "updatedAt": "2025-10-31T18:36:58.090Z",
      "customer": {
        "id": "192e4cbf-63d3-4d87-9bca-56eabf2cea3c",
        "externalId": null,
        "merchantId": null,
        "firstName": "Ratchat Lombax",
        "lastName": null,
        "phoneNumber": null,
        "email": "ratchet@example.com",
        "active": true,
        "createdAt": "2025-10-31T15:00:07.047Z",
        "updatedAt": "2025-10-31T15:00:07.047Z",
        "notificationOptions": {}
      },
      "payerAuthentication": {
        "id": "7619358178076621304805",
        "url": "https://centinelapistag.cardinalcommerce.com/V2/Cruise/StepUp",
        "jwt": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiIwMmNkYjU1Zi1hNGM5LTQ1ZDctODE0Ni1hNzAxMmJhMzI4NTAiLCJpYXQiOjE3NjE5MzU4MTgsImlzcyI6IjVkZDgzYmYwMGU0MjNkMTQ5OGRjYmFjYSIsImV4cCI6MTc2MTkzOTQxOCwiT3JnVW5pdElkIjoiNjg3NmRlZDA1YWExYzYxNWI3MTg5ZGUyIiwiUGF5bG9hZCI6eyJBQ1NVcmwiOiJodHRwczovLzBtZXJjaGFudGFjc3N0YWcuY2FyZGluYWxjb21tZXJjZS5jb20vTWVyY2hhbnRBQ1NXZWIvY3JlcS5qc3AiLCJQYXlsb2FkIjoiZXlKdFpYTnpZV2RsVkhsd1pTSTZJa05TWlhFaUxDSnRaWE56WVdkbFZtVnljMmx2YmlJNklqSXVNaTR3SWl3aWRHaHlaV1ZFVTFObGNuWmxjbFJ5WVc1elNVUWlPaUl4TnpZeFpqSmlPQzA1WmpZeExUUXpNbU10T1dFMFlpMDVPVEpoTURsaU1EWXpOV1FpTENKaFkzTlVjbUZ1YzBsRUlqb2lPVFl4TjJJd1kyRXROV0V4WVMwME16aGhMV0ptWTJNdFlURXpObU00Tm1OalpUazBJaXdpWTJoaGJHeGxibWRsVjJsdVpHOTNVMmw2WlNJNklqQXlJbjAiLCJUcmFuc2FjdGlvbklkIjoiTE9EQUJ2WnhPRWxJcUJ0MW1mNzAifSwiT2JqZWN0aWZ5UGF5bG9hZCI6dHJ1ZSwiUmV0dXJuVXJsIjoiaHR0cHM6Ly93ZWJob29rLnNpdGUvMmFiOGFmNWEtZWU0Ni00OTU0LTllMDQtMzJlNDk5YTM3NDg1In0.iwePLhyUKh9BF90BACBiCn8KQIsfnpZ17JndrH0Ccbo",
        "authenticationTransactionId": "LODABvZxOElIqBt1mf70"
      }
    }
  ```
</Accordion>

<Tip>When challenge isn't required, the status will be `COMPLETED`. The order was approved without any customer action, it's called 'frictionless' approval</Tip>

### 5. Display Challenge

Using the `payerAuthentication.url` and `payerAuthentication.jwt` from the response in step 4, display the 3DS challenge iframe where the customer will complete identity verification.

<Warning>
  The challenge must be initiated within **30 seconds** of receiving the response, or the authentication session will timeout.
</Warning>

<Tip>
  When the customer completes authentication, the challenge iframe automatically redirects to the `returnUrl` you provided in step 3.2. This is why the example uses two separate pages: `challenge-page` displays the challenge, and `redirection-page` handles the redirect completion.
</Tip>

<AccordionGroup>
  <Accordion title="Display Challenge Example HTML" icon="code">
    <CodeGroup>
      ```html challenge-page.html theme={null}
      <!DOCTYPE html>
      <html>
      <head>
          <title>3D Secure Authentication</title>
      </head>
      <body>
          <div id="challenge-container">
              <p>Verifying your payment...</p>
              <!-- Visible iframe for the 3DS challenge -->
              <iframe
                  id="step-up-iframe"
                  name="step-up-iframe"
                  width="400"
                  height="400"
                  style="border: 1px solid #ccc; border-radius: 8px;">
              </iframe>
          </div>

          <!-- Hidden form to submit JWT to Cardinal Commerce -->
          <form
              id="step-up-form"
              method="POST"
              target="step-up-iframe"
              style="display: none;">
              <input type="hidden" name="JWT" value="{jwt-from-response}" />
          </form>

          <script>
          // Set the action URL from the API response
          document.getElementById('step-up-form').action = '{url-from-response}';

          // Submit the form on page load to display the challenge
          window.onload = function() {
              var stepUpForm = document.querySelector('#step-up-form');
              if (stepUpForm) {
                  stepUpForm.submit();
              }
          };

          // Listen for completion message from redirection page
          window.addEventListener("message", function(event) {
              if (event.data === "challenge_complete") {
                  console.log("3DS challenge completed successfully");

                  // Hide the challenge iframe
                  var container = document.getElementById('challenge-container');
                  if (container) {
                      container.style.display = 'none';
                  }

                  // Next step: Call POST /v2/payment-orders/{id}/payer-authentication/validate
                  // to complete the payment
              }
          }, false);
          </script>
      </body>
      </html>
      ```

      ```html redirection-page.html theme={null}
      <!DOCTYPE html>
      <html>
      <head>
          <title>Authentication Complete</title>
      </head>
      <body>
          <p>Authentication complete. Please wait...</p>

          <script>
          // Notify parent window that challenge is complete
          if (window.parent) {
              window.parent.postMessage("challenge_complete", "*");
          }
          </script>
      </body>
      </html>
      ```
    </CodeGroup>
  </Accordion>

  <Accordion title="Display Challenge Example React" icon="react">
    <CodeGroup>
      ```jsx ChallengePage.jsx theme={null}
      import { useState, useEffect, useRef } from 'react';

      function ChallengePage() {
        const [showChallenge, setShowChallenge] = useState(true);
        const formRef = useRef(null);

        // Auto-submit form on component mount
        useEffect(() => {
          if (formRef.current) {
            formRef.current.action = '{url-from-response}';
            formRef.current.submit();
          }
        }, []);

        // Listen for completion message from redirection page
        useEffect(() => {
          const handleMessage = (event) => {
            if (event.data === "challenge_complete") {
              console.log("3DS challenge completed successfully");

              // Hide the challenge iframe
              setShowChallenge(false);

              // Next step: Call POST /v2/payment-orders/{id}/payer-authentication/validate
              // to complete the payment
            }
          };

          window.addEventListener("message", handleMessage);

          // Cleanup listener on unmount
          return () => {
            window.removeEventListener("message", handleMessage);
          };
        }, []);

        return (
          <div>
            {showChallenge && (
              <div id="challenge-container">
                <p>Verifying your payment...</p>

                {/* Visible iframe for the 3DS challenge */}
                <iframe
                  id="step-up-iframe"
                  name="step-up-iframe"
                  width="400"
                  height="400"
                  style={{ border: '1px solid #ccc', borderRadius: '8px' }}
                />

                {/* Hidden form to submit JWT to Cardinal Commerce */}
                <form
                  ref={formRef}
                  id="step-up-form"
                  method="POST"
                  target="step-up-iframe"
                  style={{ display: 'none' }}
                >
                  <input type="hidden" name="JWT" value="{jwt-from-response}" />
                </form>
              </div>
            )}
          </div>
        );
      }

      export default ChallengePage;
      ```

      ```jsx RedirectionPage.jsx theme={null}
      import { useEffect } from 'react';

      function RedirectionPage() {
        useEffect(() => {
          // Notify parent window that challenge is complete
          if (window.parent) {
            window.parent.postMessage("challenge_complete", "*");
          }
        }, []);

        return (
          <div>
            <p>Authentication complete. Please wait...</p>
          </div>
        );
      }

      export default RedirectionPage;
      ```
    </CodeGroup>
  </Accordion>
</AccordionGroup>

## Improve Success Rates

### Include Device Information

Sending device data helps banks assess risk and approve payments without showing challenges:

```json theme={null}
{
  "deviceInformation": {
    "ipAddress": "192.168.1.100",
    "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)...",
    "httpBrowserLanguage": "es-MX",
    "httpBrowserScreenWidth": "1920",
    "httpBrowserScreenHeight": "1080",
    "httpBrowserColorDepth": "24",
    "httpBrowserTimeDifference": "-360",
    "httpBrowserJavaEnabled": "false"
  }
}
```

### Best Practices

<AccordionGroup>
  <Accordion title="Always Include Device Info" icon="mobile">
    Device fingerprinting reduces friction by enabling frictionless 3DS flows.
  </Accordion>

  <Accordion title="Save Payment Methods" icon="floppy-disk">
    Returning customers with saved cards are less likely to trigger 3DS challenges.
  </Accordion>

  <Accordion title="Consistent Customer Data" icon="user-check">
    Use the same customer information across payments for better risk scoring.
  </Accordion>

  <Accordion title="Local Processing" icon="location-dot">
    Process from the same region when possible to reduce risk signals.
  </Accordion>
</AccordionGroup>

## 3D-Secure testing data

Use these test cards in sandbox:

| amount (MXN cents) | Card Number        | Behavior                           |
| ------------------ | ------------------ | ---------------------------------- |
| \< 100000          | `4000000000002701` | No 3DS - direct approval           |
| >= 100000          | `4000000000002503` | 3DS challenge required (full flow) |
| >= 100000          | `4000000000002701` | 3DS frictionless (no challenge)    |

## Next Steps

<CardGroup cols={2}>
  <Card title="Process Card Payments" icon="credit-card" href="/features/card-payments">
    Learn about basic card payment processing
  </Card>

  <Card title="Save Payment Methods" icon="floppy-disk" href="/features/payment-methods">
    Reduce 3DS challenges with saved cards
  </Card>

  <Card title="Testing Guide" icon="flask" href="/guides/testing">
    Test all 3DS scenarios in sandbox
  </Card>

  <Card title="Error Handling" icon="triangle-exclamation" href="/guides/error-handling">
    Handle authentication failures gracefully
  </Card>
</CardGroup>
