[Langlopend werk, ondertekende levering]

Async jobs

Plaats langlopend werk in de wachtrij met `POST /v1/jobs`, krijg een `id` terug en peil dan `GET /v1/jobs/{id}` of ontvang een HMAC-ondertekende webhook bij voltooiing. Initieel ondersteunde kinds zijn `intelligence.enrich.bulk`, `export.bulk`, `import.bulk` en `graph.compute`. Elke statusovergang draagt dezelfde `trace_id` uit G2 voor end-to-end-correlatie.

Async jobs

Een job in de wachtrij plaatsen

`POST /v1/jobs` vereist het scope `argus:jobs:write` op een bearer token. De body moet `kind` (een van de ondersteunde kinds) en `payload` bevatten. Optionele velden zijn `callback_url` (https-URL waarnaar het platform bij voltooiing ondertekend POST) en `secrecy_level` (standaard `standard`; de job erft de waarde en verhoogt deze nooit tijdens uitvoering). Bij succes is het antwoord 201 met `{ id, kind, status, created_at, trace_id }`. De job start in `queued` en de `trace_id` is dezelfde waarde die door de G2-responseheaders stroomt, zodat je de inschakeling kunt correleren met downstream werk.

Levenscyclus en statussen

Een job doorloopt een strikte statusmachine. Workers claimen rijen in de wachtrij met `SELECT ... FOR UPDATE SKIP LOCKED`, zodat elke job aan slechts één worker tegelijk wordt geleverd. Tijdens uitvoering sturen workers elke 30 seconden een heartbeat en werken `progress_pct` bij. Een reaper plaatst lopende jobs waarvan de heartbeat ouder is dan 2 minuten terug in de wachtrij, zodat een gecrashte worker een job nooit blokkeert. Eindstatussen (`succeeded`, `failed`, `cancelled`) worden precies één keer geschreven.

Idempotentie

Stuur `Idempotency-Key: <uuid>` (UUID v4 aanbevolen) op `POST /v1/jobs` om de aanroep veilig herhaalbaar te maken. Binnen een venster van 24 uur per tenant geven herhalingen met dezelfde key 200 terug met dezelfde `id` en body als de oorspronkelijke 201, zelfs als de oorspronkelijke aanvraag payload-velden veranderde. Zonder de header dedupliceert Argus niet; een tijdelijke netwerkretry zou tweemaal in de wachtrij kunnen plaatsen. Keys zijn per tenant gescoped, dus twee tenants kunnen dezelfde UUID gebruiken zonder conflict.

Polling of webhooks

Kies per verzoek een model. Polling: laat `callback_url` weg en roep `GET /v1/jobs/{id}` aan tot de status `queued`/`running` verlaat, roep dan eenmalig `GET /v1/jobs/{id}/result` aan voor de ondertekende URL. Webhooks: geef `callback_url` op en het platform ondertekent en POSTet de voltooiingsgebeurtenis naar die URL. Polling is het eenvoudigste pad voor batches die je zelf gestart hebt; webhooks zijn de juiste keuze wanneer een extern systeem het resultaat binnen seconden na voltooiing nodig heeft. Beide modi gebruiken hetzelfde record en dezelfde `trace_id`.

Webhook payload en handtekeningverificatie

Bij voltooiing POSTet Argus het canonieke JSON-body naar je `callback_url` met de headers `Content-Type: application/json`, `X-Argus-Trace-ID` (dezelfde trace_id als in G2), `X-Argus-Job-ID` en `X-Argus-Signature: sha256=<hex>`. De handtekening is HMAC-SHA256 over de body met je geregistreerde gedeelde secret. Ontvangers MOETEN de handtekening verifiëren tegen de ruwe bytes van de body voordat ze op de gebeurtenis handelen. Gebruik een vergelijking met constante tijd (`hmac.compare_digest` in Python, `crypto.timingSafeEqual` in Node.js) om timing-aanvallen te vermijden. Argus probeert mislukte leveringen tot 24 uur opnieuw met exponentiële backoff.

Annulering

`DELETE /v1/jobs/{id}` vraagt annulering aan en vereist het scope `argus:jobs:write`. Een `queued` of `running` job geeft 204 terug en gaat over naar `cancelled` (de worker observeert de flag op de eerstvolgende heartbeat-grens). Een al `cancelled` job geeft 200 terug (idempotente replay). Een job die al een succesvolle eindstatus (`succeeded` of `failed`) heeft bereikt, geeft 409: voltooid werk kan niet ongedaan gemaakt worden. Als er een webhook geregistreerd was, wordt er geen voltooiingswebhook geleverd voor een geannuleerde job.

Kinds-catalogus

