> For the complete documentation index, see [llms.txt](https://docs.selcier.xyz/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.selcier.xyz/build-on-selcier/event-delivery.md).

# Event delivery

Signed HTTPS notifications from Selcier for share class deployments, settled transfers, closed offerings and other activity on Robinhood Chain.

Rather than polling, your systems can have Selcier tell them when something happens. An HTTPS `POST` goes out to each endpoint registered for an event type whenever such an event occurs, a settled transfer or a cleared Liquidity Window being two examples.

Robinhood Chain is where most events begin. An indexer at Selcier follows the contracts of every company, converts each relevant log into an event, and sends it after the block has reached the confirmation level named in the payload.

## Adding an endpoint

You can do this in the Console at **Developers → Webhooks** or with an API call:

```bash
curl https://api.selcier.xyz/v1/webhook_endpoints \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: we-cap-table-sync" \
  -d '{
    "url": "https://hooks.example.com/selcier",
    "events": ["transfer.settled", "offering.closed", "distribution.funded"],
    "description": "Cap table sync"
  }'
```

You get a `secret` starting with `whsec_` in the response. It appears a single time, so keep it alongside your other secrets. HTTPS is mandatory for endpoints, and each must answer within 10 seconds.

## Subscribable events

| Event                      | Fires when                                                                 |
| -------------------------- | -------------------------------------------------------------------------- |
| `share_class.deployed`     | Token, rule engine and oracle for a share class are all live               |
| `holder.verified`          | Verification is complete for a holder, with claims written on-chain        |
| `holder.claim_expiring`    | A claim belonging to a holder has 30 days or less left                     |
| `transfer.settled`         | Inclusion on-chain of a share token transfer                               |
| `transfer.blocked`         | A rule check rejected a transfer attempted through the API or Selcier apps |
| `offering.subscribed`      | A subscription from an investor comes in                                   |
| `offering.closed`          | Successful close of an offering, with shares minted                        |
| `offering.failed`          | The raise at close falls short of the offering's minimum                   |
| `liquidity_window.opened`  | Orders can now be placed in a Liquidity Window                             |
| `liquidity_window.cleared` | The clearing price is set and the window settles at it                     |
| `distribution.funded`      | `DistributionVault` receives the money for a distribution                  |
| `distribution.claimed`     | A claim is made by a holder on a distribution                              |
| `proposal.finalized`       | The outcome of a finished vote is on record                                |
| `recovery.scheduled`       | The timelock starts for a wallet recovery                                  |
| `recovery.executed`        | A wallet recovery has finished                                             |
| `token.paused`             | Pausing of a share token                                                   |
| `multiplier.scheduled`     | Scheduling of a change to `uiMultiplier`                                   |

To receive all of them, subscribe to `"*"`. The list can grow, so have your handler skip any type it does not recognize.

## Delivery envelope

All deliveries share one envelope, with the affected object at `data.object`.

```json
{
  "id": "evt_2Hk9sPq7Xc",
  "object": "event",
  "type": "transfer.settled",
  "created_at": "2026-11-04T16:02:41Z",
  "api_version": "2026-10-01",
  "livemode": true,
  "chain_id": 4663,
  "block_number": 18311902,
  "tx_hash": "0x9c1e...4b7a",
  "log_index": 3,
  "confirmation": "l1",
  "data": {
    "object": {
      "object": "transfer",
      "share_class": "cls_3VbT9pLm",
      "from": "0x1111111111111111111111111111111111111111",
      "to": "0x2222222222222222222222222222222222222222",
      "amount": "250",
      "raw": "250000000000000000000",
      "via": "liquidity_window",
      "liquidity_window": "lqw_7Tn4Vb1s"
    }
  }
}
```

### Finality levels

Finality on Robinhood Chain comes in two levels, and the `confirmation` field on each event says which one applied at delivery.

| Value       | Status of the transaction                                        | Usual delay    |
| ----------- | ---------------------------------------------------------------- | -------------- |
| `sequencer` | Soft confirmation: the Robinhood Chain sequencer has included it | Under a second |
| `l1`        | Its batch has been posted to Ethereum                            | Minutes        |

Out of the box, `l1` applies to the settlement events (`transfer.settled`, `offering.closed`, `liquidity_window.cleared`, `distribution.claimed`, `recovery.executed`) and `sequencer` applies to the rest. The Console lets you override the level for each endpoint. Pick `sequencer` when an interface needs speed, and `l1` for whatever feeds books of record.

## Signature verification

A `Selcier-Signature` header accompanies each delivery:

```
Selcier-Signature: t=1793894561,v1=5f2b8e...c91a
```

Here `t` holds the delivery's Unix timestamp, and `v1` holds a hex HMAC-SHA256 computed over the string `{t}.{raw body}` using your endpoint secret as the key. Run verification on the **raw** request body before any JSON parsing happens, and turn away a delivery if its timestamp is older than five minutes.

{% tabs %}
{% tab title="Using the SDK" %}

```ts
import express from "express";
import { Selcier } from "@selcier/sdk";

const selcier = new Selcier(process.env.SELCIER_SECRET_KEY!);
const app = express();

app.post("/selcier", express.raw({ type: "application/json" }), (req, res) => {
  let event;
  try {
    event = selcier.webhooks.constructEvent(
      req.body,
      req.header("Selcier-Signature")!,
      process.env.SELCIER_WEBHOOK_SECRET!,
    );
  } catch (err) {
    return res.status(400).send("Invalid signature");
  }

  switch (event.type) {
    case "transfer.settled":
      // Update your internal cap table view.
      break;
    case "offering.closed":
      // Mark subscriptions as allocated.
      break;
  }

  res.sendStatus(200);
});
```

{% endtab %}

{% tab title="Using node:crypto" %}

```ts
import { createHmac, timingSafeEqual } from "node:crypto";

const TOLERANCE_SECONDS = 300;

export function verifySelcierSignature(rawBody: Buffer, header: string, secret: string): boolean {
  const parts = Object.fromEntries(
    header.split(",").map((part) => part.split("=") as [string, string]),
  );
  const timestamp = Number(parts.t);
  const signatures = header
    .split(",")
    .filter((part) => part.startsWith("v1="))
    .map((part) => part.slice(3));

  if (!timestamp || signatures.length === 0) return false;
  if (Math.abs(Date.now() / 1000 - timestamp) > TOLERANCE_SECONDS) return false;

  const expected = createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody.toString("utf8")}`)
    .digest();

  return signatures.some((sig) => {
    const received = Buffer.from(sig, "hex");
    return received.length === expected.length && timingSafeEqual(received, expected);
  });
}
```

{% endtab %}
{% endtabs %}

While a secret is being rotated, the header holds two `v1` values, one per active secret. The node:crypto example accepts a match on either.

## Acknowledgement and retry behavior

To count as delivered, your endpoint must return a `2xx` status inside 10 seconds. Any other result is a failure, which Selcier retries with exponential backoff for as long as three days. If failures continue for all three days, the event is marked failed, and the Console lets you resend it.

Design your handler around these points:

* **Answer fast.** Return `200` straight away and leave slow work to a background job.
* **Expect repeats.** A single event may arrive multiple times. Deduplicate on the event `id` or on the combination of `tx_hash` and `log_index`.
* **Assume nothing about sequence.** A retry may turn up after later events. If sequence matters, compare `block_number` and `log_index`, or read the current object from the API.
* **Reconcile on a schedule.** Webhooks alone are not enough for books of record, so also poll `GET /v1/events` periodically, beginning at the last block you processed. The events there are identical, which protects you if your endpoint was ever down.

An endpoint that keeps failing for three days gets disabled, and an email goes out to the Console members who own it.

## Trying it out

An endpoint's Console page offers three tools:

* test deliveries for any event type
* a log of every recent delivery, showing request, response and timing
* a resend action for individual events

Sandbox activity on testnet generates exactly the webhooks the same actions would generate on mainnet.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.selcier.xyz/build-on-selcier/event-delivery.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
