# Eventos

Publica eventos y entrégalos a los suscriptores por webhook, consulta o transmisión en vivo, con mensajes muertos y reenvío.

URL base: `https://events.inovacc.dev`

## Descripción general

Eventos permite que una parte de tu sistema anuncie que algo ocurrió, y que cada parte interesada se entere, sin que las dos se conozcan. Un publicador envía un evento como `order.paid` a `https://events.inovacc.dev`; cada **suscripción** cuyo patrón coincida con el tipo del evento lo recibe, por **webhook** a tu endpoint HTTPS, por **pull** desde tu worker, o por **transmisión en vivo** mediante un WebSocket. Los eventos que no se pueden entregar se conservan como **mensajes muertos**, que puedes inspeccionar y reproducir.

El problema que resuelve es la difusión confiable. Llamar directamente a cada servicio interesado los acopla, pierde mensajes cuando uno está caído y reintenta mal. Eventos acepta la publicación una vez, la entrega al menos una vez a cada suscripción, reintenta las entregas fallidas con espera creciente, deduplica las publicaciones repetidas y conserva lo que no pudo entregar.

Usa Eventos para reaccionar a los cambios: enviar un correo cuando se paga un pedido, refrescar una caché cuando cambia un registro, enviar una notificación a una página. Mantén el estado en sí en [Datos](/es/products/data/) y los bytes en [Archivos](/es/products/files/): un evento debe decir *qué ocurrió*, no cargar el objeto. Para la historia completa del receptor de webhooks, consulta la [guía de webhooks](/es/guides/webhooks/).

## Conceptos

**Espacio de trabajo.** Todas las rutas están bajo `/v1/workspaces/{workspace}`. El espacio de trabajo en la ruta es una afirmación que se verifica contra tu credencial: una clave de aplicación puede nombrar solo su propio espacio de trabajo, y cualquier otro responde `403 forbidden`, igual que uno que no existe.

**Eventos y el envoltorio.** Publicas `type` (palabras en minúscula separadas por puntos, como `invoice.created`, hasta 128 bytes), `data` (cualquier valor JSON) y un `idempotency_key` opcional. Los suscriptores reciben un envoltorio con `event_id` (`evt_` y 26 caracteres, asignado por Inovacc), `type`, `source` (tu aplicación, establecida a partir de la credencial), `time`, `tenant`, `idempotency_key` y `data`.

**Niveles.** Una publicación puede nombrar un `tier`: `basic` (el predeterminado), `reliable` o `advanced`. El nivel fija el mayor `data` que puede llevar un evento: 16 KiB para `basic` y `reliable`, 32 KiB para `advanced`.

**Suscripciones y patrones.** Una suscripción lista de 1 a 20 patrones en `types`, cada uno `*` (todo), un tipo exacto, o un prefijo que termina en `.*` (`order.*` coincide con `order.paid` y `order.item.added`), y un modo de entrega.

**Modos de entrega.** `webhook` envía con POST cada evento a tu URL HTTPS, firmado con el secreto de la suscripción. `pull` mantiene un buzón que tu código lee y confirma. `websocket` mantiene el mismo buzón y además lo transmite en vivo; pull sigue funcionando junto a la transmisión.

**Al menos una vez.** Cada evento llega una vez a cada suscripción que coincide en operación normal, pero un reintento o una confirmación perdida puede entregarlo de nuevo. Los receptores deduplican por `event_id`.

**Mensajes muertos.** Una entrega de webhook que sigue fallando se mueve a los mensajes muertos de tu espacio de trabajo, con el último error y el número de intentos, y se conserva 30 días.

## Cómo funciona

Toda llamada lleva `Authorization: Bearer <credential>`. La credencial es el tenant: la organización, la cuenta y la aplicación se obtienen de ella, y nada en un cuerpo puede nombrar a otra. Una **clave secreta** es para servidores y puede usar todas las rutas. Una **clave publicable** (`apb_...`) solo puede abrir una transmisión, desde un origen que su aplicación liste. El token de un usuario final no puede usar nada. Cada ruta necesita su propio permiso, como `events:event.publish`, `events:subscription.write` o `events:dead.replay`.

**Publicar.** `POST /events` con `{"event": {...}}` o `{"events": [...]}` (de 1 a 100). La respuesta es `202` con `accepted` (cada uno con su `event_id` y `duplicate`) y `rejected` (cada uno con un índice y un código); un evento incorrecto nunca bloquea a los demás. Enviar un `idempotency_key` ya usado en el espacio de trabajo en las últimas 168 horas devuelve el `event_id` original con `duplicate: true` y no entrega nada nuevo.