Vier job-kinds zijn in de eerste release. `intelligence.enrich.bulk` verrijkt een lijst van profile-IDs tegen partner-connectoren. `export.bulk` produceert tenant-gescopte exports (CSV, JSONL, FHIR Bundle) en schrijft het resultaat naar R2; het result-endpoint geeft een kortlevende ondertekende URL. `import.bulk` ingesteert een door een partner geleverde bundle en geeft resultaten per rij. `graph.compute` voert een voorberekende query uit op de operationele graph en geeft het gematerialiseerde resultaat. Toekomstige kinds zijn additief; bestaande kinds behouden hun payload-vorm onder semver.

Tenantisolatie

Elke query op de jobs-tabel bevat `tenant_id` EN `organization_id` uit het bearer token. Cross-tenant-lookups geven 404 terug (nooit 403), zodat het bestaan van een job in een andere tenant nooit wordt onthuld. Het record erft `secrecy_level` van het verzoek en levenscyclusovergangen verhogen het nooit. Elke statuswijziging schrijft een audit-entry met gebruiker, organisatie, tenant, actie (`jobs.enqueue`, `jobs.start`, `jobs.complete`, `jobs.fail`, `jobs.cancel`, `jobs.read`, `jobs.list`), `resource_id = id`, geheimhoudingsniveau en metadata `{ kind, status, progress_pct, trace_id }`.

Levenscyclus en statussen

queued | running | succeeded | failed | cancelled

Een job inschakelen

# Enqueue an async job
curl -X POST https://api.knogin.com/v1/jobs \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 8b9d8a2e-4a8c-4a7e-8a2e-4a8c4a7e8a2e" \
  -d '{
    "kind": "intelligence.enrich.bulk",
    "payload": { "profile_ids": ["p1", "p2"] },
    "callback_url": "https://partner.example.com/argus-webhook",
    "secrecy_level": "standard"
  }'

# 201 Created
# {
#   "id": "01HFXY...",
#   "kind": "intelligence.enrich.bulk",
#   "status": "queued",
#   "created_at": "2026-05-08T12:00:00Z",
#   "trace_id": "0af7651916cd43dd8448eb211c80319c"
# }

Status, resultaat en webhook payload

# GET /v1/jobs/{id} while running
{
  "id": "01HFXY...",
  "kind": "intelligence.enrich.bulk",
  "status": "running",
  "progress_pct": 42,
  "created_at": "2026-05-08T12:00:00Z",
  "updated_at": "2026-05-08T12:00:30Z",
  "completed_at": null,
  "error": null,
  "trace_id": "0af7651916cd43dd8448eb211c80319c"
}

# GET /v1/jobs/{id}/result after success
{
  "id": "01HFXY...",
  "result_ref": "r2://argus-jobs/results/01HFXY.json",
  "signed_url": "https://r2-presigned-url..."
}

# GET /v1/jobs?kind=intelligence.enrich.bulk&status=running&limit=50
{
  "jobs": [ /* job objects */ ],
  "next_cursor": "eyJjcmVhdGVkX2F0IjoiLi4uIn0=",
  "total_count": 1234
}

Webhook-levering

# Argus -> partner.example.com/argus-webhook
POST /argus-webhook HTTP/1.1
Host: partner.example.com
Content-Type: application/json
X-Argus-Trace-ID: 0af7651916cd43dd8448eb211c80319c
X-Argus-Job-ID: 01HFXY...
X-Argus-Signature: sha256=<hex of HMAC-SHA256(canonical_json_body, partner_secret)>

{
  "id": "01HFXY...",
  "kind": "intelligence.enrich.bulk",
  "status": "succeeded",
  "trace_id": "0af7651916cd43dd8448eb211c80319c",
  "result_ref": "r2://argus-jobs/results/01HFXY.json"
}

De webhook-handtekening verifiëren

// Node.js (Express): verify X-Argus-Signature on the raw body
import crypto from 'node:crypto';

function verifyArgusSignature(rawBody, headerValue, sharedSecret) {
  const expectedHex = crypto
    .createHmac('sha256', sharedSecret)
    .update(rawBody)
    .digest('hex');
  const expected = Buffer.from(`sha256=${expectedHex}`, 'utf8');
  const provided = Buffer.from(headerValue ?? '', 'utf8');
  if (provided.length !== expected.length) return false;
  return crypto.timingSafeEqual(provided, expected);
}
# Python (Flask): canonical-body verification
import hmac, hashlib, json

def verify_argus_signature(raw_body: bytes, header_value: str, shared_secret: bytes) -> bool:
    expected = "sha256=" + hmac.new(shared_secret, raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, header_value or "")

# Argus computes the signature over the canonical JSON body it sent:
# canonical = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
# sig = hmac.new(secret, canonical, hashlib.sha256).hexdigest()
# header = f"sha256={sig}"

Klaar om async jobs te integreren?

Open de API-referentie voor het publieke contract of neem contact op met Knogin als je hulp nodig hebt bij een aangepast kind.