Walos
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 to sui.event in 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);
}