**Entrega por webhook.** Inovacc envía con POST el envoltorio a tu URL con `x-event-id`, `x-event-type`, `x-event-timestamp` y `x-event-signature: v1=<hex>`, un HMAC-SHA256 de la marca de tiempo y del cuerpo sin procesar. Cualquier `2xx` dentro de 10 segundos es una entrega; cualquier otra cosa se reintenta después de 10 segundos, y la espera se duplica hasta 10 minutos, hasta que se alcanza el tope de 5 reintentos y el evento se convierte en un mensaje muerto. Las redirecciones nunca se siguen, y tu URL debe ser HTTPS pública en el puerto 443.

**Pull.** `POST /subscriptions/{id}/pull` con `max` (de 1 a 100, por defecto 10) y `lease_seconds` (de 5 a 300, por defecto 30) devuelve los mensajes más antiguos, cada uno con su `delivery_count`. Un mensaje obtenido queda oculto durante su arrendamiento; confírmalo con `POST .../ack` y los ids, o regresa cuando termina el arrendamiento.

**Transmisión.** `GET /subscriptions/{id}/stream` se actualiza a un WebSocket con el subprotocolo `inovacc.v1`; un navegador pasa su clave como un segundo subprotocolo, `bearer.<key>`. Cada trama es un envoltorio; responde `{"ack": [...]}` para confirmar. Una trama sin confirmar se vuelve a enviar después de su arrendamiento.

**Monitoreo.** `GET /stats` devuelve `published`, `completed`, `depth`, `oldest_pending_age_seconds`, `retries`, `retry_rate` y `dlq_inflow` para el espacio de trabajo. Cada llamada se registra en el [Registro de actividad](/es/products/activity/). Lo que se mide es el evento entregado.

## Primeros pasos

Necesitas una clave secreta vinculada a tu espacio de trabajo con los permisos de eventos ([Autenticación](/es/guides/authentication/)).

