Pago con tarjeta (3D Secure)
Flujo completo de un cobro con tarjeta: creación de la transacción, fingerprint del dispositivo, autenticación 3DS y captura.
Inicia la transacción (backend)
const init = await wpay.initPayment({
order_id: 'ORD-1042',
name: 'Pedido #1042',
description: 'Compra online',
amount: 350.0,
id_currency: 1, // 1 = BOB · 2 = USD
metadata: { cart_id: 88 }, // opcional, JSON libre
})Respuesta:
{
"reference": "WP-1784169095747-578AFA46",
"status": "PENDIENTE",
"msg": "TRANSACTION_CREATED",
"fingerprint": { "org_id": "<org-id-del-ambiente>", "merchant_id": "<tu-merchant-id>" }
}fingerprint.org_id y merchant_id cambian según el ambiente (test o producción) — no los
hardcodees, úsalos siempre tal como llegan en la respuesta.
Lanza el fingerprint del dispositivo (browser)
Apenas tengas la respuesta de init, carga el tag antifraude con un session_id que tú
generas (GUID sin guiones). Ese mismo valor será fingerprint_session_id en los siguientes pasos.
// GUID sin guiones
const sessionId = crypto.randomUUID().replace(/-/g, '')
function startFingerprint(orgId, merchantId) {
const tmSession = `${merchantId}${sessionId}`
const script = document.createElement('script')
script.src = `https://h.online-metrix.net/fp/tags.js?org_id=${orgId}&session_id=${tmSession}`
document.head.appendChild(script)
const iframe = document.createElement('iframe')
iframe.style.cssText = 'width:100px;height:100px;border:0;position:absolute;top:-5000px;visibility:hidden;'
iframe.src = `https://h.online-metrix.net/fp/tags?org_id=${orgId}&session_id=${tmSession}`
document.body.appendChild(iframe)
}
startFingerprint(init.fingerprint.org_id, init.fingerprint.merchant_id)Authentication Setup (backend)
Con los datos de la tarjeta ya capturados, pide el setup de autenticación:
const auth = await wpay.authSetup({
reference,
card: { type: '001', number: '4111111111111111', expiry: '12/2027', cvv: '123' },
fingerprint_session_id: sessionId,
})
// → { reference_id, access_token, device_data_collection_url }Guarda auth.reference_id: lo pasarás como cs_reference_id en /payment/process para no
repetir este paso.
Device Data Collection (browser)
El proveedor 3DS necesita recolectar datos del dispositivo. Se hace con un iframe oculto que
recibe el access_token por form POST y avisa por postMessage:
function runDDC(deviceDataCollectionUrl, accessToken) {
return new Promise((resolve) => {
const ddcOrigin = new URL(deviceDataCollectionUrl).origin
const iframe = Object.assign(document.createElement('iframe'), {
name: 'collectionIframe', width: '10', height: '10',
})
iframe.style.display = 'none'
const form = document.createElement('form')
form.method = 'POST'
form.target = 'collectionIframe'
form.action = deviceDataCollectionUrl
const input = document.createElement('input')
input.type = 'hidden'
input.name = 'JWT'
input.value = accessToken
form.appendChild(input)
document.body.append(iframe, form)
const cleanup = () => {
clearTimeout(timer)
window.removeEventListener('message', onMsg)
iframe.remove(); form.remove()
}
const onMsg = (event) => {
if (event.origin === ddcOrigin) { cleanup(); resolve() }
}
// No bloquees el checkout si el proveedor no contesta: timeout de 5s
const timer = setTimeout(() => { cleanup(); resolve() }, 5000)
window.addEventListener('message', onMsg, false)
form.submit()
})
}
await runDDC(auth.device_data_collection_url, auth.access_token)El origin del postMessage es el mismo host de device_data_collection_url. Derivarlo de la
URL, como arriba, funciona igual en test y producción sin hardcodear nada.
Procesa el pago (backend)
const result = await wpay.processPayment({
reference,
cs_reference_id: auth.reference_id, // evita repetir el auth-setup
fingerprint_session_id: sessionId,
card: { type: '001', number: '4111111111111111', expiry: '12/2027', cvv: '123' },
billing: {
address1: 'Av. Arce 2299',
address2: '',
city: 'La Paz',
state: 'LP',
country_code: 'BO',
postal_code: '0000',
currency: 'BOB',
},
personal: {
full_name: 'María Fernández',
email: '[email protected]',
phone: '70000000',
},
save_card: false, // true → tokeniza la tarjeta (ver Tarjetas guardadas)
return_url: 'https://tutienda.com',
merchant_data: { // opcional — MDDs antifraude (recomendado)
user_logged_in: 'SI',
buyer_document: '1234567',
sales_channel: 'Pagina Web',
},
})Maneja los tres resultados posibles
function handlePaymentResult(data) {
if (data.status === 'APROBADO') {
if (data.saved_card?.key) storeSavedCard(data.saved_card) // si pediste save_card
location.href = `/payment/success?reference=${reference}`
} else if (data.status === 'PENDING_3DS' && data.authentication?.step_up_url) {
show3DS(data.authentication.step_up_url, data.authentication.access_token)
} else if (data.status === 'RECHAZADO') {
location.href = `/payment/failed?reference=${reference}`
}
}Step-Up 3DS (solo si PENDING_3DS)
El banco exige que el cliente se verifique (OTP, app del banco, etc.). Abre el
step_up_url en un popup (evita iframes: Chrome bloquea por Private Network Access)
haciendo form POST del access_token como JWT:
function show3DS(stepUpUrl, accessToken) {
const popup = window.open('', 'threeds', 'width=520,height=720')
if (!popup) return alert('Activa las ventanas emergentes para completar la verificación.')
const form = popup.document.createElement('form')
form.method = 'POST'
form.action = stepUpUrl
const input = popup.document.createElement('input')
input.type = 'hidden'; input.name = 'JWT'; input.value = accessToken
form.appendChild(input)
popup.document.body.appendChild(form)
form.submit()
// Cuando el cliente termina, el proveedor 3DS hace POST a WizPay (/3ds/callback),
// WizPay ejecuta el cobro y redirige ESE popup a:
// {return_url}/payment/success?reference=... ó
// {return_url}/payment/failed?reference=...&reason=...
// Detecta la redirección (o el cierre del popup) y verifica el estado:
const poll = setInterval(async () => {
if (popup.closed) {
clearInterval(poll)
const st = await fetch(`/api/payment/status/${reference}`).then(r => r.json())
location.href = st.status === 'APROBADO'
? `/payment/success?reference=${reference}`
: `/payment/failed?reference=${reference}`
}
}, 800)
}El return_url se resuelve con esta prioridad: el return_url del body → el frontend_url
configurado en tu sistema (panel) → el global de WizPay.
Confirma el estado final (backend)
Nunca confíes solo en la redirección del navegador — confírmalo servidor a servidor:
const st = await wpay.getStatus(reference)
if (st.status === 'APROBADO') markOrderPaid(st.order_id)O mejor: suscríbete a los webhooks payment.card.success / transaction.success.
Bloqueo antifraude
Si la tarjeta está en la lista de bloqueo del sistema antifraude, /payment/process responde
400 con Tarjeta bloqueada por política antifraude, la transacción queda FALLIDO y se
dispara el webhook payment.card.fraud_blocked. Muestra al cliente un mensaje genérico de
rechazo.