# Events & webhooks
URL: /docs/x1id/events

> The X1ID identity-event stream — a stable, typed envelope over every on-chain identity change (register, transfer, primary, attest, market, and more) delivered to your endpoint as an HMAC-signed webhook, with the verification recipe.

# Events & webhooks

<Pill tone="mainnet">Mainnet</Pill>

<LlmActions />

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](/docs/x1id/pay-webhooks), covers payments and invoices, so you don't poll.

<Callout type="info">
  **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](/docs/x1id/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 `support@x1id.io` 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.
</Callout>

## 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.

```jsonc
{
  "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 `type`s.

| Category       | Example types                                                                                                |
| -------------- | ------------------------------------------------------------------------------------------------------------ |
| `registration` | `identity.registered`                                                                                        |
| `ownership`    | `identity.transferred`, `identity.released`, `identity.recovered`, `identity.nft_minted`                     |
| `records`      | `record.created/updated/closed/verified/cleared`, `text_record.*`, `record.delegate_set/revoked`             |
| `primary`      | `primary.set`, `primary.cleared`                                                                             |
| `attestation`  | `attestation.created`, `attestation.revoked`                                                                 |
| `market`       | `market.listed`, `market.sold`, `market.offer_placed`, `market.auction_started/settled`, `market.bid_placed` |
| `lock`         | `lock.locked`, `lock.unlock_initiated`, `lock.unlocked`                                                      |
| `subname`      | `subname.created/revoked`, `subname.record_*`                                                                |
| `namespace`    | `namespace.created`, `namespace.status_changed`, `namespace.override_*`                                      |
| `voucher`      | `voucher.created`, `voucher.refunded`                                                                        |
| `commitment`   | `commitment.created`, `commitment.cancelled`                                                                 |
| `lease`        | `lease.renewed`                                                                                              |
| `admin`        | `admin.config_changed` (the specific op is in `data.action`)                                                 |
| `other`        | `unknown` — 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:

| Token         | Matches                                                   |
| ------------- | --------------------------------------------------------- |
| `*`           | everything                                                |
| `market`      | every event in that category                              |
| `market.*`    | normalized types under that prefix                        |
| `market.sold` | that exact type                                           |
| `buy_handle`  | events 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**.

```js title="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

<Cards>
  <Card href="/docs/x1id/pay-webhooks" icon="BellRing" title="Pay webhooks" description="Self-serve webhooks for payments and invoices to your wallet or @name." />

  <Card href="/docs/x1id/gate" icon="DoorClosed" title="Gate" description="Pair eligibility events with gate checks to react when a handle becomes verified." />

  <Card href="/docs/x1id/platform" icon="Server" title="Developer platform" description="Keys, tiers, and the rest of the /v1 surface." />

  <Card href="/docs/x1id/contracts" icon="FileCode2" title="Contracts" description="The registry program every event originates from." />
</Cards>
