> ## 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.

# Enviar eventos a Brein

> Manda contactos desde cualquier sistema a tu CRM con un webhook firmado.

Cualquier sistema puede mandarle cosas a Brein por una sola puerta: tu backend, una herramienta sin código o un agente. Cada envío es un **CloudEvent 1.0** firmado con **Standard Webhooks 1.0.0**. Brein lo guarda tal cual llegó, lo aplica a tu CRM y deja rastro de todo lo que pasó.

```text theme={"theme":"github-light"}
Tu sistema  →  POST firmado  →  Fuente  →  Llegada  →  Contacto · Empresa · Oportunidad
```

## 1. Crea una Fuente

Una **Fuente** es por donde entran cosas a Brein. Créala en **Ajustes → Integraciones**. Solo el dueño o un admin del espacio puede hacerlo.

Al crearla ves dos cosas:

* **La dirección** a la que envías: `https://api.trybrein.com/api/v1/door/<id de la Fuente>`.
* **El secreto**, `whsec_` seguido de base64. Se muestra **una sola vez**. Guárdalo en tu gestor de secretos; nunca en un navegador ni en un repositorio.

Si quieres que cada Llegada abra también una Oportunidad, elige el Pipeline y la etapa al crearla. Se puede cambiar después.

## 2. Arma el evento

El cuerpo es un CloudEvent 1.0 en JSON, de hasta 256 KiB.

```json theme={"theme":"github-light"}
{
  "specversion": "1.0",
  "id": "lead-48213",
  "source": "https://tuempresa.mx/registro",
  "type": "contact.created",
  "time": "2026-10-15T18:04:11Z",
  "data": {
    "name": "Ana Torres",
    "email": "ana@constructoranorte.mx",
    "phone": "+52 81 5555 0101",
    "role": "Directora de compras",
    "company": "Constructora Norte",
    "website": "constructoranorte.mx"
  }
}
```

