Ga naar inhoud

Webhooks en de feed via de API

Deze pagina is voor wie een eigen programma laat bijhouden wat er in OS verandert. Ze laat zien hoe je de feed leest, een webhook registreert, een aflevering controleert en wat je doet als er iets misgaat. Wat gebeurtenissen en webhooks zijn, staat in Gebeurtenissen en webhooks. Hoe je een verzoek aan OS doet (adres, aanmelden, pagina's, fouten), staat in De access-API.

De feed lezen

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"}
  • Zonder cursor begin je bij de oudste gebeurtenis die OS nog bewaart. Verwerk de pagina, bewaar next_cursor en vraag de volgende keer ?cursor= met die waarde.
  • next_cursor staat ook op een korte pagina: dan wijst hij voorbij alles wat OS tot nu vastlegde, ook de soorten die je met types wegliet. Is er niets nieuws, dan krijg je een lege pagina en dezelfde next_cursor; vraag later opnieuw.
  • types beperkt de feed tot bepaalde soorten: ?types=directory.membership.removed.
  • limit is ten hoogste 100. Een pagina kan minder gebeurtenissen bevatten dan limit terwijl next_cursor doorloopt: je ziet alleen wat je mag zien.
  • 410 betekent dat de gebeurtenissen na je cursor niet meer bewaard zijn. Lees de stand opnieuw in en begin zonder cursor.

Je hebt see op event nodig.

Een webhook registreren

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_..."}
  • Bewaar secret meteen. OS laat het geheim alleen in dit antwoord zien, en nooit meer.
  • kinds leeg betekent elke soort, ook soorten die later bijkomen.
  • actor_membership_id is het account waarnamens de webhook ontvangt: een actief lid van je organisatie dat je mag wijzigen en waarvan je alles hebt wat het heeft.
  • Een organisatie heeft ten hoogste 20 webhooks.
Antwoord Betekenis
201 Geregistreerd; Location wijst naar de webhook.
403 Je hebt niet alles wat het account heeft.
409 Webhooks staan niet aan in deze omgeving, of je organisatie heeft er al 20.
422 Het adres is geen https, noemt een IP-adres in plaats van een hostnaam, bevat een gebruiker, wachtwoord of #, of de host komt uit op een privé-, intern of geblokkeerd adres (webhook_host_refused); een soort bestaat niet; het account is geen actief lid.

Wijzigen, vervangen, verwijderen

Verzoek Wat het doet
GET .../webhooks de webhooks, zonder geheim
GET .../webhooks/{id} één webhook, met ETag
PATCH .../webhooks/{id} label, url, kinds of actor_membership_id wijzigen, met If-Match: "<versie>"; een nieuw adres en een nieuw account controleert OS zoals bij registreren
DELETE .../webhooks/{id} de webhook en zijn logboek weg; de gebeurtenissen blijven in de feed
POST .../webhooks/{id}/rotate-secret {"overlap_hours": 24}: een nieuw geheim, alleen in dit antwoord; het oude ondertekent nog zo lang mee (0 tot 168 uur, 0 is meteen)
POST .../webhooks/{id}/enable een falende of uitgeschakelde webhook weer actief; 409 zolang het account niet actief is
POST .../webhooks/{id}/test stuurt één gebeurtenis events.webhook.tested naar alleen deze webhook, in elke toestand; 202 met delivery_id

Al deze verzoeken vragen write op webhook (verwijderen delete, lezen see).

Een aflevering ontvangen

OS stuurt elke gebeurtenis als een POST met de gebeurtenis als JSON en deze headers:

Header Waarde
Content-Type application/json
User-Agent artific-os
Artific-Signature t=<unix-seconden>,v1=<hex>, en tijdens een vervanging een tweede v1
Idempotency-Key het id van de aflevering, bij elke poging hetzelfde
Artific-Event-Id het id van de gebeurtenis

Antwoord met een 2xx zodra je de gebeurtenis veilig hebt opgeslagen; doe het werk daarna. OS wacht ten hoogste 10 seconden op je antwoord en leest de inhoud ervan niet. Een andere status, een doorverwijzing of geen antwoord telt als mislukt, en OS probeert het opnieuw.

Je kunt dezelfde aflevering meer dan eens ontvangen, bijvoorbeeld als je antwoord onderweg verloren ging. Gooi een aflevering weg waarvan je de Idempotency-Key al verwerkte.

Controleer de handtekening

De handtekening is de HMAC-SHA256 van de tijd, een punt en de exacte inhoud, met het geheim, als hexadecimaal. Accepteer een aflevering wanneer één van de v1-waarden klopt en de tijd niet meer dan vijf minuten afwijkt:

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

Controleer op de onbewerkte inhoud, zoals je hem ontving, niet op opnieuw opgebouwde JSON. Dezelfde functie controleert ook de webhooks van de AI-functies, die op dezelfde manier ondertekend zijn.

Tijdens een vervanging met overlap draagt elke aflevering twee v1-waarden, de nieuwe eerst. Zet het nieuwe geheim in je programma wanneer het je uitkomt, zolang de overlap duurt.

Het afleverlogboek

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

Elke aflevering heeft state (queued, sending, delivered, failed of skipped), attempts, last_status, last_error (timeout, connect, tls, host_refused, redirect), en bij een overgeslagen aflevering skipped_reason:

skipped_reason Betekenis
not_visible het account mag de gebeurtenis of het ding niet zien
actor_inactive het account is niet meer actief; de webhook is uitgeschakeld
webhooks_off webhooks staan niet aan in deze omgeving

POST .../deliveries/{delivery_id}/redeliver stuurt een afgeronde aflevering opnieuw, met dezelfde Idempotency-Key, zolang de gebeurtenis bewaard is; 409 als ze nog wacht of verstuurd wordt. Wil je een hele periode opnieuw, lees dan de feed vanaf je laatste cursor.