X1ID is live on X1 mainnet
X1ID / Events & webhooks

Events & webhooks

Mainnet

Every on-chain identity change — a handle registered, transferred, a primary set, an attestation added, a sale — becomes a stable, typed event. Register a webhook and X1ID delivers each matching event to your endpoint as a signed POST, with retries and a dead-letter record. One stream powers identity-change alerts and Gate eligibility hooks, and a sibling stream, Pay webhooks, covers payments and invoices, so you don't poll.

Payments and invoices: self-serve today. To get a webhook when a wallet or an @name you control is paid, or when one of its invoices is created, paid, cancelled or closed, register it yourself with your API key and a wallet signature. See Pay webhooks. Those deliveries use the x1id.pay-events/v1 payload and the same signature scheme as below.

Identity events: provisioned on request. Self-serve registration for the identity stream on this page (registrations, transfers, records, market, …) isn't public yet. Email [email protected] with your endpoint URL and the event types you want, and you'll get back a signing secret. The envelope and verification recipe below are the stable delivery contract, so you can build your receiver against them today.

The event envelope

Every event is a small, stable JSON object (x1id.events/v1). The on-chain instruction name and raw indexer shape are implementation details that churn; you get a stable { category, type }, a typed data projection of the fields you most want, and — always — the untouched row in raw as an escape hatch.

{
  "schema": "x1id.events/v1",
  "id": 148123,                       // monotonic; dedupe on this
  "type": "market.sold",              // stable, namespaced
  "category": "market",              // coarse filtering axis
  "rawEventType": "buy_handle",       // the on-chain instruction
  "handle": { "name": "alice", "account": "<Handle PDA base58>" },  // null for config/sentinel events
  "owner": "<pubkey|null>",
  "chain": { "signature": "<sig>", "ixIndex": 0, "slot": 183500000, "blockTime": "2026-10-02T00:00:00.000Z" },
  "data": { "priceLamports": "10000000000", "feeLamports": "250000000", "buyer": "…", "seller": "…" },
  "raw": { /* the indexer detail, verbatim */ },
  "emittedAt": "2026-10-02T12:00:00.000Z"
}

Lamport amounts are decimal strings. A value whose shape isn't certain stays only in raw — nothing is reshaped or guessed.

Event types

Events are grouped into categories; each category has a set of normalized types.

CategoryExample types
registrationidentity.registered
ownershipidentity.transferred, identity.released, identity.recovered, identity.nft_minted
recordsrecord.created/updated/closed/verified/cleared, text_record.*, record.delegate_set/revoked
primaryprimary.set, primary.cleared
attestationattestation.created, attestation.revoked
marketmarket.listed, market.sold, market.offer_placed, market.auction_started/settled, market.bid_placed
locklock.locked, lock.unlock_initiated, lock.unlocked
subnamesubname.created/revoked, subname.record_*
namespacenamespace.created, namespace.status_changed, namespace.override_*
vouchervoucher.created, voucher.refunded
commitmentcommitment.created, commitment.cancelled
leaselease.renewed
adminadmin.config_changed (the specific op is in data.action)
otherunknown — any instruction a build predates is still delivered, never dropped

Filtering

A subscription's filter is an array of tokens; an event matches if the array is empty (all events) or any token matches:

TokenMatches
*everything
marketevery event in that category
market.*normalized types under that prefix
market.soldthat exact type
buy_handleevents whose rawEventType matches (the raw instruction)

Verifying a delivery

Each delivery is a POST of the JSON body with these headers:

Content-Type: application/json
X1id-Signature: t=<unix-seconds>,v1=<hex HMAC-SHA256>
X1id-Event-Id: <event id>
X1id-Event-Type: <normalized type>
X1id-Delivery-Id: <delivery id>
X1id-Subscription-Id: <whsub_...>

The signature is HMAC-SHA256 over `${t}.${rawBody}` using your subscription secret (whsec_…, shown once at creation). This is the Stripe scheme — the timestamp t foils replay.

To verify: (1) read the raw request body bytes (don't re-serialize the parsed JSON); (2) parse t and v1 from X1id-Signature; (3) reject if |now − t| exceeds your tolerance (300 s recommended); (4) recompute the HMAC and compare in constant time.

verify.js
import { createHmac, timingSafeEqual } from "node:crypto";

function verify(secret, rawBody, header, toleranceSecs = 300) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const t = Number(parts.t);
  if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSecs) return false;
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  const a = Buffer.from(expected), b = Buffer.from(parts.v1 ?? "");
  return a.length === b.length && timingSafeEqual(a, b);
}

Delivery, retries & ordering

  • A delivery succeeds only on a 2xx. A 3xx redirect is a failure — a webhook endpoint must not redirect.
  • On failure it retries with full-jitter exponential backoff, up to a configured number of attempts (6 by default), then dead-letters the delivery while keeping the full signed payload for inspection or replay.
  • Dedupe on the event id (and the X1id-Event-Id header): delivery is at-least-once, so the same event can arrive more than once, but id is monotonic and stable.
  • Respond fast (2xx) and process asynchronously — a slow handler just invites a retry.

Next

Browse the docs

Events & webhooks