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

# Recibir eventos en tu servidor

> Un Destino entrega los eventos de tu espacio a tu servidor, firmados y con reintentos.

Un **Destino** es por donde salen los eventos de Brein hacia otro sistema. Le das una dirección y los tipos de evento que te importan; Brein le manda cada uno, firmado con **Standard Webhooks 1.0.0**, en cuanto ocurre.

## Crear un Destino

Por la API, con una llave de API del dueño o de un admin del espacio.

* **Dirección:** `https` pública, puerto 443 u 8443, hasta 2048 caracteres. No se aceptan direcciones privadas ni internas, y Brein vuelve a revisarlo en cada envío.
* **Eventos:** de 1 a 12 tipos del [catálogo](/es/api/events#catálogo). Se pueden cambiar después; un tipo agregado recibe solo lo que pase desde ese momento, no historia.
* **Secreto:** `whsec_…`, se muestra **una sola vez**.

Un espacio de prueba no tiene Destinos: ahí nada sale de Brein.

## Lo que recibes

```http theme={"theme":"github-light"}
POST /tu/ruta HTTP/1.1
content-type: application/cloudevents+json
webhook-id: 4d6f0c2e-6a1b-5f7e-9c3d-2b8a7e1f0a55
webhook-timestamp: 1792097051
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=

{ "specversion": "1.0", "id": "4d6f0c2e-…", "type": "opportunity.moved", … }
```

El cuerpo es el [CloudEvent](/es/api/events#la-forma) completo. `webhook-id` es el `id` del evento y **no cambia entre reintentos**: úsalo para descartar repetidos.

## Verificar la firma

Verifica antes de confiar en el cuerpo. Usa los bytes tal cual llegaron, no un JSON vuelto a serializar.

```js theme={"theme":"github-light"}
import crypto from 'node:crypto'

function verify(rawBody, headers, secret) {
  const id = headers['webhook-id']
  const timestamp = headers['webhook-timestamp']
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false

  const key = Buffer.from(secret.slice('whsec_'.length), 'base64')
  const expected = crypto.createHmac('sha256', key).update(`${id}.${timestamp}.${rawBody}`).digest('base64')

  return headers['webhook-signature'].split(' ').some((part) => {
    const [version, signature] = part.split(',')
    return version === 'v1' && signature.length === expected.length &&
      crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
  })
}
```

Puede venir más de una firma separada por espacio: cuando rotas el secreto, durante 24 horas Brein firma con el nuevo y con el anterior. Acepta si cualquiera coincide.

## Responder y reintentos

Responde `2xx` rápido y procesa después. Brein espera 10 segundos como máximo y no sigue redirecciones.

| Tu respuesta | Qué hace Brein |
| - | - |
| `2xx` | Entregada. |
| `410` | Deja de enviar: el Destino se **deshabilita** y lo pendiente se abandona. |
| Cualquier otra, 10 s sin respuesta o un error de red | Reintenta. |

Son diez intentos. Después de cada fallo espera 5 s, 5 min, 30 min, 2 h, 5 h, 10 h, 14 h, 20 h y 24 h: unas 75 horas del primero al último. Cada reintento lleva el mismo `webhook-id` y una firma nueva. Si las últimas cinco entregas terminadas se agotaron todas, el Destino se deshabilita.

**Un mismo evento puede llegar más de una vez.** Así funciona Standard Webhooks: es mejor que perderlo. Descarta por `webhook-id`. El orden tampoco está garantizado: usa `time` y el `subject`.

## Un agente que responde no se avisa a sí mismo

Un agente que recibe una entrega suele escribir de vuelta por el MCP o la API, y esa escritura puede producir un evento del mismo tipo que escucha. Para cortar el bucle, manda el id del evento que recibiste en el encabezado `X-Brein-Cause` de cada llamada con que respondes (en el MCP, el mismo encabezado de la petición). Brein guarda ese evento como `causationid` de lo que escribas y nunca entrega a tu Destino un evento cuya causa ya le entregó. Los otros Destinos sí lo reciben. Un id que no es un evento de tu espacio se rechaza con `X-Brein-Cause` como campo.

## Tipos de evento retirados

El recorrido de una Llegada (`arrival.received`, `.accepted`, `.decided`, `.rejected`, `.quarantined`, `.reprocessed`) ya no está en el catálogo. Un Destino que estaba suscrito a uno conserva la suscripción guardada pero no recibe nada por ella, y ya no se ve en su lista; al guardar la lista de nuevo se quita. Las entregas que ya estaban en cola se envían.

## Revisar, probar y reenviar

Todo por la API, con la misma llave:

| Acción | Ruta |
| - | - |
| Ver entregas recientes | `GET …/door/destinations/<id>/deliveries` |
| Ver los intentos de una entrega (hora, duración, código) | `GET …/door/destinations/<id>/deliveries/<delivery>/attempts` |
| Reenviar una entrega | `POST …/door/destinations/<id>/deliveries/<delivery>/redeliver` |
| Mandar un evento de ejemplo ahora | `POST …/door/destinations/<id>/test` con `{ "type": "<un tipo suscrito>" }` |
| Cambiar los eventos | `PATCH …/door/destinations/<id>` con `{ "event_types": [...] }` |
| Pausar · reanudar | `POST …/door/destinations/<id>/pause` · `/resume` |
| Reactivar uno deshabilitado | `POST …/door/destinations/<id>/resume` |
| Rotar el secreto | `POST …/door/destinations/<id>/rotate` |

Las rutas empiezan con `https://api.trybrein.com/api/v1/workspaces/<slug>`. Brein no guarda lo que tu servidor responde, solo el código y el tiempo. Un Destino pausado guarda lo pendiente y lo envía al reanudar. El evento de ejemplo no se anota en tu libro.

¿Te pierdes algo por una caída larga? [Lee el libro](/es/api/events#leer-el-libro) desde tu último cursor.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.