Documentación de integración

Tres niveles, un mismo núcleo: botón integrado, redirección, o la API directa. Elige cuánto trabajo quieres hacer — la seguridad no cambia entre niveles.

🔒
El navegador nunca es la fuente de la verdad.

El popup solo le dice a tu interfaz qué pintar. Lo que de verdad pasó lo confirman dos cosas, ambas servidor a servidor: el webhook firmado y la consulta de estado. Un comercio que le cree al navegador es un comercio al que le van a falsificar un "pago aprobado" con la consola abierta.

Autenticación

Cada llamada de tu servidor lleva tu clave secreta como Bearer. Nunca la pongas en el navegador.

Authorization: Bearer ezk_live_TU_CLAVE_SECRETA
Tu clave la emitimos nosotros al darte de alta — todavía no hay autorregistro. Pídela desde la sección de contacto.

1 · Crear una sesión de cobro

POST /v1/sessions — devuelve el sessionId para el botón y el processUrl por si prefieres redirigir.

# El monto va entero, en la unidad menor de la moneda: 12450 = $124,50
curl -X POST https://clientes.ezipagos.com/v1/sessions \
  -H "Authorization: Bearer ezk_live_TU_CLAVE" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": { "amount": 12450, "currency": "USD" },
    "reference": "A-2048",
    "description": "Plan Aurora · 12 meses"
  }'
const res = await fetch('https://clientes.ezipagos.com/v1/sessions', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.EZIPAGOS_SECRET_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    amount: { amount: 12450, currency: 'USD' },
    reference: 'A-2048',
    description: 'Plan Aurora · 12 meses',
  }),
});
const { data } = await res.json();
// data.sessionId  → para el botón
// data.processUrl → para redirigir directo
$ch = curl_init('https://clientes.ezipagos.com/v1/sessions');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('EZIPAGOS_SECRET_KEY'),
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'amount' => ['amount' => 12450, 'currency' => 'USD'],
        'reference' => 'A-2048',
        'description' => 'Plan Aurora · 12 meses',
    ]),
]);
$body = json_decode(curl_exec($ch), true);
$sessionId = $body['data']['sessionId'];
import os, requests

response = requests.post(
    "https://clientes.ezipagos.com/v1/sessions",
    headers={"Authorization": f"Bearer {os.environ['EZIPAGOS_SECRET_KEY']}"},
    json={
        "amount": {"amount": 12450, "currency": "USD"},
        "reference": "A-2048",
        "description": "Plan Aurora · 12 meses",
    },
)
session_id = response.json()["data"]["sessionId"]

2 · Embeber el botón

El popup se abre en el clic, antes de que tu backend responda — si esperas al fetch, el navegador lo bloquea. El SDK ya resuelve esto.

<script src="https://js.ezipagos.com/v1"></script>
<div id="ezipagos-button"></div>

<script>
  Ezipagos.Button({
    // Tu backend crea la sesión con tu clave secreta y devuelve el sessionId.
    createSession: () =>
      fetch('/api/pagos/crear', { method: 'POST' })
        .then((r) => r.json())
        .then((d) => d.sessionId),

    // Señal de interfaz: el pagador terminó. Todavía no es dinero confirmado.
    onApprove: ({ sessionId }) =>
      fetch(`/api/pagos/${sessionId}/confirmar`, { method: 'POST' })
        .then(() => { window.location = '/gracias'; }),

    onPending: () => mostrarAviso('Tu pago está en revisión, te avisamos por correo.'),
    onCancel: () => {},
    onError: (err) => mostrarAviso(err.message),
  }).render('#ezipagos-button');
</script>
En móvil el mismo código redirige en la pestaña en vez de abrir un popup — Safari y el 3DS del banco no conviven bien con ventanas emergentes. No hace falta que cambies nada.

3 · Verificar el webhook

El navegador puede morir después de que el banco aprobó. El webhook llega igual, firmado con HMAC-SHA256 sobre timestamp.cuerpo, en el header Ezzipay-Signature con forma t=…,v1=….

import crypto from 'node:crypto';

function verifyWebhook(rawBody, header, secret, toleranceSeconds = 300) {
  const [tPart, vPart] = header.split(',');
  const timestamp = Number(tPart.split('=')[1]);
  const signature = vPart.split('=')[1];
  const expected = crypto.createHmac('sha256', secret)
    .update(`${timestamp}.${rawBody}`).digest('hex');
  const fresco = Math.abs(Date.now() / 1000 - timestamp) <= toleranceSeconds;
  return fresco && crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}

// Monta esta ruta con el body CRUDO (express.raw), nunca ya parseado a JSON.
app.post('/webhooks/ezipagos', express.raw({ type: 'application/json' }), (req, res) => {
  const header = req.header('ezzipay-signature');
  if (!verifyWebhook(req.body.toString('utf8'), header, process.env.EZIPAGOS_WEBHOOK_SECRET)) {
    return res.sendStatus(400);
  }
  const event = JSON.parse(req.body);
  res.sendStatus(200);
});
function verify_webhook(string $rawBody, string $header, string $secret, int $tolerance = 300): bool {
    [$tPart, $vPart] = explode(',', $header);
    $timestamp = (int) explode('=', $tPart)[1];
    $signature = explode('=', $vPart)[1];
    $expected = hash_hmac('sha256', "{$timestamp}.{$rawBody}", $secret);
    $fresco = abs(time() - $timestamp) <= $tolerance;
    return $fresco && hash_equals($expected, $signature);
}

$rawBody = file_get_contents('php://input');
$header  = $_SERVER['HTTP_EZZIPAY_SIGNATURE'] ?? '';
if (!verify_webhook($rawBody, $header, getenv('EZIPAGOS_WEBHOOK_SECRET'))) {
    http_response_code(400);
    exit;
}
$event = json_decode($rawBody, true);
http_response_code(200);
import hmac, hashlib, time, os

def verify_webhook(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
    t_part, v_part = header.split(",")
    timestamp = int(t_part.split("=")[1])
    signature = v_part.split("=")[1]
    payload = f"{timestamp}.{raw_body.decode()}".encode()
    expected = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()
    fresco = abs(time.time() - timestamp) <= tolerance
    return fresco and hmac.compare_digest(expected, signature)

@app.route("/webhooks/ezipagos", methods=["POST"])
def webhook():
    header = request.headers.get("Ezzipay-Signature", "")
    if not verify_webhook(request.get_data(), header, os.environ["EZIPAGOS_WEBHOOK_SECRET"]):
        abort(400)
    event = request.get_json()
    return "", 200

Referencia rápida

MétodoRutaQué hace
POST/v1/sessionsAbre una sesión de cobro
GET/v1/sessions/:idConsulta el estado — la verdad, servidor a servidor
GET/v1/connectorsBancos habilitados para tu comercio