Events volgen met een stream of webhook¶
Core kan je applicatie laten weten wanneer er iets verandert in je organisatie: een thread kreeg een item, een run is klaar, een document is verwerkt. Je kunt een stream openhouden, of Core een webhook laten aanroepen. Beide bevatten verwijzingen, nooit inhoud: id's en een status. Haal het ding zelf op via de API als je het nodig hebt.
Je sleutel heeft hiervoor geen capability nodig. Elke sleutel leest de events van zijn eigen organisatie.
Hoe een event eruitziet¶
{
"id": "evt_01M3…",
"type": "response.completed",
"cursor": "4418",
"created_at": "2026-09-25T09:14:03Z",
"refs": {"response_id": "resp_01M3…", "status": "completed"}
}
type |
refs |
|---|---|
thread.item.created |
thread_id, item_id, item_type, role, en response_id als een beurt het schreef |
response.completed, response.failed, response.incomplete, response.cancelled |
response_id, status |
response.check_failed |
response_id, en de name, stage, kind en outcome (failed of errored) van de check: een live check slaagde niet, nooit met de tekst die hij las |
run.suspended, run.resumed |
response_id, status |
run.approval.requested, run.approval.decided |
response_id, approval_id, call_id, tool, en status (pending) of decision (approved, rejected): een run wacht op goedkeuring van een toolaanroep, en daarover is beslist |
thread.task.started, thread.task.completed, thread.task.failed, thread.task.cancelled, thread.task.expired |
thread_id, task_id, tool, status, en response_id: de beurt die de taak startte |
knowledge.source.created, knowledge.source.updated, knowledge.source.deleted, knowledge.source.restored |
source_id, collection_id |
knowledge.source.ingested |
source_id, index_id, status, en error_code als het mislukte |
knowledge.index.reindex_requested, knowledge.index.reindexed |
index_id, run_id, en status als het klaar is |
knowledge.connector.synced |
connector_id, run_id, en de aantallen added, updated, removed, skipped, failed |
memory.commitment_overdue |
memory_id, index_id, status (overdue): een open toezegging in het geheugen passeerde haar vervaltijd, één keer per vervaltijd |
mail.received |
mail_connector, folder, uid: een nieuw bericht in een map die een mailkoppeling bewaakt, nooit de afzender, het onderwerp of de tekst (Nieuwe mail) |
datastore.record.written, datastore.record.deleted |
datastore_id, datastore, key, revision, en created bij een schrijfactie: een record van een datastore waarvoor emit_events aanstaat, nooit de gegevens zelf |
workflow.run.started, workflow.run.completed, workflow.run.failed, workflow.run.cancelled, workflow.run.expired |
workflow_id, run_id, status, en error_code als hij mislukte: een workflowrun |
workflow.decision.requested, workflow.decision.decided, workflow.decision.expired |
workflow_id, run_id, step_id, node_id, assignee_kind, status, en outcome zodra er een genomen is: een beslissing |
improvements.proposal.created |
proposal_id, check, target_kind, target_id, status: een nieuw voorstel |
improvements.analysis.finished |
analysis_id, status, proposals: een analyse is klaar |
usage.threshold_reached |
cap_kind (organisation of key), key_id bij de limiet van een sleutel, threshold, period_start: het verbruik van deze maand bereikte een van de budgetgrenzen van een maandlimiet, één keer per grens, limiet en maand, ook als de mail erover uitstaat; nooit de limiet of het verbruikte bedrag |
usage.cap_exceeded |
subject (key:<id>, tenant:<id> of ancestor): een maandlimiet weigerde een aanroep, één keer per minuut (het weigeringsvenster van de omgeving) voor dezelfde weigering; nooit de limiet of het verbruikte bedrag, die GET /usage/caps geeft |
Een veld dat een event niet heeft, ontbreekt.
Elke soort kan een stream en een webhook bereiken. Een workflowtrigger
luistert ook naar de meeste, behalve de soorten die elke agentstap zelf maakt: een soort
response.*, run.* of thread.*, response.check_failed inbegrepen, kan geen workflow starten.
De events knowledge.source.created, .updated, .deleted en .restored worden alleen voor
kennisdocumenten gestuurd, via welke route ze ook zijn gemaakt, gewijzigd, verwijderd of
teruggezet: een text-, records- of connector-bron, en een bestand dat in een kenniscollectie
is geüpload of gekopieerd (Wat een bestands-id bereikt).
Een verwijderd document gaat eerst naar de prullenbak; .restored
zegt dat het terug is, zodat een abonnee die het bij .deleted weghaalde, het kan terugzetten. Een
bestand in de eigen collectie van een sleutel of in de collectie uploads van je organisatie, en
een artefact, sturen geen van de vier. Wat daarmee gebeurde, staat in de
auditgebeurtenissen van je sleutel, onder namen als
file.created, file.deleted, file.restored en artifact.deleted.
Een stream openhouden¶
curl -N "$CORE_URL/events/stream" -H "Authorization: Bearer $CORE_KEY"
Het antwoord is server-sent events. De id van elk frame is de cursor van het event, event is
het type en data is het event hierboven. Een regel die met : begint houdt de verbinding open;
negeer die.
Core beëindigt de stream na ongeveer tien minuten. Maak opnieuw verbinding met de laatste cursor
die je las, in de header Last-Event-ID of als ?after=, en je mist niets. Zonder cursor begint de
stream bij nu. Events blijven drie dagen beschikbaar. Is je cursor ouder dan dat, dan stuurt de
stream eerst één gap-frame (event: gap); lees dan de toestand die je bijhield opnieuw in.
Core beëindigt de stream ook zodra je sleutel of token niet meer geldig is. Je sleutels en je
console kunnen hooguit tien streams tegelijk openhouden, en de subject-tokens van elke persoon nog
drie van hun eigen; nog een geeft 429 met een Retry-After-header.
In een browser gebruik je fetch: EventSource kan de header Authorization niet meesturen,
en Core neemt de credential op geen andere manier aan. Geef het apparaat een
subject-token, nooit je sleutel. De stream van een subject-token bevat
de events over de eigen threads, antwoorden en bestanden van die persoon, en -- waar je deployment
groepsgesprekken op basis van regels aanzet (threads_group_reach_by_rule, standaard uit) -- de
events over een gesprek dat een regel die de persoon noemt voor haar opent: in een gesprek waarin meerdere mensen
schrijven hoort het apparaat van elke persoon de
berichten en antwoorden van de anderen binnenkomen. De events van een gedeeld gesprek waarop geen
regel de persoon noemt, bevat ze nooit.
const response = await fetch(`${coreUrl}/events/stream`, {
headers: { Authorization: `Bearer ${subjectToken}`, "Last-Event-ID": lastCursor ?? "" },
});
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
for (let buffer = ""; ; ) {
const { value, done } = await reader.read();
if (done) break; // Core beëindigde de stream: verbind opnieuw met lastCursor
buffer += value;
const frames = buffer.split("\n\n");
buffer = frames.pop();
for (const frame of frames) {
const data = frame.split("\n").find((line) => line.startsWith("data: "));
if (!data || frame.startsWith("event: gap")) continue; // heartbeat, retry-hint of gap
const event = JSON.parse(data.slice(6));
lastCursor = event.cursor;
redraw(event);
}
}
Laat Last-Event-ID weg bij de eerste verbinding.
Een webhook ontvangen¶
curl -s -X POST "$CORE_URL/events/subscriptions" \
-H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
-d '{"url": "https://jouw-app.example/core-events",
"kinds": ["response.completed", "response.failed"]}'
Core geeft 201 terug met het abonnement en zijn secret. Bewaar het geheim meteen: Core toont
het niet nog eens. Laat kinds weg om elk type te ontvangen.
De host van de webhook moet een http- of https-host zijn die bij de
toegestane hosts van je organisatie
staat; regels over hosts in de policy van je sleutel gelden er ook voor. Core levert nooit af op
een adres op een privénetwerk. Een sleutel met een regiobeperking bereikt alleen een
host die de policy in een van die regio's plaatst. Core controleert de host nu en opnieuw voor
elke aflevering. Een host die wordt geweigerd geeft 403 en maakt niets aan. Een organisatie heeft hooguit 20 abonnementen; nog een geeft 409.
Elke aflevering is een POST van één event met deze headers:
| Header | Waarvoor |
|---|---|
Artific-Signature |
t=<unix-seconden>,v1=<handtekening>: bewijst dat Core het stuurde |
Idempotency-Key |
gelijk bij elke herhaling van één aflevering: gooi een herhaling weg |
Artific-Event-Id |
het id van het event |
Antwoord met een 2xx. Bij iets anders, of geen antwoord, probeert Core het opnieuw met steeds
langere pauzes. Afleveringen kunnen in een andere volgorde aankomen; sorteer op cursor.
De handtekening controleren¶
De handtekening is de HMAC-SHA256 van het tijdstip, een punt en precies de body die je ontving, met het geheim als sleutel, in hex. Controleer hem voordat je de body vertrouwt:
import hashlib, hmac, time
def verify(secret: str, header: str, body: bytes, tolerance: int = 300) -> bool:
try:
parts = dict(part.split("=", 1) for part in header.split(","))
sent, signature = parts["t"], parts["v1"]
age = abs(time.time() - int(sent))
except (KeyError, ValueError):
return False # no signature, or not one Core wrote
expected = hmac.new(secret.encode(), f"{sent}.".encode() + body, hashlib.sha256)
return age <= tolerance and hmac.compare_digest(expected.hexdigest(), signature)
Gebruik de ruwe bytes van de body, niet een opnieuw gecodeerde kopie van de geparste JSON.
Een callback van een run wordt op dezelfde manier ondertekend, met het callbackgeheim van de sleutel die de run startte, dus deze functie controleert beide.
Het geheim vervangen¶
curl -s -X POST "$CORE_URL/events/subscriptions/evs_01M3…/rotate-secret" \
-H "Authorization: Bearer $CORE_KEY"
Het antwoord bevat het nieuwe geheim, en dat geldt meteen: zet het in je ontvanger zodra je het
hebt. Een aflevering die al onderweg is kan nog de oude handtekening dragen; beantwoord die met
iets anders dan 2xx en Core probeert het opnieuw, ondertekend met het nieuwe geheim.
GET /events/subscriptions toont je abonnementen en DELETE /events/subscriptions/{id} verwijdert
er een. Geen van beide toont een geheim. Alleen de sleutel die een abonnement aanmaakte, of een
sleutel met management, kan het geheim vervangen of het abonnement verwijderen. Verwijder je een
sleutel, dan verdwijnen ook zijn abonnementen.