La Payment Request API en 2026: datos del comprador, reintentos y Apple Pay (parte 2)

La Payment Request API en 2026: datos del comprador, reintentos y Apple Pay (parte 2)

Actualizado en noviembre de 2026. La versión de 2023 de esta parte usaba un formato antiguo de Google Pay, cerraba la ventana de pago con complete('success') antes de procesar el pago y tenía una rama de cancelación que nunca llegaba a ejecutarse. El código de abajo la sustituye.

En la parte 1 montamos una petición de pago que funciona: Google Pay en modo de prueba, un botón que solo aparece cuando el navegador puede pagar y un buen manejo de las cancelaciones. En esta parte pedimos al comprador sus datos de contacto, vemos el envío, tratamos los pagos que fallan en el servidor, añadimos Apple Pay y lo juntamos todo en una demo que puedes probar.

Pedir los datos del comprador

new PaymentRequest() acepta un tercer argumento opcional: un objeto de opciones que le dice a la ventana de pago qué más tiene que pedir. Todas valen false por defecto.

const options = {
  requestPayerName: true,   // el nombre del comprador
  requestPayerEmail: true,  // su email
  requestPayerPhone: false  // su teléfono
};

const request = new PaymentRequest([googlePay], details, options);

Cuando el usuario confirma, los valores llegan en la respuesta como payerName, payerEmail y payerPhone. Google recomienda pedir el email, el teléfono y la dirección de envío con estas opciones del navegador y no con la API de Google Pay, para que funcionen igual con cualquier método de pago, y usar Google Pay solo para los datos de pago y, si hace falta, la dirección de facturación.

Pide solo lo que vayas a usar. Cada campo de más es una cosa más que el usuario tiene que revisar antes de pagar.

El envío: sigue funcionando, pero va de salida

La API también puede pedir una dirección de envío y dejar que el usuario elija una forma de envío, con el precio actualizado en la ventana. Se activa con requestShipping y las opciones se describen dentro de los detalles del pago:

const options = { requestShipping: true, shippingType: 'shipping' }; // o 'delivery', 'pickup'

const details = {
  total: { label: 'Total', amount: { currency: 'EUR', value: '100.00' } },
  shippingOptions: [
    { id: 'standard', label: 'Estándar (3-5 días)', amount: { currency: 'EUR', value: '0.00' }, selected: true },
    { id: 'express', label: 'Urgente (24 h)', amount: { currency: 'EUR', value: '9.90' } }
  ]
};

Hay dos eventos que te avisan cuando el usuario cambia algo. En los dos se responde con event.updateWith(), al que se le pasan los detalles nuevos o una promesa que se resuelve con ellos:

request.addEventListener('shippingaddresschange', (event) => {
  event.updateWith(updateForAddress(request.shippingAddress));
});

request.addEventListener('shippingoptionchange', (event) => {
  event.updateWith(updateForOption(request.shippingOption));
});

function updateForAddress(address) {
  if (address.country !== 'ES') {
    // una lista vacía significa que no podemos enviar a esa dirección
    return { error: 'Lo sentimos, solo enviamos a España.', shippingOptions: [] };
  }
  return { total: details.total, shippingOptions: details.shippingOptions };
}

function updateForOption(optionId) {
  const shipping = optionId === 'express' ? 9.9 : 0;
  return {
    shippingOptions: details.shippingOptions.map((option) => ({ ...option, selected: option.id === optionId })),
    total: { label: 'Total', amount: { currency: 'EUR', value: (100 + shipping).toFixed(2) } }
  };
}

Hay tres detalles en los que se suele tropezar:

  • el total no se recalcula solo: cuando cambia la forma de envío, el total nuevo lo mandas tú;
  • mientras la ventana está abierta, el navegador puede ocultar parte de la dirección por privacidad, así que el cálculo tiene que basarse en el país o en el código postal, y la dirección completa se comprueba en la respuesta final;
  • Safari da 30 segundos para resolver updateWith(); pasado ese tiempo, el pago caduca.

Ahora, el estado de todo esto. MDN marca shippingaddresschange, shippingoptionchange y las propiedades relacionadas (shippingAddress, shippingOption, shippingType) como obsoletas y fuera del estándar. Chrome (desde la versión 60) y Safari (desde la 11.1) las siguen admitiendo, así que las integraciones que ya existen siguen funcionando. En una tienda nueva es más seguro no depender de ellas: pide la dirección en tu propio formulario antes del paso de pago, o deja que se encargue el componente de pago de tu proveedor.

Cuando el pago falla en el servidor: retry()

El usuario puede confirmar en la ventana y que el cobro falle igualmente: una tarjeta rechazada, un email que no es válido. En lugar de cerrar la ventana y empezar de cero, response.retry() vuelve a mostrar la misma ventana con tu mensaje de error para que el usuario lo corrija. La promesa se resuelve cuando vuelve a confirmar, y el mismo objeto response pasa a tener los datos nuevos. Chrome lo admite desde la versión 78 y Safari desde la 12.1.

let outcome = await sendToServer(response);

while (!outcome.ok && typeof response.retry === 'function') {
  try {
    await response.retry({ error: outcome.message });
  } catch (error) {
    return; // el usuario ha cerrado la ventana
  }
  outcome = await sendToServer(response);
}

await response.complete(outcome.ok ? 'success' : 'fail');

Además de un error general, retry() acepta errores para campos concretos, por ejemplo { payer: { email: 'Este email no es válido.' } }, que la ventana muestra junto al campo. En los navegadores sin retry() el bucle no se ejecuta y el pago termina con complete('fail').

