Skip to main content

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:

SMS Verification

One-time code sent via text message

Biometric Auth

Face ID, fingerprint, or other biometrics

Banking App

Confirmation through bank’s mobile app

Security Questions

Personal security questions or PINs
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.
Most payments (60-80%) complete automatically without showing a challenge to your customer. Cheqpay handles the verification in the background.

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
You don’t need to decide when to use 3DS - we handle it automatically based on risk assessment and regulations.

How 3D Secure Works

1

Create Order

Send your payment request as usual to POST /v2/payment-orders
2

Check Order Response

If 3DS is required, status will be PAYER_AUTHENTICATION_DEVICE_DATA_REQUIRED.
3

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
4

Check Authentication Response

If further challenge is needed, status will be PAYER_AUTHENTICATION_CHALLENGE_REQUIRED.
5

Display Challenge

Display an iframe for the customer to complete the 3DS challenge.
6

Validate

Once the customer completes challenge, call POST /v2/payment-orders/{id}/payer-authentication/validate to complete the payment.

Implementation

1. Create Order

Send a normal payment order request to POST /v2/payment-orders.
curl -X POST https://prod.cheqpay.mx/payment-orders \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "externalId": 'order-123',
    "customer": {
      "email": "[email protected]",
      "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"
    }
  }'
amount unit is the smallest currency unit (e.g., cents). So for MXN, 100 MXN = 10000 cents

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.
{
  "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": "[email protected]"
  },
  "payerAuthentication": {
      "id": "a0c76393-0505-4721-81de-f605180e0eb8",
      "url": "https://centinelapistag.cardinalcommerce.com/V1/Cruise/Collect",
      "jwt": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiI2NTU3ZmI2NS0zZDExLTRiYWMtOTZmMS05MjZjNWFkNjY1MzkiLCJpYXQiOjE3NjE5MjI5MTksImlzcyI6IjVkZDgzYmYwMGU0MjNkMTQ5OGRjYmFjYSIsImV4cCI6MTc2MTkyNjUxOSwiT3JnVW5pdElkIjoiNjg3NmRlZDA1YWExYzYxNWI3MTg5ZGUyIiwiUmVmZXJlbmNlSWQiOiJhMGM3NjM5My0wNTA1LTQ3MjEtODFkZS1mNjA1MTgwZTBlYjgifQ.7udCNTQ2XIpvbCrFvr6XF64nBTJOLI5S9oBcGD7WkEo"
  }
}
When 3DS is not required, the status will be COMPLETED

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.
index.html
  <!-- 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>
DeviceDataCollector.jsx
  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;

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.
  curl --location '{HOST}/pos/v2/payment-orders/3737a6ec-1068-4948-94b8-cf10246de080/payer-authentication' \
  --header 'Content-Type: application/json' \
  --header 'Authorization: Bearer {API_KEY}' \
  --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"
  }'

4. check Authentication Response

Check the status in the response. If it’s PAYER_AUTHENTICATION_CHALLENGE_REQUIRED, proceed to display the challenge.
  {
    "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": "[email protected]",
      "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"
    }
  }
When challenge isn’t required, the status will be COMPLETED. The order was approved without any customer action, it’s called ‘frictionless’ approval

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.
The challenge must be initiated within 30 seconds of receiving the response, or the authentication session will timeout.
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.
<!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>
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;

Improve Success Rates

Include Device Information

Sending device data helps banks assess risk and approve payments without showing challenges:
{
  "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

Device fingerprinting reduces friction by enabling frictionless 3DS flows.
Returning customers with saved cards are less likely to trigger 3DS challenges.
Use the same customer information across payments for better risk scoring.
Process from the same region when possible to reduce risk signals.

3D-Secure testing data

Use these test cards in sandbox:
amount (MXN cents)Card NumberBehavior
< 1000004000000000002701No 3DS - direct approval
>= 10000040000000000025033DS challenge required (full flow)
>= 10000040000000000027013DS frictionless (no challenge)

Next Steps