# Eventos

Cada transición de una unidad y cada cambio de escalado se escribe en un registro de solo añadir, en la **misma transacción de base de datos** que el cambio. Léelo con un cursor.

**curl**

```bash
# long-poll: returns as soon as an event after cursor 0 exists, or after 30 s
curl -sS -G "$UNITS/events" -H "$AUTH" \
  --data-urlencode "after=0" --data-urlencode "wait=30" --data-urlencode "limit=100" \
  --data-urlencode "types=unit.ready,unit.exited,unit.destroyed"
```

**TypeScript**

```ts
let after = 0;
for (;;) {
  const { status, body } = await call(
    "GET",
    `${UNITS}/events?after=${after}&wait=30&types=unit.ready,unit.exited,unit.destroyed`,
  );
  if (status === 410) { after = body.resume_after; continue; }      // cursor_expired
  for (const e of body.events) {
    console.log(e.seq, e.type, e.unit?.key, e.data);
    // persist e.seq next to your own state: it is the dedupe key
  }
  after = body.next_after;
}
```

**Python**

```python
after = 0
while True:
    status, body = call(
        "GET", f"{UNITS}/events",
        params={"after": after, "wait": 30, "types": "unit.ready,unit.exited,unit.destroyed"},
    )
    if status == 410:                      # cursor-expired
        after = body["resume_after"]
        continue
    for e in body["events"]:
        print(e["seq"], e["type"], (e["unit"] or {}).get("key"), e["data"])
        # persist e["seq"] next to your own state: it is the dedupe key
    after = body["next_after"]
```


## El sobre (contrato v1)

```json
{
  "seq": 1042,
  "id": "evt_0e8b5c6a-6f43-4b6b-8d36-6c9c1f2ad711",
  "type": "unit.exited",
  "contract_version": 1,
  "occurred_at": "2026-10-14T10:31:07Z",
  "service_instance_id": "6c9d...",
  "unit": {"id": "3f2a...", "key": "job-81", "labels": {"queue": "render"}},
  "data": {"code": 0, "restarted": false}
}
```

`seq` es **continuo y estrictamente creciente por Workload**, de modo que un cursor nunca se salta un evento que se confirma tarde. Quien consume debe ignorar los campos y los tipos de evento desconocidos: las adiciones nunca son una versión nueva.

| Tipo | `data` |
|---|---|
| `unit.created`, `unit.starting`, `unit.ready`, `unit.stopped`, `unit.idle`, `unit.busy`, `unit.draining` | |
| `unit.accepting`, `unit.not_accepting` | |
| `unit.allocated`, `unit.allocation_delivered`, `unit.allocation_undelivered` | |
| `unit.exited` | `{code, restarted}` |
| `unit.failed` | `{reason}` |
| `unit.destroyed` | `{reason}` (`expired` cuando se destruye tras estar detenida demasiado tiempo) |
| `units.scaled` | `{from, to, reason}`; `unit` es `null` |
| `rollout.started`, `rollout.completed`, `rollout.paused` | `unit` es `null` |
| `webhook.ping` (solo en entregas de webhooks) | |
| `events.gap` (solo en entregas de webhooks) | `{from_seq, to_seq}`: eventos que un webhook reactivado se perdió porque ya no se conservaban |

`unit.ready` y `unit.allocated` llevan el nombre privado y la dirección pública de la unidad, así que un matchmaker no necesita una segunda lectura. `idle` y `busy` se emiten **solo en las transiciones**.

## Parámetros

| Parámetro | Significado |
|---|---|
| `after` | el último `seq` que viste; `0` empieza por el evento conservado más antiguo |
| `types` | lista separada por comas que se conserva |
| `limit` | hasta 500 |
| `wait` | de 0 a 30 segundos: si no hay nada nuevo, retiene la petición y responde en cuanto llega un evento |

La respuesta es `{events, next_after, oldest_retained_seq}`. Envía `next_after` como el siguiente `after`. Los eventos se conservan **30 días**. Un cursor más antiguo responde `410 cursor_expired` con `resume_after`.

## La entrega es al-menos-una-vez

Guarda el último `seq` procesado **en la misma transacción que tu propio efecto** y trata un `seq` repetido como ya hecho. Es la misma regla que hace seguros los webhooks.
