E2 Apple Pay · bajo demanda
STG api.stg.conekta.io Company d API 2.3.0 Firma

Apple Pay con credencial guardada

Consentimiento en el sheet con recurringPaymentRequest → guardar la credencial sin cobrar → cobrar después, con el cliente ausente y montos distintos. Fijado a la compañía de staging: esta es la única donde existe el feature.

¿Quién firma la merchant session?

Esta perilla tiene que casar con la del descifrado: Apple cifra el token con el Payment Processing Certificate del merchant que firmó la sesión. Si no coinciden, sale OpenSSL::Cipher::CipherError (finding F-CIPHER).

En staging NO se puede obtener un MPAN. Ninguna combinación lo permite.

Apple elige el gateway según el dispositivo: una cuenta Apple real con tarjeta real va al gateway de producción; una cuenta Apple Pay Sandbox con tarjeta de prueba va al de -cert. Verificado en vivo el 11-sep-2026:

FirmaGatewayTarjetaSesiónMPAN
PSP Conekta Stage-certsandbox ✓ válida✗ el sandbox de Apple nunca emite MPAN
PSP Conekta Stageproducciónreal ✗ 417
Cuenta propia merchant.ANB.QAproducciónreal ✓ válida✗ merchant ID sin respaldo de redes

El 417 del PSP en el gateway de producción no es falta de Mass Enablement: la company está enrolada, pero para el entorno sandbox de Apple — que es lo correcto para staging. La única captura de MPAN que existe (CRD-1979) se hizo en producción, con tarjeta real y merchant.io.conekta.

0

Comprobaciones del dispositivo

Antes de nada: si algo de esto falla, el resto no puede funcionar.

1

Crear el customer

POST /customers — sin método de pago todavía. Devuelve el cus_ al que después se le cuelga la credencial.

2

Consentimiento y guardado

Abre el sheet con recurringPaymentRequest y manda el pk_payment a POST /customers/:id/payment_sources. Conekta hace una verificación (autorización de ~$1 reversada) para abrir la serie. No cobra el monto de abajo — ese solo se muestra en el sheet.

No está documentado cuál usa la llave del Vault (tu cert) y cuál la del PSP. Si sale posible_cipher_error, prueba el siguiente de la lista: es la perilla que decide con qué llave descifra Conekta.

Crea el customer primero.
3

Cobrar bajo demanda

POST /orders con payment_method {type:"apple", id:"src_…"}, sin pk_payment y sin el cliente presente. Cada cobro puede llevar un monto distinto — ese es el punto del ejercicio.

4

Ver el wallet del customer

GET /customers/:id/payment_sources — qué credenciales quedaron y con qué credential_type.

Log

Esperando…
Qué verificamos contra la documentación oficial (y qué no cuadra)
La versión de la sesión es lo primero que hay que cambiar. El brief dice “agregar el recurringPaymentRequest al request existente” sin mencionar la versión. Pero recurringPaymentRequest se introdujo en la v14 de la Apple Pay JS API. Una integración que hoy hace new ApplePaySession(3, …) —como la nuestra antes de esta página— no va a emitir merchant token. Esta herramienta no sube la versión a todo: negocia cada camino por separado (versionPara(), abajo) y, cuando el dispositivo no llega a v14, retira el guardado y deja vivo el one-off en v3 — para que se vea que la separación es la respuesta correcta, no una limitación.
“Added support for automatic reload payments, recurring payment requests, and multiple payment tokens in ApplePayModifier and ApplePayPaymentRequest.” Apple — Apple Pay on the Web Version 14 Release Notes (macOS 13 / iOS 16)

El patrón, para copiar

Así queda la negociación por camino. Es lo que corre en esta página:

// El one-off basta con v3. El recurrente EXIGE v14 (recurringPaymentRequest).
// Apple: usa la versión más baja que soporte lo que necesitas.
const V_ONE_OFF = 3;
const V_RECURRENTE = 14;

function versionPara(camino) {
  const quiero = camino === 'recurrente' ? V_RECURRENTE : V_ONE_OFF;
  if (!window.ApplePaySession) return null;
  if (typeof ApplePaySession.supportsVersion !== 'function') {
    // Navegador viejo: solo dejamos pasar one-off.
    return camino === 'recurrente' ? null : V_ONE_OFF;
  }
  return ApplePaySession.supportsVersion(quiero) ? quiero : null;
}

// Clave: si el recurrente no está disponible NO se degrada a una versión
// menor. Se retira la opción de guardar. Fingir que se puede es justo lo
// que produce el on_demand_eligible:false que nadie sabe diagnosticar.
const vRec = versionPara('recurrente');
if (vRec === null) {
  ocultarBotonGuardarCredencial();      // el one-off sigue intacto
} else {
  new ApplePaySession(vRec, { ...request, recurringPaymentRequest });
}

El MPAN no está garantizado

Incluir recurringPaymentRequest es condición necesaria pero no suficiente. Apple emite merchant token solo “where supported by the issuer”; si el emisor no lo soporta, regresa un DPAN normal. Por eso el paso 2 pinta credential_type y on_demand_eligible en grande: un false ahí no es un bug de la integración.

“Apple Pay defaults back to returning the DPAN.” Apple — Apple Pay Merchant Integration Guide (marzo 2026), p.18

managementURL es obligatoria

En el WebIDL de ApplePayRecurringPaymentRequest, managementURL está declarada como required, junto con paymentDescription y regularBilling. trialBilling, billingAgreement y tokenNotificationURL son opcionales. Por eso esta herramienta trae su propia página: /apple-pay-gestion.

Sobre tokenNotificationURL

El brief la apunta a un host de Conekta. Apple la documenta como una URL del merchant, con mutual TLS y allow-list de IPs de Apple, que recibe un GET con un eventId y obliga a un POST de vuelta a Apple para leer el detalle. Que Conekta la centralice como PSP es plausible, pero ese endpoint no existe en el spec público. Por eso aquí el campo va vacío por default: es opcional, y mandarla apuntando a algo que no responde solo agrega ruido al diagnóstico.

Nada de este flujo está publicado

Barrido del spec OpenAPI de Conekta (v2.3.0) y de las 271 páginas de developers.conekta.com: cero ocurrencias de credential_type, on_demand_eligible, merchant_decrypted, token_notifications y de los cinco códigos de error wallet.*. El endpoint /customers/:id/payment_sources existe pero su oneOf público solo acepta card, cash y spei — no apple. Es un feature interno; los nombres de campo de esta herramienta salen del brief, no de un contrato.

Ojo con un falso amigo: ya existe un campo público llamado on_demand_enabled (bandera de configuración del checkout, flujo de tarjeta). No es lo mismo que on_demand_eligible.

Fuentes