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​
- Project → Settings → Webhooks
- Click Add Webhook
- 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
- URL — your endpoint (HTTPS recommended; HTTP works for
development against
- 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​
| Event | When it fires |
|---|---|
PROJECT_CREATED | A project is created |
PROJECT_UPDATED | A project's settings change |
PROJECT_DELETED | A project is deleted |
TASKS_CREATED | One or more tasks imported into a project |
TASKS_DELETED | Tasks deleted |
ANNOTATION_CREATED | An annotation is submitted |
ANNOTATIONS_CREATED | Bulk-create (e.g. via API import) |
ANNOTATION_UPDATED | An existing annotation is edited |
ANNOTATIONS_DELETED | One or more annotations deleted (single deletes fire this too) |
LABEL_LINK_CREATED | A reusable label (from Labels Manager) is linked to a project |
LABEL_LINK_UPDATED | A label link is edited |
LABEL_LINK_DELETED | A 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/jsonX-AfriAnnotate-Event: <action>— the event name, also present asactionin the body- When the webhook has a signing secret:
X-AfriAnnotate-Timestamp(Unix seconds) andX-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
POSTper event, sent from the request that caused it, with a short timeout per attempt (1 second by default,WEBHOOK_TIMEOUTenv 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​
- Project settings → — the rest of the per-project configuration
- API → Overview → — the same data your webhook receives is fetchable via REST API
- Data export → — for batch / scheduled pulls instead of real-time push