Skip to main content

Webhooks

Webhooks let your external services react to events in AfriAnnotate — an HTTP POST to your URL whenever an annotation is submitted, tasks are created, a project is updated.

Useful for:

  • Piping annotations into your ML training pipeline
  • Notifying a Slack channel when work lands
  • Triggering downstream BI / warehouse ingestion

Webhooks are configured per project at Project → Settings → Webhooks.

Configuring a webhook​

  1. Project → Settings → Webhooks
  2. Click Add Webhook
  3. Fill in:
    • URL — your endpoint (HTTPS recommended; HTTP works for development against localhost)
    • Is active — pause / resume without deleting
    • Headers — extra HTTP headers sent on every request (e.g. Authorization: Bearer ...). This is the only authentication mechanism; there is no signing secret
    • Send payload — on: send the full event body; off: send only {"action": ...} and fetch details via API
    • Send for all actions — on: every event; off: tick the individual events below
  4. Save

The next matching event triggers a POST to your URL. There is no name field and no "send test event" button — trigger a real event (e.g. submit an annotation) to verify connectivity.

Event types​

EventWhen it fires
PROJECT_CREATEDA project is created
PROJECT_UPDATEDA project's settings change
PROJECT_DELETEDA project is deleted
TASKS_CREATEDOne or more tasks imported into a project
TASKS_DELETEDTasks deleted
ANNOTATION_CREATEDAn annotation is submitted
ANNOTATIONS_CREATEDBulk-create (e.g. via API import)
ANNOTATION_UPDATEDAn existing annotation is edited
ANNOTATIONS_DELETEDOne or more annotations deleted (single deletes fire this too)
LABEL_LINK_CREATEDA reusable label (from Labels Manager) is linked to a project
LABEL_LINK_UPDATEDA label link is edited
LABEL_LINK_DELETEDA label link is removed

The most common subscriptions are ANNOTATION_CREATED and ANNOTATION_UPDATED — the "new label arrived" trigger downstream ML pipelines wait for.

Payload shape​

The POST body is JSON with action plus the event's objects (when Send payload is on):

{
"action": "ANNOTATION_CREATED",
"project": {
"id": 42,
"title": "Hausa sentiment v1"
},
"task": {
"id": 1234,
"data": { "text": "Sannu da zuwa" }
},
"annotation": {
"id": 9876,
"result": [
{
"from_name": "sentiment",
"to_name": "txt",
"type": "choices",
"value": { "choices": ["positive"] }
}
],
"completed_by": 10,
"lead_time": 4.288,
"created_at": "2026-05-12T08:42:11Z"
}
}

For events that affect many objects (e.g. TASKS_CREATED after a bulk import), the payload contains arrays:

{
"action": "TASKS_CREATED",
"project": { "id": 42 },
"tasks": [
{ "id": 1234, "data": {...} },
{ "id": 1235, "data": {...} }
]
}

With Send payload off, the body is just {"action": "..."}.

Headers your endpoint receives​

  • Content-Type: application/json
  • X-AfriAnnotate-Event: <action> — the event name, also present as action in the body
  • When the webhook has a signing secret: X-AfriAnnotate-Timestamp (Unix seconds) and X-AfriAnnotate-Signature: sha256=<hex>, an HMAC-SHA256 over "<timestamp>.<raw body>" with your secret. Verify by recomputing and comparing in constant time; reject stale timestamps (e.g. older than 5 minutes) to block replays.
  • Any custom headers you configured on the webhook.
import hmac, hashlib

def verify(secret: str, timestamp: str, body: bytes, signature: str) -> bool:
expected = "sha256=" + hmac.new(secret.encode(), f"{timestamp}.".encode() + body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)

Delivery semantics​

  • One POST per event, sent from the request that caused it, with a short timeout per attempt (1 second by default, WEBHOOK_TIMEOUT env on the server).
  • Retries: timeouts, connection errors, HTTP 5xx and 429 are retried up to 3 attempts (0.5 s, then 1.5 s back-off). Other 4xx responses are treated as your endpoint's verdict and not retried.
  • Delivery log: the last 50 deliveries per webhook — event, result, status code, attempts, latency, error — are kept and shown under Recent deliveries on the webhook page (GET /api/webhooks/<id>/deliveries/). There is no replay button; re-run the action or reconcile via the API.

Keep your endpoint fast — acknowledge immediately and process asynchronously.

When a delivery finally fails (after retries), the org owner is emailed — capped at one alert per webhook per 24 hours, so a broken integration surfaces without flooding anyone's inbox.

Common patterns​

Slack notification on every submitted annotation​

URL: a Slack incoming-webhook URL. Events: ANNOTATION_CREATED. Send payload: on.

ML training pipeline trigger​

URL: an internal endpoint that queues a training job. Events: ANNOTATION_CREATED, ANNOTATION_UPDATED. Send payload: off (you have your own API access to fetch the data; you only need the trigger).

Audit mirror​

URL: your SIEM / log-ingestion endpoint. Send for all actions: on. Send payload: on.

The platform's audit log (Platform → Audit log) is the canonical record; the webhook is for near-real-time mirroring.

What's next​