Ir al contenido

Eventos y webhooks

Lo que pasa en las cámaras se lee de dos formas: preguntando (GET /events, alcance events) o recibiéndolo en tu sistema con un webhook (alcance webhooks). Los dos ven lo mismo: los eventos de las cámaras que la clave puede ver.

Catálogo

Cada evento tiene un type. Al crear un webhook se eligen los que querés con su nombre exacto o con comodines: * (todo), camera.* (todo lo de las cámaras), vision.*.

Tipo Cuándo
camera.online, camera.offline La cámara se conectó o se desconectó
camera.linked Una cámara dada de alta se conectó por primera vez
camera.revoked Se dio de baja
camera.vision_threshold Una regla de un punto de interés pasó su límite
camera.lease.taken, camera.lease.released Una clave tomó o soltó el control de la cámara (details.holder, details.by)
camera.stream_started, camera.crowd.*… Los del servidor de video y sus aplicaciones
camera.<src>.<evento> Lo que escribe la cámara en su registro, en tiempo real: camera.motion.motion_start, camera.patrol.started, camera.ptz.moving, camera.voice.wake, camera.record.started, camera.upload.snapshot_sent, camera.system.boot…
vision.result Cada resultado de los puntos de interés

Los eventos que nacen de un pedido de la API llevan details.actor (api:<nombre de la clave>).

Formato

Cada envío es un POST con JSON:

{
  "type": "camera.lease.taken",
  "created_at": "2026-11-08T14:03:11-03:00",
  "space": {"id": "…", "slug": "deposito"},
  "data": {
    "camera_id": "8f0c…",
    "camera_name": "Depósito",
    "kind": "lease.taken",
    "record_id": 1234,
    "message": "La API «Robot del depósito» tomó el control.",
    "details": {"actor": "api:Robot del depósito", "holder": "Robot del depósito", "expires_at": "…"}
  }
}

Con estas cabeceras:

Cabecera Qué es
X-Tero-Event El type
X-Tero-Delivery El id del envío. Se repite en los reintentos: usalo para descartar duplicados
X-Tero-Timestamp Segundos desde 1970, cuando se firmó
X-Tero-Signature sha256=<HMAC-SHA256(secret, "<timestamp>.<cuerpo>")> en hexadecimal

Verificar la firma

  1. Tomá el cuerpo crudo, tal cual llegó: volver a serializar el JSON cambia la firma.
  2. Rechazá un X-Tero-Timestamp de más de 5 minutos de diferencia con tu reloj.
  3. Calculá el HMAC y comparalo en tiempo constante.
  4. Descartá un X-Tero-Delivery que ya procesaste: la entrega es "al menos una vez".

Los SDK lo traen hecho (verifyWebhook en TypeScript, verify_webhook en Python; ver Guías). Sin el SDK:

TypeScript / JavaScript — sdk/js/examples/portal/webhook-verify.ts

const ts = headers['x-tero-timestamp'] ?? '';
if (!/^\d+$/.test(ts) || Math.abs(now - Number(ts)) > 300) return false;
const expected = 'sha256=' + createHmac('sha256', secret).update(`${ts}.${rawBody}`).digest('hex');
const got = headers['x-tero-signature'] ?? '';
return got.length === expected.length && timingSafeEqual(Buffer.from(got), Buffer.from(expected));

Python — sdk/python/examples/portal/webhook_verify.py

ts = headers.get("X-Tero-Timestamp", "")
if not ts.isdigit() or abs((now or time.time()) - int(ts)) > 300:
    return False
expected = "sha256=" + hmac.new(secret.encode(), ts.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, headers.get("X-Tero-Signature", ""))

Entregas y reintentos

  • Si tu sistema no contesta 2xx en 5 segundos, se reintenta a los 30 s, 2 min, 10 min, 30 min y 2 h; después el envío queda failed. GET /webhooks/{id}/deliveries muestra cada uno.
  • Con 50 fallas seguidas el webhook se apaga solo y lo dice en disabled_reason; se prende de nuevo con PATCH {"enabled": true}.
  • POST /webhooks/{id}/ping manda un ping firmado para probar.
  • La URL tiene que ser https y pública: la conexión se fija a la dirección revisada y las redirecciones se vuelven a revisar. La URL se guarda cifrada (puede llevar un token tuyo) y después solo se ve el servidor. Hasta 5 webhooks por espacio, 64 KB por cuerpo.

Leer sin webhook

GET /events?since=…&kind=… y GET /cameras/{id}/events devuelven lo mismo, paginado. Sirve para ponerse al día después de un corte.