Docs
Webhooks
Webhooks
Walos can push newly indexed Sui events to your own HTTPS endpoint. Subscriptions are package-scoped and start from the activation cursor, so only events after configuration are delivered.
Request headers
x-walos-delivery-id: unique delivery id for consumer-side idempotency.x-walos-event-type: fixed tosui.eventin v1.x-walos-timestamp: unix timestamp used in the signature base string.x-walos-signature: hex HMAC-SHA256 of${timestamp}.${rawBody}.
Retry semantics
Deliveries are at-least-once. Any non-2xx response is retried with the fixed schedule 1m, 5m, 15m, 1h, 6h, 24h. Including the initial send, that is 7 total attempts before the delivery is marked failed.
Payload example
{
"id": "0f3ab6b2-b3e2-4b6a-8b7e-aacfd0d7c314",
"type": "sui.event",
"createdAt": "2026-03-27T00:00:00.000Z",
"projectId": "8a2b9c6d-8b76-4b10-a48e-b7fe6a215d2f",
"environmentId": "a7cc0d59-76e5-4b34-b1bf-3f0f7767c45c",
"packageId": "0xabc",
"event": {
"txDigest": "0xtx9",
"eventSeq": 2,
"checkpoint": 555,
"eventType": "market::ItemListed",
"sender": "0xsender",
"timestamp": "2026-03-27T00:00:00.000Z",
"data": {
"price": "1000000"
}
}
}Verify the signature
A stand-alone HMAC check is not enough on its own: retries and captured requests will still look valid. The example below also bounds x-walos-timestamp skew and treatsx-walos-delivery-id as a one-time token, so state-changing handlers never double-process a replay.
import crypto from 'node:crypto';
const HEX_SHA256 = /^[0-9a-f]{64}$/i;
const MAX_SKEW_SECONDS = 5 * 60;
type DeliveryStore = {
// Returns true the first time this delivery id is seen (and records it);
// false when the id has already been processed. Back this with Redis,
// Postgres, or any atomic "insert if not exists" store in production.
registerDelivery(deliveryId: string): Promise<boolean>;
};
async function verifyWalosWebhook({
rawBody,
timestamp,
deliveryId,
signature,
secret,
deliveryStore,
now = Date.now()
}: {
rawBody: string;
timestamp: string;
deliveryId: string;
signature: string;
secret: string;
deliveryStore: DeliveryStore;
now?: number;
}) {
// x-walos-signature is hex HMAC-SHA256 (32 bytes = 64 hex chars).
// Reject malformed headers up front so timingSafeEqual never throws
// on a length mismatch and a bad signature stays a normal "false".
if (!HEX_SHA256.test(signature)) return false;
// Bound x-walos-timestamp skew before spending any HMAC work so a
// captured request can't be replayed indefinitely after the original send.
const tsSeconds = Number(timestamp);
if (!Number.isFinite(tsSeconds)) return false;
const skewSeconds = Math.abs(now / 1000 - tsSeconds);
if (skewSeconds > MAX_SKEW_SECONDS) return false;
const expected = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
const expectedBuf = Buffer.from(expected, 'hex');
const signatureBuf = Buffer.from(signature, 'hex');
if (expectedBuf.length !== signatureBuf.length) return false;
if (!crypto.timingSafeEqual(expectedBuf, signatureBuf)) return false;
// Deliveries are at-least-once. Pair signature verification with
// x-walos-delivery-id deduplication so retries of the same request
// are only processed once on state-changing handlers.
return deliveryStore.registerDelivery(deliveryId);
}