1. **Crea una suscripción pull.** `POST /v1/workspaces/{workspace}/subscriptions` con `{"types":["demo.*"],"delivery":{"mode":"pull"}}` (consulta [los ejemplos](#example)). La respuesta es `201` con un `subscription_id`.
2. **Publica.** `POST .../events` con `{"event":{"type":"demo.hello","data":{"n":1}}}`. La respuesta es `202` con una entrada en `accepted` y su `event_id`.
3. **Obtén con pull.** `POST .../subscriptions/{id}/pull` con `{}`. Tu evento está en `messages`, con `delivery_count` 1.
4. **Confirma.** `POST .../subscriptions/{id}/ack` con `{"ids":["<event_id>"]}` responde `{"acked":1}`; un segundo pull viene vacío.
5. **Agrega un webhook.** Crea una segunda suscripción con `{"mode":"webhook","url":"https://<your endpoint>"}`, conserva el `signing_secret` que la respuesta muestra una sola vez, y sigue la [guía de webhooks](/es/guides/webhooks/) para verificar las entregas.

## Casos de uso

**Cumplimiento de pedidos.** El pago publica `order.paid` con el id del pedido en `data` y el id del pago como `idempotency_key`. Una suscripción por webhook para `order.*` llega al sistema del almacén; una suscripción pull alimenta el trabajo de facturación. Un pago reintentado por el cliente se publica una sola vez.

**Actualizaciones en vivo en una página.** Un panel abre una transmisión en una suscripción `websocket` para `ticket.*` con una clave publicable desde su propio origen, muestra cada ticket nuevo a medida que llega la trama, y la confirma.

**Recuperación tras una caída.** El endpoint de un socio estuvo caído una hora. Sus entregas terminaron en mensajes muertos con `last_error` `webhook_timeout`. Cuando vuelve, tu equipo lista `GET /dead` y reproduce cada evento; va solo a las suscripciones que nunca lo recibieron.

## Límites y precios

| Límite | Valor |
|---|---|
| Eventos por publicación | 100 |
| Cuerpo de la solicitud | 5 MiB |
| `data` por evento | 16 KiB (`basic`, `reliable`), 32 KiB (`advanced`) |
| Ventana de deduplicación de `idempotency_key` | 168 horas |
| Suscripciones por espacio de trabajo; patrones por suscripción | 50; 20 |
| Mensajes por pull; arrendamiento | 100; de 5 a 300 segundos |
| Tiempo de respuesta del webhook | 10 segundos |
| Reintentos | desde 10 segundos, duplicándose hasta 10 minutos; tope de 5, luego mensaje muerto |
| Mensajes muertos conservados | 30 días |
| Trama de transmisión desde el cliente | 49,152 bytes |
| Tasa | 600 llamadas por minuto por credencial, por ubicación |

**Precios:** a consultar. La unidad de precio es el **evento entregado**.

## Errores

| Estado | Código | Qué significa y qué hacer |
|---|---|---|
| 400 | `invalid_body` | El cuerpo está mal formado, o nombra `source` (proviene de tu credencial). |
| 400 | `invalid_query` | Solo `GET /dead` acepta una consulta (`limit`). |
| 400 | `invalid_subscription`, `invalid_delivery`, `invalid_limit`, `invalid_id` | Corrige la suscripción, la entrega o el parámetro. |
| 400 | `invalid_url`, `url_not_https`, `url_has_credentials`, `url_port_not_allowed`, `url_destination_blocked` | La URL del webhook debe ser HTTPS pública en el puerto 443, sin credenciales. |
| 401 | `invalid_credentials` | Envía una credencial válida. |
| 403 | `forbidden`, `account_required` | Falta un permiso o el espacio de trabajo es incorrecto; usa una clave de aplicación. |
| 403 | `publishable_key_not_allowed`, `origin_not_allowed`, `secret_key_in_browser` | Una clave publicable solo puede transmitir, desde un origen listado; mantén las claves secretas en servidores. |
| 404 | `subscription_not_found`, `dead_event_not_found` | No existe en tu espacio de trabajo. |
| 409 | `wrong_delivery_mode`, `too_many_subscriptions`, `secret_not_managed` | La operación no encaja con el modo de entrega, o estás en el tope de 50 suscripciones. |
| 413 | `payload_too_large` | El cuerpo supera 5 MiB. |
| 426 | `upgrade_required` | La ruta de transmisión necesita una actualización a WebSocket. |
| 429 | `rate_limited`, `too_many_streams` | Reduce el ritmo; cierra los sockets que no necesites. |
| 503 | `service_unavailable`, `events_disabled`, `signing_unavailable` | Reintenta más tarde; una publicación reintentada con las mismas claves reutiliza sus ids. |

Un evento rechazado dentro de un `202` lleva su propio código: `invalid_event`, `unknown_field`, `invalid_type`, `invalid_idempotency_key`, `payload_too_large` o `data_required`.

## Buenas prácticas

- **Envía un `idempotency_key`** derivado de lo que ocurrió (el id del pago, la versión del registro), para que una publicación reintentada no sea un segundo evento.
- **Deduplica por `event_id`** en cada receptor: la entrega es al menos una vez.
- **Pon ids en `data`, no objetos**: obtén el estado actual desde [Datos](/es/products/data/) cuando manejes el evento.
- **Responde los webhooks rápido** con un `2xx` y haz el trabajo después; 10 segundos es el límite.
- **Verifica la firma de cada webhook** antes de analizar el cuerpo ([guía de webhooks](/es/guides/webhooks/)).
- **Confirma los mensajes obtenidos con pull** después de procesarlos, y elige un arrendamiento más largo que tu tiempo de procesamiento.
- **Vigila `depth` y `dlq_inflow`** en `/stats`, y reproduce los mensajes muertos una vez corregida la causa.

## Relacionados

- [Guía de webhooks](/es/guides/webhooks/): recibe, verifica y reproduce entregas
- [Datos](/es/products/data/), [Registro de actividad](/es/products/activity/)
- [Autenticación](/es/guides/authentication/), [Errores](/es/guides/errors/), [Límites](/es/guides/limits/)

## Endpoints

- POST /v1/workspaces/{workspace_id}/events
- GET /v1/workspaces/{workspace_id}/subscriptions
- POST /v1/workspaces/{workspace_id}/subscriptions
- DELETE /v1/workspaces/{workspace_id}/subscriptions/{subscription_id}
- POST /v1/workspaces/{workspace_id}/subscriptions/{subscription_id}/rotate-secret
- POST /v1/workspaces/{workspace_id}/subscriptions/{subscription_id}/pull
- POST /v1/workspaces/{workspace_id}/subscriptions/{subscription_id}/ack
- GET /v1/workspaces/{workspace_id}/subscriptions/{subscription_id}/stream
- GET /v1/workspaces/{workspace_id}/dead
- POST /v1/workspaces/{workspace_id}/dead/{event_id}/replay
- GET /v1/workspaces/{workspace_id}/stats

## Ejemplo

```sh
curl -X POST "https://events.inovacc.dev/v1/workspaces/{workspace_id}/events" -H "Authorization: Bearer $INOVACC_API_KEY" -H "Content-Type: application/json" -d '{}'
```
