> ## Documentation Index
> Fetch the complete documentation index at: https://docs.fraudeg.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Cómo te avisamos de una decisión, y cómo compruebas que el aviso es nuestro

Registra un endpoint y FraudEG te avisa cuando algo pasa. **La configuración es del
dashboard** (KYC → Webhooks): no hay endpoint público para dar de alta el webhook. El
secreto de firma se crea antes que el endpoint.

Tu endpoint debe ser `https` y resolver a una dirección pública: la comprobamos al guardarlo y
antes de cada entrega.

## Los eventos

| Evento                   | Cuándo                                                                          |
| ------------------------ | ------------------------------------------------------------------------------- |
| `verification.decided`   | La verificación tiene una decisión, por política o por una persona de tu equipo |
| `verification.abandoned` | Nadie terminó dentro del plazo                                                  |
| `webhook.test`           | Solo cuando alguien pulsa "enviar evento de prueba"                             |

No existe `verification.created`: esa llamada la hiciste tú y tienes la respuesta.

**Una verificación enviada a revisión manda `verification.decided` dos veces.** La primera con
`decision: "REVIEW"`; la segunda cuando una persona resuelve el caso, con `APPROVED` o `DECLINED`.
No trates el primero como definitivo.

**Un `verification.abandoned` puede ser seguido por un `verification.decided`.** Damos por perdida
a una persona pasado el plazo, y hoy el enlace que le dimos puede sobrevivir a ese momento: si
vuelve y termina, se decide con normalidad. Aplica el que llegue después.

## El payload

```json theme={null}
{
  "event_id": "evt_01j9c2k4m7q8r5t3v6x9z2b4d7",
  "event": "verification.decided",
  "occurred_at": "2026-09-12T09:43:01Z",
  "data": {
    "verification_id": 19482,
    "customer_ref": "CUS-48213",
    "status": "DECIDED",
    "decision": "REVIEW",
    "decision_rule": "…",
    "flow_config_version": 4,
    "decided_at": "2026-09-12T09:43:01Z"
  }
}
```

Un aviso es un aviso, no una exportación de datos: no trae las imágenes ni las señales con las que
se decidió. Si necesitas el detalle, lee la verificación.

## Cabeceras

```text theme={null}
X-FraudEG-Event:      verification.decided
X-FraudEG-Delivery:   dlv_01j9c2k4m7q8r5t3v6x9z2b4d7
X-FraudEG-Timestamp:  1789268113
X-FraudEG-Signature:  v1=9f86d0818884...
```

`X-FraudEG-Delivery` identifica el **intento** y cambia en cada reintento. `X-FraudEG-Event` te deja
enrutar antes de parsear.

## Verificar la firma

Calcula `HMAC-SHA256(timestamp + "." + cuerpo_crudo, secreto)` en hexadecimal y compáralo contra
cada valor `v1=` de la cabecera, en tiempo constante.

```python theme={null}
import hmac, hashlib, time

def verificar(cuerpo_crudo: bytes, cabeceras: dict, secretos: list[str]) -> bool:
    timestamp = cabeceras["X-FraudEG-Timestamp"]
    if abs(time.time() - int(timestamp)) > 300:
        return False
    mensaje = timestamp.encode() + b"." + cuerpo_crudo
    presentadas = cabeceras["X-FraudEG-Signature"].split(",")
    for secreto in secretos:
        esperada = "v1=" + hmac.new(secreto.encode(), mensaje, hashlib.sha256).hexdigest()
        if any(hmac.compare_digest(esperada, p.strip()) for p in presentadas):
            return True
    return False
```

Tres cosas importan, y saltarse cualquiera es un error real:

* **Usa los bytes crudos**, antes de parsear el JSON. Pasar por un parser puede reordenar claves o
  cambiar cómo se escribe un número, y tu firma deja de coincidir por razones que nadie puede
  depurar.
* **Compara en tiempo constante.** Un `==` entre cadenas filtra la firma byte a byte.
* **Comprueba el timestamp.** Rechaza lo que tenga más de cinco minutos, o una entrega capturada
  vale para siempre.

Nunca enviamos una entrega sin firmar. Si tu compañía no tiene secreto, las entregas quedan
retenidas y el registro lo dice.

### Rotar el secreto

La cabecera puede traer **más de un** valor `v1=`, y durante una rotación trae dos. Por eso el
ejemplo recorre una lista:

1. Rota en el panel. El secreto nuevo se muestra una sola vez; el anterior sigue firmando 24 horas.
2. Agrega el nuevo a tu verificador, junto al viejo.
3. Cuando lo veas verificar, quita el viejo.

Un verificador con un solo secreto fijo se rompe el día de la rotación. Escribe el bucle ahora.

## Qué garantizamos, y qué no

* **Al menos una vez.** Deduplica por `event_id`. Dos endpoints de una misma compañía reciben el
  mismo `event_id` para el mismo hecho.
* **Sin orden garantizado entre verificaciones.** Dentro de una, gana el `occurred_at` más reciente.
* **Un 2xx es éxito.** Cualquier otra cosa se reintenta, incluido un 3xx: no seguimos redirecciones,
  porque una redirección vieja o un DNS comprometido entregaría payloads firmados a un host que
  nunca registraste.
* **Responde rápido.** Una entrega expira a los 10 segundos. Contesta 2xx y haz el trabajo después.
* **Los reintentos son acotados.** Ocho intentos repartidos en unas 28 horas; después la entrega
  queda marcada, visible en el panel y se puede reenviar a mano.
* **Un endpoint muerto se pausa.** Si nada tiene éxito durante 24 horas dejamos de enviar y te
  avisamos. Lo que te debíamos sigue en cola.
* **No garantizamos la entrega.** Por eso lo siguiente no es opcional.

## Reconcilia igual

Una vez al día, lista las verificaciones del día anterior y compáralas con tus registros. Eso cubre
lo que la entrega haya perdido, y es exactamente el caso que un presupuesto acotado de reintentos
deja abierto. Es parte del contrato, no un parche.

```bash theme={null}
curl "https://api.fraudeg.com/api/v1/verifications?status=DECIDED&from=2026-09-12T00:00:00&to=2026-09-13T00:00:00" \
  -H "X-Api-Key: tu_api_key"
```

En pre-official-integration el host es `https://sandbox-api.fraudeg.com/api`. Ver
[GET /verifications](/referencia/verificacion) y [Entornos](/entornos).
