Signed webhooks
Deliveries carry four headers: AfricaOne-Webhook-Id, AfricaOne-Webhook-Timestamp,
AfricaOne-Webhook-Key-Id and AfricaOne-Webhook-Signature (ao1= followed by lowercase hex
HMAC-SHA256). Signed bytes: UTF-8 timestamp + "." + event_id + "." + raw_body, keyed by the
endpoint's secret (scheme ao1, spec §11, CCR-078).
Verify a delivery
- Compute HMAC-SHA256 with your endpoint secret over
timestamp + "." + event_id + "." + raw_body, using the exact bytes you received. - Compare in constant time. Use the secret named by the key id (rotation overlap) -- a receiver should accept the old and the new key during a rotation window.
- Refuse timestamps more than 300 seconds from your clock.
- Only after the signature verifies, record the event id and refuse repeats for at least 7 days (the replay retention window).
The reference receiver (packages/developer-events/src/signing.ts) implements exactly this:
verifyWebhook returns { ok: true, eventId, keyId } or { ok: false, reason } with one of
MALFORMED, STALE_TIMESTAMP, UNKNOWN_KEY, BAD_SIGNATURE or REPLAYED. The replay store is
consulted only after the signature verifies, so a forged request can never burn a real event
id.
import { verifyWebhook, InMemoryReplayStore } from '@africaone/developer-sdk';
const result = await verifyWebhook({
rawBody, // exact bytes received, before any parsing
headers: request.headers,
secrets: { [keyId]: endpointSecret }, // old + new key during rotation
replay: new InMemoryReplayStore(), // persist this in production
});
if (!result.ok) {
// result.reason: MALFORMED | STALE_TIMESTAMP | UNKNOWN_KEY | BAD_SIGNATURE | REPLAYED
}
Key rotation
Endpoint secrets are never stored directly: each is
HMAC(master, "ao-webhook/v1/" + endpoint + "/" + key_version) derived from a platform signing
key. AfricaOne-Webhook-Key-Id names which key version signed a given delivery, so your
receiver can hold both the outgoing and the incoming key during a rotation and verify either.
Events today
Only two events exist in the sandbox outbox: consent.granted and consent.revoked. Failed
deliveries retry with backoff, appear in the console's "Failed deliveries" list, and a human can
audit and replay them. Every further event will be documented here, with its payload shape and
scope requirement, only once it is built and released.
Try it
In the sandbox console, "Webhooks and sandbox" sends signed test deliveries to a public HTTPS endpoint you register, using synthetic sandbox players -- no real player data is ever in a test delivery.