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:

if (returnedPaymentId === paymentId) {
              iframeContainer.style.display = "none";
              fetch(`https://api.payclip.com/payments/${returnedPaymentId}`, {
                method: "GET",
                headers: {
                  "Content-Type": "application/json",
                  "Authorization": "Bearer tu-api-key"; //Aquí va el prefijo "Bearer" seguido de tu API Key
                }
              })

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 = "Bearer tu-api-key"; //Aquí va el prefijo "Bearer" seguido de tu API Key
            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
                    fetch(`https://api.payclip.com/payments/${returnedPaymentId}`, {
                        method: "GET",
                        headers: {
                            "Content-Type": "application/json",
                            "Authorization": "Bearer tu-api-key"; //Aquí va el prefijo "Bearer" seguido de tu API Key            
                        }
                    })
                    .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:

<!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 = "Bearer tu-api-key"; // Aquí va el prefijo 'Bearer' seguido de tu API Key

      // 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
          }
        };

        fetch("https://api.payclip.com/payments", {
          method: "POST",
          body: JSON.stringify(payload),
          headers: {
            "Content-Type": "application/json",
            "Authorization": API_KEY // Aquí debes poner tu API Key
          },
        })
        .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) {
              //iframeContainer.style.display = "none";
              fetch(`https://api.payclip.com/payments/${returnedPaymentId}`, {
                method: "GET",
                headers: {
                  "Content-Type": "application/json",
                  "Authorization": API_KEY //Tu API Key
                }
              })
              .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>