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.
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.
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.
Collect Device Data HTML Example
index.html
Copy
<!-- 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>
Collect Device Data React Example
DeviceDataCollector.jsx
Copy
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;
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.
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.
Display Challenge Example HTML
Copy
<!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>
Display Challenge Example React
Copy
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;