Saltar a contenido

Webhooks y el feed por la API

Esta página es para quien hace que un programa propio siga lo que cambia en OS. Muestra cómo lees el feed, registras un webhook, compruebas una entrega y qué haces cuando algo sale mal. Qué son los eventos y los webhooks está en Eventos y webhooks. Cómo haces una solicitud a OS (dirección, inicio de sesión, páginas, errores) está en La API de acceso.

Leer el feed

curl -s "$OS/api/v1/organisations/$ORG/events?limit=100" -H "Authorization: Bearer $TOKEN"
{"items": [
   {"id": "evt_...", "type": "directory.invitation.created", "organisation_id": "org_...",
    "cursor": "41", "created_at": "2026-10-17T09:14:03Z", "refs": {"invitation_id": "inv_..."}}
 ],
 "next_cursor": "41"}
  • Sin cursor empiezas por el evento más antiguo que OS aún conserva. Procesa la página, guarda next_cursor y la próxima vez pide ?cursor= con ese valor.
  • next_cursor también aparece en una página corta: entonces apunta más allá de todo lo que OS registró hasta ahora, también de los tipos que omitiste con types. Si no hay nada nuevo, recibes una página vacía y el mismo next_cursor; vuelve a pedir más tarde.
  • types limita el feed a ciertos tipos: ?types=directory.membership.removed.
  • limit es como máximo 100. Una página puede contener menos eventos que limit mientras next_cursor avanza: solo ves lo que puedes ver.
  • 410 significa que los eventos posteriores a tu cursor ya no se conservan. Vuelve a leer el estado y empieza sin cursor.

Necesitas see sobre event.

Registrar un webhook

curl -s -X POST "$OS/api/v1/organisations/$ORG/webhooks" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"url": "https://crm.example.com/os-events", "label": "CRM",
       "kinds": ["directory.membership.created", "directory.membership.removed"],
       "actor_membership_id": "mem_..."}'
{"id": "whk_...", "label": "CRM", "url": "https://crm.example.com/os-events",
 "kinds": ["directory.membership.created", "directory.membership.removed"],
 "actor_membership_id": "mem_...", "state": "active", "consecutive_failures": 0,
 "allowed_actions": ["see", "write", "delete"], "secret": "whsec_..."}
  • Guarda secret enseguida. OS muestra el secreto solo en esta respuesta, nunca más.
  • kinds vacío significa todos los tipos, también los que se añadan más adelante.
  • actor_membership_id es la cuenta en cuyo nombre recibe el webhook: un miembro activo de tu organización que puedes modificar y del que tienes todo lo que tiene.
  • Una organización tiene como máximo 20 webhooks.
Respuesta Significado
201 Registrado; Location apunta al webhook.
403 No tienes todo lo que tiene la cuenta.
409 Los webhooks no están activados en este entorno, o tu organización ya tiene 20.
422 La dirección no es https, nombra una dirección IP en lugar de un nombre de host, contiene un usuario, una contraseña o #, o el host lleva a una dirección privada, interna o bloqueada (webhook_host_refused); un tipo no existe; la cuenta no es un miembro activo.

Modificar, reemplazar, eliminar

Solicitud Qué hace
GET .../webhooks los webhooks, sin secreto
GET .../webhooks/{id} un webhook, con ETag
PATCH .../webhooks/{id} modificar label, url, kinds o actor_membership_id, con If-Match: "<versión>"; una dirección nueva y una cuenta nueva las comprueba OS como al registrar
DELETE .../webhooks/{id} desaparecen el webhook y su registro; los eventos siguen en el feed
POST .../webhooks/{id}/rotate-secret {"overlap_hours": 24}: un secreto nuevo, solo en esta respuesta; el antiguo sigue firmando durante ese tiempo (de 0 a 168 horas, 0 es de inmediato)
POST .../webhooks/{id}/enable un webhook que falla o desactivado vuelve a estar activo; 409 mientras la cuenta no esté activa
POST .../webhooks/{id}/test envía un evento events.webhook.tested solo a este webhook, en cualquier estado; 202 con delivery_id

Todas estas solicitudes piden write sobre webhook (eliminar delete, leer see).

Recibir una entrega

OS envía cada evento como un POST con el evento en JSON y estas cabeceras:

Cabecera Valor
Content-Type application/json
User-Agent artific-os
Artific-Signature t=<segundos-unix>,v1=<hex>, y durante un reemplazo un segundo v1
Idempotency-Key el id de la entrega, el mismo en cada intento
Artific-Event-Id el id del evento

Responde con un 2xx en cuanto hayas guardado el evento con seguridad; haz el trabajo después. OS espera tu respuesta como máximo 10 segundos y no lee su contenido. Otro estado, una redirección o ninguna respuesta cuenta como fallo, y OS reintenta.

Puedes recibir la misma entrega más de una vez, por ejemplo si tu respuesta se perdió por el camino. Descarta una entrega cuya Idempotency-Key ya procesaste.

Comprobar la firma

La firma es el HMAC-SHA256 de la hora, un punto y el contenido exacto, con el secreto, en hexadecimal. Acepta una entrega cuando uno de los valores v1 es correcto y la hora no se desvía más de cinco minutos:

import hashlib, hmac, time

def is_from_os(secret: str, header: str, body: bytes) -> bool:
    parts = [part.split("=", 1) for part in header.split(",")]
    timestamp = next(value for key, value in parts if key == "t")
    expected = hmac.new(secret.encode(), f"{timestamp}.".encode() + body, hashlib.sha256).hexdigest()
    signed = any(hmac.compare_digest(expected, value) for key, value in parts if key == "v1")
    return signed and abs(time.time() - int(timestamp)) <= 300

Comprueba sobre el contenido sin tocar, tal como lo recibiste, no sobre un JSON reconstruido. La misma función comprueba también los webhooks de las funciones de IA, que se firman del mismo modo.

Durante un reemplazo con solapamiento, cada entrega lleva dos valores v1, el nuevo primero. Pon el secreto nuevo en tu programa cuando te venga bien, mientras dure el solapamiento.

El registro de entregas

curl -s "$OS/api/v1/organisations/$ORG/webhooks/$WEBHOOK/deliveries?state=failed" \
  -H "Authorization: Bearer $TOKEN"

Cada entrega tiene state (queued, sending, delivered, failed o skipped), attempts, last_status, last_error (timeout, connect, tls, host_refused, redirect), y en una entrega omitida skipped_reason:

skipped_reason Significado
not_visible la cuenta no puede ver el evento o la cosa
actor_inactive la cuenta ya no está activa; el webhook está desactivado
webhooks_off los webhooks no están activados en este entorno

POST .../deliveries/{delivery_id}/redeliver reenvía una entrega terminada, con la misma Idempotency-Key, mientras el evento se conserve; 409 si aún espera o se está enviando. Si quieres todo un periodo de nuevo, lee el feed desde tu último cursor.