Webhooks
Push notifications when a Federal ID's status, level, consent or membership changes. Signed, retried, and never a substitute for a lookup.
OFIS can POST a signed JSON notification to your endpoint when something changes, instead of you polling. It is a fan-out outbox with retries: register one or more endpoint URLs, and every matching event is delivered to each.
Registering an endpoint
Requires ofis:assert.
curl -s -X POST "https://sandbox.identity.federal.africa/api/v1/webhook-endpoints/" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"url": "https://example.app/hooks/ofis", "events": ["identity.suspended", "consent.revoked"]}'
{
"id": "2d5dfc4b-d0eb-4c99-80fb-11c5b244ac6b",
"url": "https://example.app/hooks/ofis",
"events": ["identity.suspended", "consent.revoked"],
"is_active": true,
"created_at": "2026-07-06T20:44:16.284023Z",
"secret": "73df095b460aa8764e60289bc93b6c0781fa0ab0dd2366a4f9aa1dcd04807250"
}
secret is returned exactly once. Store it at once; there is no way to read it again. If you lose it, delete the endpoint and register a new one. GET /api/v1/webhook-endpoints/ lists your own endpoints without secrets; DELETE /api/v1/webhook-endpoints/{id}/ removes one.
The url must be https on a public hostname. IP addresses, embedded credentials, fragments, and hostnames that resolve to private addresses are rejected. OFIS re-checks the address on every delivery and does not follow redirects, so register the final URL.
Events
| Event | Fires when |
|---|---|
identity.suspended |
An identity is suspended |
identity.reactivated |
A suspended identity is reactivated |
verification.level_changed |
A verification level goes up or down |
consent.revoked |
A member revokes your client’s consent from their portal |
identity.revoked |
An identity is permanently revoked |
entitlement.changed |
A member’s effective membership tier, its end date or its source changes |
Payload
{
"event": "identity.suspended",
"occurred_at": "2026-07-06T20:42:14.840783+00:00",
"data": { "federal_id": "FA-52-00000001" }
}
entitlement.changed carries the new effective tier: data.tier, data.until, data.source, all null when the member no longer holds any tier. verification.level_changed adds verification_level; consent.revoked adds client_id. federal_id is always present.
Verifying the signature
Every delivery carries X-OFIS-Signature: sha256=<hex HMAC-SHA256 of the raw body, keyed with your secret>. Verify over the raw bytes, not a re-serialised payload, and compare in constant time.
import hashlib, hmac
def verify_signature(raw_body: bytes, signature_header: str, secret: str) -> bool:
algo, _, digest = signature_header.partition("=")
if algo != "sha256":
return False
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, digest)
Reject any delivery whose signature does not verify.
Retries
A non-2xx response is retried after 1, 5, 30, 120 and 720 minutes, then the delivery is marked dead with no further attempts and no notification. Delivery is driven by a job that runs about once a minute, so the first retry may come a little later. Monitor your own receipt rate.
Re-fetch, do not trust
A webhook is a notification to go and look, not the record. On receipt, verify the signature, then re-fetch the current state with a lookup. Deliveries are not deduplicated: dedupe on (event, federal_id, occurred_at) if double-processing would hurt. Keep the 30-day re-validation cycle running regardless; webhooks reduce latency, they do not replace it.