Añadir Apple Pay

Safari solo ofrece Apple Pay a través de esta API. El método va en el mismo array que Google Pay; cada navegador usa los métodos que admite y no hace caso de los demás:

const applePay = {
  supportedMethods: 'https://apple.com/apple-pay',
  data: {
    version: 3,
    merchantIdentifier: 'merchant.com.example',
    merchantCapabilities: ['supports3DS', 'supportsCredit', 'supportsDebit'],
    supportedNetworks: ['amex', 'discover', 'masterCard', 'visa'],
    countryCode: 'ES'
  }
};

const request = new PaymentRequest([googlePay, applePay], details, options);

Para que esto funcione necesitas un identificador de comerciante de Apple Pay (el merchant.com.example de arriba) y un servidor preparado para procesar pagos con Apple Pay. La guía de Apple sobre cómo configurar el entorno explica los pasos.

Después viene la validación del comerciante. Antes de que Safari muestre la ventana de Apple Pay, tu web tiene que conseguir una sesión de pago de Apple. Poco después de llamar a show(), Safari lanza un evento merchantvalidation con una validationURL. Tu servidor pide la sesión a esa URL y tu página se la pasa, o le pasa una promesa de ella, a event.complete():

request.onmerchantvalidation = (event) => {
  event.complete(fetchPaymentSession(event.validationURL));
};

async function fetchPaymentSession(validationURL) {
  const answer = await fetch('/apple-pay/session', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ validationURL })
  });
  return answer.json();
}

El endpoint /apple-pay/session lo pones tú: se conecta con Apple con tus credenciales de comerciante y devuelve la sesión. MDN también marca este evento como obsoleto y fuera del estándar, y Chrome no lo admite, pero es como Safari gestiona Apple Pay. Cuando el usuario autoriza el pago, response.details contiene el token de Apple Pay para tu proveedor de pagos, y terminas con complete('success') o complete('fail'), igual que con Google Pay.

La demo completa

La demo de CodePen lo junta todo con Google Pay en modo de prueba, así que no se cobra nada. Necesita un botón con el id pay-button, oculto al principio, y un párrafo vacío con el id result. Apple Pay no está en la demo, porque la validación del comerciante necesita un servidor y una cuenta de comerciante real. Este es su JavaScript, con los textos traducidos:

const button = document.getElementById('pay-button');
const result = document.getElementById('result');

// Google Pay en modo TEST, en el formato que documenta Google para la Payment Request API
const googlePay = {
  supportedMethods: 'https://google.com/pay',
  data: {
    environment: 'TEST',
    apiVersion: 2,
    apiVersionMinor: 0,
    merchantInfo: { merchantName: 'Example Merchant' },
    allowedPaymentMethods: [{
      type: 'CARD',
      parameters: {
        allowedAuthMethods: ['PAN_ONLY', 'CRYPTOGRAM_3DS'],
        allowedCardNetworks: ['MASTERCARD', 'VISA']
      },
      tokenizationSpecification: {
        type: 'PAYMENT_GATEWAY',
        parameters: { gateway: 'example', gatewayMerchantId: 'exampleGatewayMerchantId' }
      }
    }]
  }
};

const details = {
  total: { label: 'Total', amount: { currency: 'EUR', value: '100.00' } },
  displayItems: [
    { label: 'Producto A', amount: { currency: 'EUR', value: '50.00' } },
    { label: 'Producto B', amount: { currency: 'EUR', value: '50.00' } }
  ]
};

const options = { requestPayerName: true, requestPayerEmail: true };

async function setUp() {
  if (!window.PaymentRequest) {
    return;
  }
  const request = new PaymentRequest([googlePay], details, options);
  if (await request.canMakePayment()) {
    button.hidden = false;
  }
}

button.addEventListener('click', async () => {
  const request = new PaymentRequest([googlePay], details, options);
  let response;
  try {
    response = await request.show();
  } catch (error) {
    if (error.name === 'AbortError') {
      result.textContent = 'Pago cancelado.';
    } else {
      console.error(error);
      result.textContent = 'Algo ha fallado.';
    }
    return;
  }

  let outcome = await sendToServer(response);
  while (!outcome.ok && typeof response.retry === 'function') {
    try {
      await response.retry({ error: outcome.message });
    } catch (error) {
      result.textContent = 'Pago cancelado.';
      return;
    }
    outcome = await sendToServer(response);
  }

  await response.complete(outcome.ok ? 'success' : 'fail');
  result.textContent = outcome.ok
    ? 'Gracias, ' + response.payerName + '. El recibo llegará a ' + response.payerEmail + '.'
    : outcome.message;
});

// Solo para la demo: una tienda real envía por POST response.details, payerName y payerEmail a su servidor,
// que cobra el token a través de su proveedor de pagos y responde con ok o con un mensaje de error
async function sendToServer(response) {
  console.log(response.methodName, response.details, response.payerName, response.payerEmail);
  return { ok: true };
}

setUp();

Antes de pasar a producción

  • Sirve la página por HTTPS y mantén tu proceso de pago normal para los navegadores sin la API, Firefox entre ellos.
  • No te fíes nunca del total que llega del navegador: el servidor recalcula el pedido y cobra ese importe.
  • Llama a complete() solo cuando el servidor haya respondido.
  • Trata el AbortError como una cancelación, no como un error.
  • Prueba primero con el entorno TEST de Google Pay y luego pasa a PRODUCTION con el ID de comerciante que te da Google.

Para saber más