# Recibir el aviso

Cuando el pago se aprueba o falla, EvePay llama a una dirección tuya. Es la única señal con la que debes despachar un pedido.

## Registra tu endpoint

En el portal, en **Ajustes → Webhook endpoint**, o por API con `POST /v1/webhook-endpoint`. Al registrarlo te damos un **secreto** que se muestra una sola vez: con él vas a verificar que el aviso es nuestro.

## Lo que te llega

```json
{
  "id": "0b0e...",
  "created": 1791068547,
  "type": "payment.completed",
  "data": {
    "paymentId": "44444444-4444-4444-8444-444444444444",
    "reference": "pedido-4417",
    "monto": 89000,
    "currency": "COP",
    "estado": "aprobado"
  }
}
```

`type` es `payment.completed` o `payment.failed`. `created` es epoch en segundos.

Con estos headers:

| Header               | Qué trae                                                      |
| -------------------- | ------------------------------------------------------------- |
| `evepay-signature`   | `t=<epoch>,v1=<hmac-sha256 en hex>`                           |
| `evepay-event`       | El tipo, repetido, para enrutar sin abrir el cuerpo.          |
| `evepay-delivery-id` | Id de la entrega. Dos entregas del mismo evento lo comparten. |

## Los eventos que existen hoy

Dos, y no más:

| Evento              | Cuándo                           |
| ------------------- | -------------------------------- |
| `payment.completed` | El banco aprobó. Aquí despachas. |
| `payment.failed`    | El pago no se completó.          |

**No hay evento de reembolso ni de expiración.** Si un cobro se reembolsa —hoy lo hace EvePay desde su consola, a petición tuya— no te llega un aviso: el cambio se ve consultando `GET /v1/pagos/:id`, donde el estado pasa a `reembolsado`. Lo decimos porque es la clase de cosa que uno asume que existe y descubre tarde.

Un cobro en efectivo tampoco avisa cuando vence el plazo del volante: se queda `pendiente`.

## Verifica la firma antes de hacer nada

Se firma `<timestamp>.<cuerpo crudo>` con HMAC-SHA256 y tu secreto. El cuerpo tiene que ser **el texto tal como llegó**: si lo parseas a objeto y lo vuelves a serializar, la firma no va a coincidir.

```php
<?php
$crudo    = file_get_contents("php://input");
$cabecera = $_SERVER["HTTP_EVEPAY_SIGNATURE"] ?? "";

parse_str(str_replace(",", "&", $cabecera), $partes);
$esperada = hash_hmac("sha256", $partes["t"] . "." . $crudo, getenv("EVEPAY_SECRETO"));

if (!hash_equals($esperada, $partes["v1"] ?? "")) {
    http_response_code(400);
    exit("Firma inválida");
}

// Rechaza avisos viejos: un atacante no puede reusar uno capturado.
if (abs(time() - (int) $partes["t"]) > 300) {
    http_response_code(400);
    exit("Aviso vencido");
}

$evento = json_decode($crudo, true);
marcarPedidoPagado($evento["data"]["reference"]);
http_response_code(200);
```

```js
import crypto from "node:crypto";

// Ojo: necesitas el cuerpo CRUDO, no el ya parseado.
app.post("/webhooks/evepay", express.raw({ type: "application/json" }), (req, res) => {
  const cabecera = req.headers["evepay-signature"] ?? "";
  const partes = Object.fromEntries(
    String(cabecera)
      .split(",")
      .map((p) => p.split("="))
  );

  const esperada = crypto
    .createHmac("sha256", process.env.EVEPAY_SECRETO)
    .update(`${partes.t}.${req.body.toString()}`)
    .digest("hex");

  const ok = partes.v1 && crypto.timingSafeEqual(Buffer.from(esperada), Buffer.from(partes.v1));

  if (!ok) return res.status(400).send("Firma inválida");
  if (Math.abs(Date.now() / 1000 - Number(partes.t)) > 300) {
    return res.status(400).send("Aviso vencido");
  }

  const evento = JSON.parse(req.body.toString());
  marcarPedidoPagado(evento.data.reference);
  res.sendStatus(200);
});
```

> **Compara con una función de tiempo constante** —`hash_equals` en PHP, `timingSafeEqual` en Node—. Comparar con `==` deja medir cuánto tarda en fallar, y con eso se adivina la firma byte a byte.

## Responde rápido y sé idempotente

Responde `200` en cuanto hayas guardado el evento, y haz el trabajo pesado después. Si no respondes `2xx`, reintentamos **hasta 3 veces** con esperas de 1, 5 y 25 segundos — así que el mismo pago puede llegarte más de una vez. Guarda el `paymentId` que ya procesaste y descarta los repetidos.

## Si el aviso no llega

Puede pasar: tu servidor estaba caído más de 31 segundos, o cambiaste de dominio. El estado real siempre se puede consultar con `GET /v1/pagos/:id`. Lo sensato es un repaso cada tanto de los pedidos que llevan horas en `pendiente`, no preguntar en bucle.

> **No despaches cuando el comprador vuelve a tu página.** El regreso del comprador es cortesía, no prueba: pudo cerrar la pestaña, o abrir esa dirección a mano. Además viaja vacío — lo comprobamos pagando de verdad en el ambiente de pruebas de la pasarela: la redirección llega sin un solo parámetro, así que esa página ni siquiera sabe qué se pagó.
