> For the complete documentation index, see [llms.txt](https://docs.coinmarketman.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.coinmarketman.com/server-side-alerts.md).

# Server-Side Alerts

Server-side alerts deliver Hyperliquid activity to your server as JSON, as it happens. You define what to watch and provide a URL; HyperTracker posts each match to it. When you configure a signing secret, each request includes a signature your receiver can verify. No polling.

Two common uses: internal systems such as trading bots, risk monitors, and research pipelines; and customer-facing notification features, where deliveries arrive on your own endpoint under your own brand.

> **Beta.** The API contract on this page is stable. Documentation is still being expanded. Report anything that behaves differently than described.

{% hint style="info" %}
**Available to:** All plans, including Free. An API token is required. Alert-management requests and successful webhook deliveries use your plan’s normal shared usage-token allowance. The Free plan currently includes 100 usage tokens per day across the entire HyperTracker API.

To see our plans click [here](https://app.coinmarketman.com/hypertracker/api).
{% endhint %}

### How it works

The system has three objects.

<table><thead><tr><th width="177.16796875">Object</th><th width="451.23046875">Purpose</th><th>Identity</th></tr></thead><tbody><tr><td><strong>Webhook endpoint</strong></td><td>An HTTPS address on your system where deliveries are sent. It can hold a signing secret, a pause switch, and a delivery health readout. Configure a signing secret for production so your receiver can verify each delivery.</td><td><code>id</code></td></tr><tr><td><strong>Alert rule</strong></td><td>A stored rule: which event type to watch, optional wallet addresses, and type-specific filters, plus the endpoint to deliver to.</td><td><code>id</code></td></tr><tr><td><strong>Webhook delivery</strong></td><td>One matched occurrence, POSTed to your endpoint as JSON.</td><td><code>deliveryId</code></td></tr></tbody></table>

An endpoint must exist before an alert rule can be created for it. The full sequence:

1. **Register a webhook endpoint.** Provide an HTTPS URL. All deliveries are sent there.
2. **Create an alert rule.** Select one of the seven event types, reference the endpoint, and set the filters.
3. **Receive deliveries.** Each match arrives as a JSON POST. When a signing secret is configured, the request includes `x-webhook-signature` for verification. No polling is required.

The [Quickstart](/server-side-alerts/quickstart.md) walks through the full integration flow and includes Node and Python receiver templates. The storage function in each template must be implemented for your own system before production use. The rest of this page covers what alert rules can watch and what is delivered.

#### The three IDs

Three IDs are involved. Mixing them up is the most common setup error.

<table><thead><tr><th width="133.59375">ID</th><th width="233.14453125">Where it comes from</th><th>What it is for</th></tr></thead><tbody><tr><td>endpoint <code>id</code></td><td>Response of <code>POST /events/endpoints</code></td><td>Becomes the <code>webhookEndpointId</code> you pass when creating an alert rule</td></tr><tr><td>alert-rule <code>ID</code></td><td>Response of <code>POST /events</code></td><td>Arrives on every delivery as <code>eventId</code> and in the <code>x-event-id</code> header. Use it to read, update, or delete the alert rule</td></tr><tr><td><code>deliveryId</code></td><td>Present on every delivery</td><td>Your idempotency key. Store the complete delivery once using it as a unique key before returning <code>2xx</code>; treat a repeated <code>deliveryId</code> as a safe duplicate</td></tr></tbody></table>

The endpoint ID and the alert-rule ID come from different resources and are never interchangeable.

### Responsibilities

<table><thead><tr><th width="125.83984375"></th><th>HyperTracker</th><th>You</th></tr></thead><tbody><tr><td>Setup</td><td>Accepts your endpoint and validates your alert rule</td><td>Run a public HTTPS receiver, pick an event type and valid filters</td></tr><tr><td>Watching</td><td>Consumes market and account streams, evaluates every alert rule continuously</td><td>Nothing. No polling required</td></tr><tr><td>Delivering</td><td>Queues a stable delivery and POSTs it as JSON</td><td>Configure a signing secret, verify <code>x-webhook-signature</code> over the exact raw body, store each delivery once using <code>deliveryId</code>, then process the stored event</td></tr><tr><td>Recovering</td><td>Retries retryable failures, suspends endpoints that keep failing</td><td>Return <code>2xx</code> only after the complete delivery is stored successfully, then process the stored delivery asynchronously. Watch endpoint health</td></tr></tbody></table>

### Event types

Seven event types. Some watch wallets you specify, some watch a market, some watch the entire exchange.

<table data-search="false"><thead><tr><th width="193.1640625">eventType</th><th>Fires when</th><th>Wallet addresses</th></tr></thead><tbody><tr><td><code>position_activity</code></td><td>a tracked wallet makes any trade (non-TWAP fills)</td><td>Required, 1 to 1000</td></tr><tr><td><code>twap_execution</code></td><td>a tracked wallet's TWAP order fills a slice</td><td>Required, 1 to 1000</td></tr><tr><td><code>twap_lifecycle</code></td><td>a TWAP order starts, finishes, is cancelled, or errors</td><td>Optional, max 1000 (blank = exchange-wide)</td></tr><tr><td><code>ledger_update</code></td><td>a deposit or withdrawal happens</td><td>Optional, max 1000 (blank = exchange-wide)</td></tr><tr><td><code>liquidation</code></td><td>a position gets liquidated</td><td>Optional, max 1000 (blank = every liquidation on the exchange)</td></tr><tr><td><code>price_crossing</code></td><td>a market's price crosses your threshold</td><td>Not used, this watches a market</td></tr><tr><td><code>large_market_order</code></td><td>anyone trades above your USD threshold</td><td>Not supported, exchange-wide by design</td></tr></tbody></table>

Leaving addresses blank on the exchange-wide types is supported. It also produces the highest delivery volume, and each successful webhook delivery uses one usage token. See [Billing](#billing) before using it in production.

Each type below documents what triggers it, the available filters, the payload, and type-specific behaviour.

#### position\_activity

Fires when a wallet you track makes a trade. Non-TWAP perpetual fills only.

* **Addresses:** required, 1 to 1000
* **Filters:** `coin`. Omit it to watch every perpetual market that wallet trades

**Payload**

{% code overflow="wrap" expandable="true" %}

```
{
  "address": "0xabc...",
  "coin": "BTC",
  "fillCount": 1,
  "liquidationCount": 0,
  "firstFillAt": 1787824800000,
  "lastFillAt": 1787824800000,
  "fills": [
    {
      "id": "4828...", "coin": "BTC", "px": "65000.0", "sz": "0.10",
      "side": "B", "time": 1787824800000, "oid": 527172081184,
      "tid": 1105194790626028, "hash": "0xdef..."
    }
  ]
}
```

{% endcode %}

**Behaviour:** The fill is delivered exactly as the exchange recorded it, without interpretation into "opened a position" or "closed a position". Fills are grouped by alert rule, address, and coin, and large batches are split across several deliveries to keep payloads bounded, so one burst of trading can arrive as several messages.

Fill objects are source-rich. Alongside the fields above they carry `fullCoinName`, `fillType`, `dir`, `startPosition`, `closedPnl`, `fee`, `crossed`, `blockNumber`, `blockTime`, `localTime`, `sentTime`, and where applicable `twapId` and a `liquidation` object. Validate the fields you consume and ignore the rest, because more will be added.

#### twap\_execution

Fires when a wallet you track has a TWAP order fill a slice. Only fills carrying a `twapId`.

* **Addresses:** required, 1 to 1000
* **Filters:** `coin`

**Payload**

{% code overflow="wrap" expandable="true" %}

```
{
  "address": "0xabc...",
  "coin": "BTC",
  "twapId": 2148161,
  "fillCount": 1,
  "firstFillAt": 1787824800000,
  "lastFillAt": 1787824800000,
  "fills": [
    { "id": "4828...", "twapId": 2148161, "px": "65000.0", "sz": "0.05", "side": "A", "time": 1787824800000 }
  ]
}
```

{% endcode %}

**Behaviour:** A single large TWAP fills gradually in many small pieces, and each piece, or a few that land together, is its own delivery. For the order's status rather than its individual fills, use `twap_lifecycle`.

#### twap\_lifecycle

Fires when a TWAP order changes status: activated, finished, terminated, or errored. Non-spot TWAP records with a non-empty status.

* **Addresses:** optional, max 1000. Blank watches the entire exchange
* **Filters:** `coin`

**Payload**

{% code overflow="wrap" expandable="true" %}

```
{
  "address": "0xabc...",
  "coin": "BTC",
  "twapId": 2148161,
  "status": "activated",
  "description": "",
  "side": "B",
  "size": "1.0",
  "executedSize": "0.0",
  "executedNotional": "0.0",
  "minutes": 30,
  "reduceOnly": false,
  "randomize": true,
  "timestamp": 1787824800000
}
```

{% endcode %}

**Behaviour:** One delivery per status change. Orders and coins are never mixed into one message. Source error objects are normalised to `status: "error"` with the message moved into `description`.

#### ledger\_update

Fires when a deposit or withdrawal happens.

* **Addresses:** optional, max 1000. Blank watches every wallet on the exchange
* **Filters:** `types` (array of `deposit` and/or `withdraw`, omitted means both), `amount` (object with one or more of `eq`, `gte`, `gt`, `lte`, `lt`, values as decimal strings)

**Payload**

{% code overflow="wrap" expandable="true" %}

```
{
  "address": "0x47cfef07ee1a6216924b5d2a203b8d64fabafc36",
  "type": "deposit",
  "amount": "12500.50",
  "time": "1787824800000",
  "hash": "0xabc123...",
  "users": ["0x47cfef07ee1a6216924b5d2a203b8d64fabafc36"],
  "delta": { "type": "deposit", "usdc": "12500.50" }
}
```

{% endcode %}

**Behaviour:** One source update produces one delivery per unique affected address, so a single transfer can generate more than one message.

#### liquidation

Fires when a position gets liquidated. This is the liquidated user's side of the fill, identified by `fill.liquidation.liquidatedUser == fill.address`. Position side is derived from the sign of `startPosition`.

* **Addresses:** optional, max 1000. Blank watches every liquidation on the exchange
* **Filters:** `thresholdUsd` (optional, greater than 0), `markets`, `side` (`long`, `short`, or `both`, defaults to both)

**Payload**

{% code overflow="wrap" expandable="true" %}

```
{
  "liquidations": [
    {
      "address": "0xdef...",
      "market": "BTC",
      "side": "long",
      "size": "0.5",
      "usdNotional": "32000.0",
      "executionPrice": "64000",
      "fills": [ { "id": "4829...", "px": "64000.0", "sz": "0.5", "startPosition": "0.5" } ],
      "sourceEventId": "0xfeed...",
      "eventTime": 1787824800000
    }
  ]
}
```

{% endcode %}

**Behaviour:** The payload contains the liquidated wallet, the market, the size, the USD value, the execution price, and the side. Fills are grouped by liquidated address, market, and source hash.

#### price\_crossing

Fires when a market's observed price crosses your threshold.

* **Addresses:** not used, this watches a market rather than a wallet
* **Filters:** `market` (required), `threshold` (required, positive decimal string), `direction` (`above`, `below`, or `both`, defaults to both)

**Payload**

{% code overflow="wrap" expandable="true" %}

```
{
  "market": "BTC",
  "threshold": "65000.00",
  "direction": "above",
  "previousPrice": "64999.90",
  "triggerPrice": "65000.10",
  "eventTime": 1787824800000
}
```

{% endcode %}

**Behaviour:** A crossing requires movement across the threshold. An `above` event requires the previous price at or below the threshold and the new price strictly above it; `below` is the inverse. Touching the threshold does not trigger an event, and price history from before the alert rule was created is never evaluated.

There is no cooldown. The alert rule triggers on every crossing for as long as it exists, so a price oscillating around the threshold produces one delivery per crossing. For a single notification, pause or delete the alert rule after the first delivery.

#### large\_market\_order

Fires when anyone places a trade above your USD threshold, anywhere on the exchange. Aggregated non-liquidation perpetual fills, grouped by wallet, market, and order ID.

* **Addresses:** not supported, this is exchange-wide by design
* **Filters:** `thresholdUsd` (required, decimal string, minimum `50000`), `markets`, `direction` (`buy`, `sell`, or `both`, defaults to `both`)

**Payload**

{% code overflow="wrap" expandable="true" %}

```
{
  "orders": [
    {
      "address": "0xabc...",
      "market": "BTC",
      "direction": "buy",
      "positionEffect": "increase",
      "orderId": 527172081184,
      "aggregatedSize": "1.0",
      "usdNotional": "65000.0",
      "averageExecutionPrice": "65000",
      "fills": [ { "id": "4828...", "px": "65000.0", "sz": "1.0" } ],
      "eventTime": 1787824800000
    }
  ]
}
```

{% endcode %}

**Behaviour:** Liquidations are excluded, as they have their own event type, so a forced sale worth $200,000 does not also appear here. Notional is the sum of `size * price` across the order's fills, and average execution price is total notional divided by total size.

### Filters and matching

<table data-search="false"><thead><tr><th>Filter</th><th>Accepted value</th></tr></thead><tbody><tr><td><code>coin</code> / <code>markets</code></td><td>Non-empty array of up to 100 unique market strings</td></tr><tr><td><code>ledger_update.types</code></td><td>Non-empty unique array containing <code>deposit</code> and/or <code>withdraw</code></td></tr><tr><td><code>ledger_update.amount</code></td><td>Object with one or more of <code>eq</code>, <code>gte</code>, <code>gt</code>, <code>lte</code>, <code>lt</code>. Values are decimal strings</td></tr><tr><td><code>price_crossing.threshold</code></td><td>Positive decimal string</td></tr><tr><td><code>price_crossing.direction</code></td><td><code>above</code>, <code>below</code>, or <code>both</code></td></tr><tr><td><code>large_market_order.thresholdUsd</code></td><td>Decimal string greater than or equal to <code>50000</code></td></tr><tr><td><code>large_market_order.direction</code></td><td><code>buy</code>, <code>sell</code>, or <code>both</code> (defaults to <code>both</code>)</td></tr><tr><td><code>liquidation.thresholdUsd</code></td><td>Optional positive decimal string</td></tr><tr><td><code>liquidation.side</code></td><td><code>long</code>, <code>short</code>, or <code>both</code></td></tr></tbody></table>

Address and filter dimensions combine with **AND**. Multiple values inside one address list or market list combine with **OR**. A `liquidation` alert rule with two addresses and `markets: ["BTC","ETH"]` therefore means "either of these wallets, on either of these markets".

**Decimals:** Where financial values such as prices, sizes, amounts, or USD thresholds are documented as decimal strings, preserve them as strings or parse them with a decimal library when exact arithmetic or comparisons matter. Binary floating-point conversion can introduce rounding error near threshold boundaries.

### What a delivery looks like

Every match arrives as a JSON POST: a common envelope, plus a `data` object whose shape depends on the event type.

{% code overflow="wrap" expandable="true" %}

```
{
  "deliveryId": "6a75e6c520d7a6a74814c55af4b671da10b05d4c37861f0d1d9d7126dfe6115f",
  "consumerId": 1327,
  "webhookEndpointId": 23,
  "eventId": 105,
  "eventType": "price_crossing",
  "occurredAt": "2026-08-27T10:00:00.000Z",
  "data": {
    "market": "BTC",
    "threshold": "65000.00",
    "direction": "above",
    "previousPrice": "64999.90",
    "triggerPrice": "65000.10",
    "eventTime": 1787824800000
  }
}
```

{% endcode %}

<table data-search="false"><thead><tr><th>Field</th><th>Meaning</th></tr></thead><tbody><tr><td><code>deliveryId</code></td><td>Stable SHA-256 delivery identity. Your idempotency key</td></tr><tr><td><code>consumerId</code></td><td>The HyperTracker account that owns the alert rule</td></tr><tr><td><code>webhookEndpointId</code></td><td>Which endpoint this was sent to</td></tr><tr><td><code>eventId</code></td><td>Which alert rule matched</td></tr><tr><td><code>eventType</code></td><td>One of the seven event types</td></tr><tr><td><code>occurredAt</code></td><td>ISO 8601 event timestamp chosen by the event processor</td></tr><tr><td><code>data</code></td><td>Event-specific payload, documented per type above</td></tr></tbody></table>

Request headers:

| Header                  | Value                                                                           |
| ----------------------- | ------------------------------------------------------------------------------- |
| `content-type`          | `application/json`                                                              |
| `x-event-id`            | alert-rule ID                                                                   |
| `x-webhook-event`       | eventType                                                                       |
| `x-webhook-delivery-id` | deliveryId                                                                      |
| `x-webhook-signature`   | `v1=<lowercase hex HMAC-SHA256>`, present only when a `secretKey` is configured |

Configure a `secretKey` for every production endpoint. If you omit it, `x-webhook-signature` is not sent, so your receiver cannot verify that a request came from HyperTracker or that its body was unchanged. With a secret configured, compute the HMAC over the exact raw request bytes and compare it in constant time before parsing the JSON. Re-serialising the body first changes whitespace or key order and produces a false mismatch. The Quickstart includes Node and Python verification templates.

### Delivery semantics

Four behaviours affect integration design.

**At-least-once:** A timeout is an unknown outcome: the request may have been processed even though no response was received, so it is sent again. The same event can arrive twice. Store the complete delivery once using `deliveryId` as a unique key before returning `2xx`, and treat a repeated `deliveryId` as a safe duplicate.

**2xx means delivered:** Return `2xx` only after the complete delivery has been stored successfully. Process the stored delivery asynchronously, off the request path.

**Retries:** HTTP 408, 409, 425, 429, and 5xx are retryable, as are network transport failures. A valid `Retry-After` header extends the backoff up to the configured maximum.

**Ordering is local, not global:** Deliveries for one alert rule are processed sequentially within a consumed batch. Different alert rules run concurrently. Do not assume ordering across events, markets, addresses, or endpoints.

**Delivery count is not occurrence count:** Batches are split to bound payload size, and several types group by address or coin. Allow for a variable number of deliveries per burst of activity, in application logic and in cost estimates.

#### Paused versus suspended

Two distinct states. `paused` is set by you; `deliveryStatus` is set by the delivery health system.

| State                         | Set by                                         | Behaviour                                                                                 |
| ----------------------------- | ---------------------------------------------- | ----------------------------------------------------------------------------------------- |
| `paused: true`                | You, via `PATCH` on the endpoint or alert rule | Delivery stays off until you turn it back on. Nothing will silently resume it             |
| `deliveryStatus: "suspended"` | The delivery health system                     | Deliveries are skipped during a cooldown. A later real event is used as a half-open probe |
| `deliveryStatus: "healthy"`   | The delivery health system                     | Endpoint is eligible for delivery                                                         |

The current suspension policy: a failure is recorded once the per-event retries are exhausted. **Ten failures within five minutes suspend an endpoint.** A successful delivery clears the failure window. Repeated failed probes back the cooldown off exponentially from five minutes up to one hour.

Events skipped during a suspension are dropped before usage authorisation, so they are not billed and **they are not replayed**. A suspension window is a permanent gap in the data. To investigate one, read `deliveryStatus`, `suspendedUntil`, `lastDeliveryFailureAt`, and `lastDeliveryFailureReason` on the endpoint resource.

### Billing

Every alert-management API request and every successful webhook delivery uses one usage token from your plan’s shared allowance. The Free plan currently includes 100 usage tokens per day across the entire HyperTracker API, so Server-Side Alerts shares that allowance with your other API usage.

* Events API requests (create, list, update, delete): 1 usage token each
* Successful webhook deliveries: 1 usage token each
* Failed delivery attempts and retries: 0 usage tokens
* Events skipped while an endpoint is paused or suspended: 0 usage tokens

An exchange-wide alert rule with no address filter generates the most deliveries and therefore uses the most tokens. A `liquidation` or `ledger_update` alert rule with blank addresses matches the whole exchange, and each successful webhook delivery uses one usage token. Start with narrow filters, measure the delivery rate, then widen.

### Building white-label alerts

> The sections above document the API contract. This section is **implementation guidance** for building a customer-facing product on top of it.

**Route to end customers with `eventId`:** There is no field for attaching a custom reference ID to an alert rule. The supported approach is one alert rule per end customer, plus a lookup from `eventId` to your customer ID. First verify `x-webhook-signature` against the raw body, then parse the verified body and use its `eventId` as the routing key. Do not route from `x-event-id` before verification; the signature covers the body, not the headers.

**Shard around the caps:** An alert rule holds at most 1000 addresses, and a market filter at most 100 entries. One alert rule per customer stays inside both limits and keeps the routing key meaningful. If a single customer tracks more than 1000 wallets, split them across several alert rules and map each `eventId` back to the same customer.

**Plan for endpoint suspension:** If all customers share one endpoint, ten delivery failures in five minutes disable alerts for every customer at once, and skipped events are not replayed. Monitor `deliveryStatus` and alert your own on-call team rather than relying on automatic recovery. Separate endpoints per tier or region limit the scope of a suspension, at the cost of more endpoints to monitor.

**Cost scales with delivery volume, not customer count:** At one usage token per successful delivery, cost is driven by each customer's event rate. A customer watching one wallet generates far fewer deliveries than one using an exchange-wide liquidation alert rule. Where several customers want the same exchange-wide feed, run one alert rule and fan it out within your own system.

### Known limitations in beta

* No custom reference ID on an alert rule. Routing to an end customer goes through `eventId`
* `price_crossing` payloads do not say which price feed the crossing was measured against
* Events skipped during an automatic suspension are never replayed
* There is no endpoint for retrieving past deliveries, so a delivery you dropped cannot be fetched again
* `price_crossing` has no cooldown and no one-shot mode. Pause or delete the alert rule yourself
* `large_market_order` cannot be scoped to specific wallets


---

# 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 dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.coinmarketman.com/server-side-alerts.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

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.
