Skip to main content
Developers/Events

Webhooks

Avonao ID tells your server when something happens to a member that you must act on: they withdrew your app, they deleted their Avonao ID, or (if you ask for it) they started or cancelled a deletion. Every event is signed.

Events

Webhook events
EventWhenDataWhat to do
grant.revokedA member revoked one of your apps in their Avonao ID dashboard.{ client_id, sub, revoked_at }End the sessions of that member that came from Avonao ID, and stop treating the connection as active. If the member signs in again they see the consent screen again, and the sub is the same as before.
member.deletedA member who used your apps has deleted their Avonao ID, and the 28-day restore window is over: the account is gone for good.{ sub, deleted_at }Delete the data you hold for that sub, as your privacy policy and the law require.
member.deletion_requestedOpt-in. A member who used your apps started deleting their Avonao ID. The account is closed but can be restored until purge_at (28 days).{ sub, requested_at, purge_at }Stop access for that sub and keep the data for now. If the member restores the account you get member.deletion_cancelled; if not, member.deleted follows.
member.deletion_cancelledOpt-in. The member restored their Avonao ID inside the restore window.{ sub, cancelled_at }Resume access for that sub. The sub is the same as before.
pingYou pressed the test button for an endpoint.{ webhook_id }Answer 2xx. Nothing else.

sub is the pairwise id of your business, the same value as in your ID tokens. Events never contain an email address or a name.

What a delivery looks like

A POST with content-type: application/json and this body (one event per request):

events
// grant.revoked: a member revoked your app in their dashboard
{ "id": "evt_...", "type": "grant.revoked", "created_at": 1793398800, "org_id": "org_...",
  "data": { "client_id": "acme-shop-web-k3j9x2ab", "sub": "avp_...", "revoked_at": 1793398800 } }

// member.deleted: a member who used your apps deleted their Avonao ID (after the 28-day restore window)
{ "id": "evt_...", "type": "member.deleted", "created_at": 1793398800, "org_id": "org_...",
  "data": { "sub": "avp_...", "deleted_at": 1793398800 } }

// member.deletion_requested (opt-in): the member started deleting; restorable until purge_at
{ "id": "evt_...", "type": "member.deletion_requested", "created_at": 1793398800, "org_id": "org_...",
  "data": { "sub": "avp_...", "requested_at": 1793398800, "purge_at": 1795818000 } }

// member.deletion_cancelled (opt-in): the member restored the account
{ "id": "evt_...", "type": "member.deletion_cancelled", "created_at": 1793400000, "org_id": "org_...",
  "data": { "sub": "avp_...", "cancelled_at": 1793400000 } }

// ping: what "send test" delivers
{ "id": "evt_...", "type": "ping", "created_at": 1793398800, "org_id": "org_...", "data": { "webhook_id": "whk_..." } }
Headers of a delivery
HeaderValue
avonao-eventThe event type, for example grant.revoked.
avonao-deliveryA delivery id. It stays the same across retries of the same delivery: use it to de-duplicate.
avonao-signaturet=<unix seconds>,v1=<hex>, where the hex is HMAC-SHA256 of "<t>.<raw body>" under your endpoint's signing secret. It is signed again with a fresh t on every attempt.
user-agentAvonaoID-Webhooks/1

Verify every request

  1. Read the raw body as received. Do not parse and re-serialize it before checking.
  2. Recompute HMAC-SHA256(secret, t + "." + rawBody), with the secret that starts with whsec_.
  3. Compare it with v1 in constant time. Reject the request when it differs.
  4. Reject a t that is more than 5 minutes away from your clock.
  5. De-duplicate on avonao-delivery (or the body id) and make the handler idempotent.
  6. Answer any 2xx within 5 seconds. Do the slow work afterwards.
verify-avonao-webhook.js
// verify-avonao-webhook.js  (Node 20 or newer, no dependencies)
import { createHmac, timingSafeEqual } from "node:crypto";

// rawBody: the request body exactly as received (a string, NOT re-serialized JSON); header: the avonao-signature header;
// secret: the whsec_... signing secret shown once when you created the endpoint.
export function verifyAvonaoWebhook(rawBody, header, secret) {
  const m = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(header ?? "");
  if (!m) return false;
  if (Math.abs(Date.now() / 1000 - Number(m[1])) > 300) return false; // reject anything older than 5 minutes
  const expected = createHmac("sha256", secret).update(m[1] + "." + rawBody).digest();
  return timingSafeEqual(expected, Buffer.from(m[2], "hex"));
}

// In your handler: if (!verifyAvonaoWebhook(raw, req.headers["avonao-signature"], SECRET)) return reply(400);
// then dedupe on the avonao-delivery header (or the body id), act on event.type, and answer 2xx within 5 seconds.

Retries and limits

  • The first attempt is made right away. Anything but a 2xx (including a timeout and a redirect, which is never followed) is retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 24 hours, then given up. Retries run on a five-minute schedule, so one can arrive a little after its time.
  • Deliveries can arrive more than once and out of order. Use the timestamps in the events and keep your handler idempotent.
  • An endpoint is an https URL on a public host name (no IP address, localhost or special-use name, no credentials in the URL). You can have up to 5 per business, each subscribed to the events you pick.
  • Return a 2xx for event types you do not know. New events can be added.

Managing endpoints

These calls belong to the business console's API (/api/v1/console): they need a signed-in session of a business member with the developer role or higher, and the console's origin, so they are made from the console itself. The console's Webhooks page makes the same calls for you.

Webhook management calls
CallWhat it does
GET /orgs/:org/webhooksYour endpoints, with status, last delivery and failure count, and the list of events you can subscribe to.
POST /orgs/:org/webhooksCreate an endpoint: { url, events: ["grant.revoked", "member.deleted"] }. The signing secret (whsec_...) is in the answer, once. Errors: invalid_url, invalid_events, too_many_webhooks.
POST /orgs/:org/webhooks/:id/testSend a signed ping now.
GET /orgs/:org/webhooks/:id/deliveriesThe last 50 deliveries: event, attempts, last status, next attempt.
DELETE /orgs/:org/webhooks/:idRemove the endpoint.

Keep the signing secret with your other secrets. To change it, delete the endpoint and create a new one.

See also Pairwise ids and the Security checklist.

One account
Avonao ID

One Avonao ID. Every platform. Your call.

Create your account in a minute with a passkey, then connect the platforms you use.

Avonao ID account