| Campo | Regla |
| - | - |
| `specversion` | `"1.0"` |
| `id` | Tu id del evento, hasta 200 caracteres. `source` + `id` identifican el evento: si lo mandas dos veces, es el mismo. |
| `source` | De dónde viene, hasta 300 caracteres. |
| `type` | Un tipo del [catálogo](/es/api/events#catálogo). Hoy se aplican al CRM `contact.created` y `contact.updated`; los demás se guardan y quedan apartados. |
| `subject` | Opcional, hasta 160 caracteres. |
| `time` | Opcional. Cuándo pasó, en ISO 8601. |
| `data` | Para `contact.*`: `name`, `email`, `phone`, `role`, `company`, `website`. Todos texto y opcionales, pero hace falta al menos `email` o `phone`. |

`data` sí lleva correo y teléfono: es lo que identifica a la persona. Lo que Brein escribe después en sus propios eventos nunca los lleva.

## 3. Fírmalo y envíalo

Se firma `webhook-id`, `webhook-timestamp` y el cuerpo, **byte por byte como lo envías**, con HMAC-SHA256. La llave es el secreto sin `whsec_`, decodificado de base64.

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

const secret = process.env.BREIN_SOURCE_SECRET // whsec_…
const key = Buffer.from(secret.slice('whsec_'.length), 'base64')

const body = JSON.stringify(event) // serializa una vez y envía estos mismos bytes
const id = `msg_${crypto.randomUUID()}` // sin puntos ni espacios, hasta 200 caracteres
const timestamp = Math.floor(Date.now() / 1000).toString()
const signature = crypto.createHmac('sha256', key).update(`${id}.${timestamp}.${body}`).digest('base64')

const response = await fetch(`https://api.trybrein.com/api/v1/door/${sourceId}`, {
  method: 'POST',
  headers: {
    'content-type': 'application/json',
    'webhook-id': id,
    'webhook-timestamp': timestamp,
    'webhook-signature': `v1,${signature}`,
  },
  body,
})
```

Cualquier librería de [Standard Webhooks](https://www.standardwebhooks.com) firma igual. Si reintentas, **usa el mismo `webhook-id`**: Brein lo reconoce y no duplica nada.

## 4. Lee la respuesta

| Código | Significa | ¿Reintentar? |
| - | - | - |
| `202` | Recibido. Responde `{ "arrival": "<id>" }` y se procesa en cola. | — |
| `200` | Ya lo tenías: mismo `webhook-id` o mismo `source` + `id`. Responde la misma Llegada y su estado; no hace nada nuevo. | no |
| `400` | Faltan o están mal los encabezados de firma (`webhook_headers`), o el cuerpo no es un CloudEvent 1.0 de un tipo del catálogo (`invalid_event`). | no, corrígelo |
| `401` | La firma no coincide, la Fuente no existe, o la marca de tiempo está a más de 5 minutos de la hora de Brein (`invalid_signature`, `stale_timestamp`). | no, corrígelo |
| `410` | La Fuente está archivada. | no |
| `413` | El cuerpo pasa de 256 KiB. | no |
| `429` | Más de 120 envíos por minuto desde tu IP. Espera lo que dice `Retry-After`. | sí |
| `503` | La Fuente está pausada. | sí, más tarde |

Un `401` nunca dice si la Fuente existe. Durante 24 horas después de rotar el secreto, Brein acepta firmas con el secreto nuevo y con el anterior: puedes cambiarlo sin cortar el envío.

## 5. Qué pasa después

Cada envío es una **Llegada** con su recorrido. Las ves en la página de su Integración, en **Ajustes → Integraciones**, o con la herramienta `arrival_read` desde tu agente.

| Estado | Qué pasó |
| - | - |
| aceptada | Llegó al CRM: Contacto, su Empresa por dominio y, si la Fuente lo pide, una Oportunidad. |
| por decidir | El correo es de un Contacto y el teléfono de otro. Una persona elige. |
| apartada | No se pudo aplicar tal cual: un tipo que todavía no se aplica, o sin correo ni teléfono. Se puede reprocesar. |
| sin persona | Solo en una Fuente de tipo `webhook` o de plantilla: el aviso no trae correo, teléfono ni LinkedIn, o su correo es de prueba o de buzón general. Se puede reprocesar desde la Fuente. En la API y el MCP es `no_person`. |
| no entendida | Solo en una Fuente de tipo `webhook` o de plantilla: el aviso no cuadra con el mapeo, o la Fuente todavía no tiene mapeo. Se puede reprocesar desde la Fuente. En la API y el MCP es `unmapped`. |
| fallida | Falló cinco veces seguidas. Se puede reprocesar. |
| rechazada | Firma mala, evento inválido, o la persona pidió que borraran sus datos. Es final. |

Nada se pierde sin dejar rastro: todo envío termina en uno de esos estados, y cada paso queda en el [libro de eventos](/es/api/events).

## Fuentes de tipo `webhook`

Además del contrato firmado de arriba, una Fuente puede ser de tipo `webhook`: recibe el aviso de cualquier herramienta tal como lo manda, sin CloudEvent, y un **mapeo** que la persona confirma dice qué campo es cada cosa. Es lo que usan las [Integraciones](/es/integraciones). Se crean con `POST https://api.trybrein.com/api/v1/workspaces/<slug>/door/sources`, con `type: "webhook"` y, si quieres, una `template` (`calcom`, `calendly`, `stripe`, `tally`, `typeform`, `jotform`, `webflow`, `shopify`, `hotmart`, `clerk` o `posthog`).

Cómo se verifica depende de la Fuente:

| Verificación | Dirección que usa la herramienta | Autenticación |
| - | - | - |
| Plantilla que firma | `POST /api/v1/door/<id de la Fuente>` | La firma de la herramienta, en sus propios encabezados. |
| Standard Webhooks | `POST /api/v1/door/<id de la Fuente>` | Los encabezados `webhook-*` de arriba. |
| Dirección secreta | `POST /api/v1/door/<id de la Fuente>/<token>` | El token de la dirección. Brein guarda solo su huella. Se muestra **una sola vez**, al crear la Fuente o al rotarla; rotar invalida el anterior al instante. |

El cuerpo puede ser JSON, `application/x-www-form-urlencoded` o `multipart/form-data`, hasta 256 KiB. Una firma o un token que no corresponden dan `401`, igual que una Fuente que no existe. Las respuestas son las de la tabla de arriba, con estas diferencias:

* `202` o `200` con `{ "arrival": "<id>", "status": "unmapped" }` cuando la Fuente aún no tiene mapeo: el aviso se guarda como ejemplo.
* `200` con `{ "ignored": true, "reason": "unsupported_event" }` cuando el tipo del aviso no es de los que el mapeo acepta. No se crea Llegada.
* Un aviso con una lista de personas (un lote) responde `{ "arrivals": [ { "arrival", "status", "duplicate" } ], "ignored": <número> }`, con una Llegada por elemento.
* Un duplicado se reconoce por el id del aviso que el mapeo señala, o por una huella de su contenido.

Para leer una Fuente con su ejemplo y su propuesta de mapeo, confirmar el mapeo y reprocesar, todo dentro de tu espacio y con tu sesión:

| Ruta | Para qué |
| - | - |
| `GET /api/v1/workspaces/<slug>/door/templates` | Lista las plantillas. |
| `GET /api/v1/workspaces/<slug>/door/sources/<id>` | La Fuente con su mapeo, su ejemplo, su propuesta y sus campos nuevos sin decidir. |
| `PUT /api/v1/workspaces/<slug>/door/sources/<id>/mapping` | Confirma el mapeo. Dueño o admin. Cada confirmación sube la versión. |
| `POST /api/v1/workspaces/<slug>/door/sources/<id>/reprocess-unmapped` | Reprocesa las Llegadas `unmapped` y `no_person`. Cuerpo opcional: `{ "arrival_id": "<id>" }`. Responde `{ requeued, changed, unchanged, expired, created }`. |

El aviso crudo y el ejemplo se borran a los 30 días; sin ellos no hay de dónde reprocesar (`expired`).

## Prueba sin riesgo

Un **espacio de prueba** copia la estructura de tu espacio (Pipelines, Formularios y Fuentes) sin tus datos ni tus miembros. Ahí no se gastan créditos, no se envía correo, no se llama a proveedores y no se entrega nada a un Destino. Lo crea el dueño o un admin con `POST https://api.trybrein.com/api/v1/workspaces/<slug>/sandboxes`; hay hasta cinco por espacio. La respuesta trae los secretos nuevos de sus Fuentes, una sola vez.

Dentro de un espacio de prueba puedes mandar una Llegada de ejemplo a cualquier Fuente sin firmar nada, con la herramienta `arrival_send_test` o con `POST …/workspaces/<slug>/door/sources/<id>/test-arrivals` y `{ "type": "contact.created" }`.


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