Skip to main content

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​

  1. Compute HMAC-SHA256 with your endpoint secret over timestamp + "." + event_id + "." + raw_body, using the exact bytes you received.
  2. 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.
  3. Refuse timestamps more than 300 seconds from your clock.
  4. 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.