Autenticación 3DS

La autenticación 3DS (3-D Secure) es un protocolo de seguridad para pagos en línea con tarjeta de crédito o débito que solicita la introducción de una contraseña o un código enviado por SMS o generado desde la aplicación de tu banco para validar la transacción.


Realizar la validación 3DS

Para realizar la validación 3DS necesitarás seguir estos pasos:

  1. Identificar si un pago requiere validación 3DS
  2. Si lo requiere, implementar un iFrame
  3. Escuchar la respuesta del iFrame
  4. Consultar el resultado de la validación a través del endpoint GET /payments/{payment_id}

Identificar si un pago requiere validación 3DS

Cuando un cargo requiere la validación 3DS, dentro de la respuesta del endpoint de POST /payments recibirás un status "pending" con un código "PE-3DS01" y un mensaje "Waiting 3ds" de esta forma:

"status": "pending",
"status_detail": {
  "code": "PE-3DS01",
  "message": "Waiting 3ds"
}

Adicionalmente, también recibirás una URL dentro del parámetro "pending_action"; esta URL te servirá para redirigir a tus clientes para que puedan realizar la autenticación 3DS:

"pending_action": {
        "type": "open_modal",
        "url": "https://3ds.payclip.com?transaction=1234"
    }

Puedes ver la respuesta completa aquí.


Implementar iFrame

Debes tomar el parámetro {pending_action.url} y abrirlo dentro de un iFrame, a continuación se muestran dos formas de hacerlo:

<iframe      
      title="cybersource3Ds"
      src={pending_action.url}
      data-testid="cybersource3Ds-iframe"
      style="width: 900px;height: 900px"
 />
