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
cursorempiezas por el evento más antiguo que OS aún conserva. Procesa la página, guardanext_cursory la próxima vez pide?cursor=con ese valor. next_cursortambié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 contypes. Si no hay nada nuevo, recibes una página vacía y el mismonext_cursor; vuelve a pedir más tarde.typeslimita el feed a ciertos tipos:?types=directory.membership.removed.limites como máximo 100. Una página puede contener menos eventos quelimitmientrasnext_cursoravanza: solo ves lo que puedes ver.410significa 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
secretenseguida. OS muestra el secreto solo en esta respuesta, nunca más. kindsvacío significa todos los tipos, también los que se añadan más adelante.actor_membership_ides 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.