Desarrolladores
Flujos de integración
Pago con tarjeta (3DS)

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.

1POST /payment/initbackend2Fingerprint del dispositivobrowser3POST /payment/auth-setupbackend4Device Data Collection — iframe ocultobrowser5POST /payment/processbackendAPROBADO → successRECHAZADO → failedPENDING_3DS → Step-Up6Step-Up: popup del bancobrowserProveedor 3DS → POST /3ds/callbackWizPay cobraredirige el navegador a{return_url}/payment/success · /payment/failed

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.

Referencia de endpoints usados