// Función para mostrar el iFrame de 3DS
      function show3DSIframe(url, paymentId) {
        const iframeContainer = document.getElementById("3ds-iframe-container");
        iframeContainer.innerHTML = 
          `<iframe
                  title="cybersource3Ds" 
                  src="${url}" data-testid="cybersource3Ds-iframe"
                  style="width: 100vw;height: 100vh;border:none;position:fixed;top:0;left:0;z-index:1000;">
           </iframe>`;
        iframeContainer.style.display = "block";

Escuchar respuesta

Agrega un listener para saber cuando la autenticación 3DS se haya completado y obtener el payment id:

window.addEventListener("message", (event) => {
          if (event.origin !== new URL(url).origin) {
            return; // Ignorar mensajes de otros orígenes
          }
          if (event.data?.paymentId) {
            const returnedPaymentId = event.data.paymentId;
            console.log("Returned Payment ID:", returnedPaymentId);

Consultar el resultado de la validación 3DS

Por último, consulta el endpoint GET /payments/{payment_id} usando el payment id obtenido en el paso anterior para consultar el status del pago después de haber realizado la validación 3DS. El status vendrá en el campo "status" de la respuesta. Es importante que la consulta la realices desde tu back-end, no desde el front-end, por seguridad de tus datos:

curl --location 'https://api.payclip.com/payments/00d2b44e-d89c-432d-94e7-27cda1e0cd' \
--header 'Authorization: Bearer 8bef80-f3d1-443f-a054-8e445a8614'

Puedes consultar la respuesta completa aquí.


Validación exitosa









Validación fallida













Flujo de validación 3DS completo

El siguiente código muestra un ejemplo de todo el flujo de la validación 3DS:

<html>
    <head>
        <title>Authentication 3DS</title>
    </head>
    <body>
        <h2>
            <div id="status-message" style="display:none;"></div>
        </h2>
        <div id="3ds-iframe-container" style="display:none;"></div>

        <script>
            var API_KEY = "tu-api-key"; //Aquí va tu API Key, no es necesario agregar nada más
            var url="https://3ds.payclip.com?transaction";//Aquí va la URL obtenida desde pending_action.url
          
            const iframeContainer = document.getElementById("3ds-iframe-container");
            iframeContainer.innerHTML = `<iframe title="cybersource3Ds" src="${url}" data-testid="cybersource3Ds-iframe" style="width: 100vw;height: 100vh;border:none;position:fixed;top:0;left:0;z-index:1000;"></iframe>`;
            iframeContainer.style.display = "block";
            
            // Función para mostrar mensajes de estado
            function showStatusMessage(message) {
                const statusMessage = document.getElementById("status-message");
                statusMessage.innerText = message;
                statusMessage.style.display = "block";
            }

            window.addEventListener("message", (event) => {
                if (event.origin !== new URL(url).origin) {
                    return; // Ignorar mensajes de otros orígenes
                }
                if (event.data?.paymentId) {
                    const returnedPaymentId = event.data.paymentId;
                    console.log("Returned Payment ID:", returnedPaymentId);
                    
                  //Comprobando el status del pago
                  
                  //IMPORTANTE: recuerda que esto lo tienes que hacer desde tu backend
                  //Este es únicamente un ejemplo para entender el flujo completo
                  
                    fetch(`https://api.payclip.com/payments/${returnedPaymentId}`, {
                        method: "GET",
                        headers: {
                            "Content-Type": "application/json",
                            "Authorization": "Bearer " + API_KEY; //Recuerda que esto lo tienes que hacer desde tu backend           
                        }
                    })
                    .then((response) => response.json())
                    .then((data) => {
                        console.log(data);
                        if (data.status === "approved") {
                            showStatusMessage("Tu pago fue exitoso");
                        } else if (data.status === "rejected") {
                            showStatusMessage("El pago fue declinado");
                        }
                    })
                    .catch((error) => {
                        console.error(error);
                        showStatusMessage("Error al verificar el estado del pago");
                    });
                }
            });
        </script>
    </body>
</html>


Respuesta completa

El siguiente JSON muestra un ejemplo de una respuesta de un pago (POST /payments) que requiere validación 3DS:


{
  "id": "85d8f78-d58-48e7-9735-82adcccf",
  "amount": 1,
  "tip_amount": 0,
  "amount_refunded": 0,
  "installment_amount": 1,
  "installments": 1,
  "capture_method": "automatic",
  "net_amount": 1,
  "paid_amount": 1,
  "captured_amount": 0,
  "binary_mode": false,
  "country": "MX",
  "currency": "MXN",
  "description": "Descripción de ejemplo",
  "external_reference": "",
  "customer": {
    "address": {
        "country": "",
        "postal_code": "",
        "state": "",
        "city": "",
        "colony": "",
        "street": "",
        "number": ""
    },
    "description": "",
    "email": "[email protected]",
    "first_name": "",
    "identification": {
        "id": "",
        "type": ""
    },
    "last_name": "",
    "phone": "5555555555"
  },
  "payment_method": {
    "id": "visa",
    "type": "credit_card",
    "card": {
      "bin": "111111",
      "issuer": "CIBANCO PR",
      "name": "John Doe",
      "country": "MX",
      "last_digits": "2222",
      "exp_year": "28",
      "exp_month": "11"
 		 },
 		"token": "608b27c-bfc7-47d1-b47c-e66abba9"
  },
  "pending_action": {
    "type": "open_modal",
    "url": "https://3ds.payclip.com?transaction=64dc-f82-452-af5b-5f740e&payment=85d78-d58-4e7-975-82adcf&origin=payments-api&ts=1477"
  },
  "receipt_no": "qIASb",
  "claims": [],
  "refunds": [],
  "statement_descriptor": "",
  "status": "pending",
  "status_detail": {
    "code": "PE-3DS01",
    "message": "Waiting 3ds"
  },
  "metadata": {},
  "return_url": "",
  "webhook_url": "",
  "created_at": "2024-06-05T17:44:37.473766884Z",
  "version": 0
}


Status Y Status_detail

Al realizar un pago a través del endpoint POST/payments o consultar la información de un pago a través del endpoint GET/payments/{payment_id} recibirás los siguientes parámetros:

"status": "approved",
    "status_detail": {
        "code": "AP-PAI01",
        "message": "paid"
    }

En el parámetro “status” encontrarás el estado del pago. Los posibles valores son los siguientes:

  • Approved: El pago fue aprobado.
  • Refunded: El pago fue reembolsado.
  • Cancelled: El pago no prospero desde un estado pendiente o autorizado.
  • Rejected: El pago fue rechazado.
  • Authorized: El pago fue autorizado pero no se ha hecho el cargo.
  • Pending: El pago quedó pendiente de una acción a realizar para que prospere. Ej. Falta la autenticación 3DS.

Dentro del objeto “status_detail” podrás encontrar un código y un mensaje proporcionando más detalles acerca del status del pago.

Puedes consultar todos los posibles códigos y mensajes en el siguiente link.


Integrándolo todo

El siguiente código muestra un ejemplo de cómo integrar todo el flujo del SDK desde la configuración y el pago hasta la validación 3DS.

❗️

IMPORTANTE: El pago lo tienes que hacer desde tu back-end por seguridad de tus datos, el siguiente es únicamente un ejemplo para entender el funcionamiento del flujo completo.


Es importante, como se menciona arriba, que el pago lo realices desde tu back-end, no desde el front-end, este ejemplo es únicamente con fines de entender cómo funciona todo el flujo:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="stylesheet" href="global.css" />
    <link rel="preconnect" href="https://fonts.googleapis.com" />
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
    <link
      href="https://fonts.googleapis.com/css2?family=Nunito+Sans:[email protected]&display=swap"
      rel="stylesheet"
    />
    <title>Transparent Checkout SDK</title>
    
    <!-- Importa el SDK de Clip -->
    <script src="https://sdk.clip.mx/js/clip-sdk.js"></script>
  </head>
  <body>
    <h1>Transparent Checkout SDK</h1>
    
    <!-- Formulario para obtener el token de la tarjeta -->
    <form id="payment-form">
      <div id="checkout"></div>
      <button id="submit">Pagar</button>
      <p id="cardTokenId"></p>
    </form>
    <br><br>
    
    <h2>
      <div id="status-message" style="display:none;"></div>
    </h2>   
    <div id="3ds-iframe-container" style="display:none;"></div>
    
    <!-- Autenticación -->
    <script>
      const API_KEY = "tu-api-key"; // Aquí va tu API Key, no es necesario agregar nada más

      // Inicializa el SDK de Clip con la API Key proporcionada
      const clip = new ClipSDK(API_KEY);
      
      // Verifica si la API Key ha sido ingresada correctamente
      if (API_KEY == "XXXXXXXXXX") {
        alert("Favor de ingresar tu API Key (https://dashboard.developer.clip.mx/applications)");
      }

      // Crea un elemento tarjeta con el SDK de Clip
      const card = clip.element.create("Card", {
        theme: "light",
        locale: "es",
      });
      card.mount("checkout");

      let cardTokenID = null;

      // Maneja el evento de envío del formulario
      document.querySelector("#payment-form").addEventListener("submit", async (event) => {
        event.preventDefault();
        try {
          // Obtén el token de la tarjeta
          const cardToken = await card.cardToken();
          // Guarda el Card Token ID de la tarjeta en una constante
          cardTokenID = cardToken.id;
          console.log("Card Token ID:", cardTokenID);
          // Muestra el mensaje de "Realizando el pago"
          showStatusMessage("Realizando el pago");
        } catch (error) {
          // Maneja errores durante la tokenización de la tarjeta
          switch (error.code) {
            case "CL2200":
            case "CL2290":
              alert("Error: " + error.message);
              throw error;
              break;
            case "AI1300":
              console.log("Error: ", error.message);
              break;
            default:
              break;
          }
        }

        await handleSubmit(event); // Llama a handleSubmit después de obtener el token
      });

      async function handleSubmit(e) {
        e.preventDefault();

        if (!cardTokenID) {
          alert("No se ha obtenido el Card Token ID");
          return;
        }

        // Oculta el formulario
        document.getElementById("payment-form").style.display = "none";

        // Obtener datos específicos relacionados con la prevención de riesgos ingresados en el Card Element.
        const preventionData = await card.preventionData();
        console.log("Prevention Data: ", preventionData);

        const payload = {
          "description": "Pago desde el SDK de Checkout Transparente",
          "customer": {
            "email": "[email protected]",
            "phone": "5555555555",
            "address":{
              "postal_code":"03800",
              "street": "Calle 1",
              "number": "3452"
            }
          },
          "payment_method": {
            "token": cardTokenID // Usa la constante cardTokenID aquí
          },
          "amount": 1,
          "currency": "MXN",
          "prevention_data": { 
            "session_id": preventionData.session_id,
            "user_agent": preventionData.user_agent,
            "device_finger_print_token": preventionData.session_id
          }
        };
        
        //IMPORTANTE: recuerda que esto lo tienes que hacer desde tu backend
        //Este es únicamente un ejemplo para entender el flujo completo
        
        fetch("https://api.payclip.com/payments", {
          method: "POST",
          body: JSON.stringify(payload),
          headers: {
            "Content-Type": "application/json",
            "Authorization": "Bearer " +  API_KEY // Recuerda que esto lo tienes que hacer desde tu back-end
          },
        })
        .then((response) => response.json())
        .then((data) => {
          console.log(
            "%c Response!",
            "color: blue; font-size: 20px; background-color: yellow;",
            data
          );

          // Mostrar el mensaje correspondiente basado en el estado de la respuesta
          if (data.status === "approved") {
            showStatusMessage("Tu pago fue exitoso");
          } else if (data.status === "rejected") {
            showStatusMessage("El pago no fue exitoso. Por favor contacta a tu banco");
          } else if (data.status === "pending" && data.status_detail.code === "PE-3DS01" && data.status_detail.message === "Waiting 3ds") {
            showStatusMessage("Por favor realiza la validación 3DS que se muestra abajo");
            const pendingActionUrl = data.pending_action.url;
            show3DSIframe(pendingActionUrl, data.id);
          }
        })
        .catch((error) => {
          console.error(error);
          showStatusMessage("El pago no fue exitoso. Por favor contacta a tu banco");
        });
      }

      // Función para mostrar mensajes de estado
      function showStatusMessage(message) {
        const statusMessage = document.getElementById("status-message");
        statusMessage.innerText = message;
        statusMessage.style.display = "block";
      }

      // Función para mostrar el iFrame de 3DS
      function show3DSIframe(url, paymentId) {
        const iframeContainer = document.getElementById("3ds-iframe-container");
        iframeContainer.innerHTML = `<iframe title="cybersource3Ds" src="${url}" data-testid="cybersource3Ds-iframe" style="width: 100vw;height: 100vh;border:none;position:fixed;top:0;left:0;z-index:1000;"></iframe>`;
        iframeContainer.style.display = "block";

        window.addEventListener("message", (event) => {
          if (event.origin !== new URL(url).origin) {
            return; // Ignorar mensajes de otros orígenes
          }
          if (event.data?.paymentId) {
            const returnedPaymentId = event.data.paymentId;
            console.log("Returned Payment ID:", returnedPaymentId);

            if (returnedPaymentId === paymentId) {
              //IMPORTANTE: recuerda que esto lo tienes que hacer desde tu backend
              //Este es únicamente un ejemplo para entender el flujo completo
              fetch(`https://api.payclip.com/payments/${returnedPaymentId}`, {
                method: "GET",
                headers: {
                  "Content-Type": "application/json",
                  "Authorization": "Bearer " + API_KEY //Recuerda que esto lo tienes que hacer desde tu back-end
                }
              })
              .then((response) => response.json())
              .then((data) => {
                console.log(data);
                if (data.status === "approved") {
                  showStatusMessage("Tu pago fue exitoso");
                } else if (data.status === "rejected") {
                  showStatusMessage("El pago fue declinado");
                }
              })
              .catch((error) => {
                console.error(error);
                showStatusMessage("Error al verificar el estado del pago");
              });
            } else {
              showStatusMessage("El Payment ID no coincide. Por favor intenta de nuevo.");
            }
          }
        });
      }
    </script>
  </body>
</html>