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.

Last updated 2026-11-02.