> For the complete documentation index, see [llms.txt](https://mute-sh.gitbook.io/mute.sh/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://mute-sh.gitbook.io/mute.sh/api/api-reference/webhooks-telegram-and-account.md).

# Webhooks, Telegram & Account

Guaranteed-delivery fill/order events pushed to your endpoint (or a Telegram chat), plus the org-management surface: API keys and billing.

## Why webhooks (read this if you're building mobile)

Prediction-market apps are **mobile apps**, and a phone cannot hold an SSE or WebSocket connection open — the OS kills background connections within seconds. Webhooks are the only way a mobile app stays alive: mute POSTs the signed event to **your** server the moment a watched bet fills, a market settles, or (Enterprise) a goal is scored; your server fires the APNs/FCM push; your user sees *"Your bet just filled ✅"* on the lock screen. Payloads are signed (verifiable, replay-window protected), with automatic retries, delivery history, and replay on Enterprise, and optional Telegram delivery. Without webhooks you're polling from a phone — burning battery, hitting rate limits, and missing the moment.

**Tier gate — webhook subscriptions are Scale + Enterprise.** Creating a webhook subscription requires the **Scale** or **Enterprise** tier. Free and Growth keys authenticate fine but get a `403` naming the Scale requirement when they try to create one. The Enterprise **power features** — order-status events, predicate filters, the market-wide firehose, delivery history, and replay — sit one tier further up and each return their own `403` naming the Enterprise requirement.

**Who can call what:** within an eligible tier, webhook subscriptions and Telegram linking accept **either** a dashboard session or your org's `mute_` API key. Key management (`/v1/keys*`) and billing are **dashboard-session only** — by design, a leaked data key can never mint keys, rotate itself, or touch billing. Telegram linking, key management, and billing are **not** tier-gated; they work on every plan.

## Webhook subscriptions: /v1/webhooks/subscriptions

Watch wallets and builders; get signed HTTP POSTs (or Telegram messages) for their fills — and on Enterprise, order events, predicate filters, and a market-wide firehose. Plan allowances — **Scale: 50 subscriptions × 500 watched addresses each; Enterprise: unlimited** + the power features. Free and Growth have no webhook access ([pricing](/mute.sh/api/getting-started/pricing.md#webhook-allowances)).

### POST /v1/webhooks/subscriptions — create

```bash
curl -X POST -H "X-API-Key: $MUTE_KEY" -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-server.example/hooks/mute",
    "event_types": ["fill"],
    "wallets": ["0xaddc680878f96b93189f700d17f4a9bfcf80ff54"],
    "filters": { "min_notional": 1000 }
  }' \
  "https://api.mute.sh/v1/webhooks/subscriptions"
```

```json
{
  "id": "sub_elShNRvtZsxvS904YJKGR5uW",
  "owner": "org:5958b5b9-…",
  "wallets": ["0xaddc680878f96b93189f700d17f4a9bfcf80ff54"],
  "builders": [],
  "event_types": ["fill"],
  "channel": "http",
  "url": "https://your-server.example/hooks/mute",
  "filters": { "min_notional": 1000 },
  "scope": "address",
  "status": "active",
  "created_at": 1783328449,
  "updated_at": 1783328449,
  "signing_secret": "whsec_one13jjcuPVNGPZzwHaNHd9VSVCUVclwMupmwxjI"
}
```

**`signing_secret` appears exactly once** — here and in `rotate-secret` responses. Store it; every other read strips it.

Body fields:

| Field                  | Description                                                                                                                           |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `event_types`          | `["fill"]`, or include `"order"` (order placed/canceled/triggered — **Enterprise**)                                                   |
| `wallets` / `builders` | watched addresses; together capped by your plan's per-webhook allowance                                                               |
| `channel`              | `http` (default; requires `url`, HTTPS enforced) or `telegram` (requires a linked `telegram_chat_id` — see below)                     |
| `filters`              | server-side AND filters: `coins[]`, `min_notional`, `sides[]`, `dirs[]`, `statuses[]` (orders)                                        |
| `predicate`            | **Enterprise** — recursive boolean filter, below                                                                                      |
| `scope`                | `address` (default) or `market` — the **Enterprise firehose**: no address list, your predicate runs against every fill/order on HIP-4 |

Tier violations are specific `403`s naming the feature and required plan; exceeding your subscription count is a `403` naming your plan's limit.

### Predicates (Enterprise)

A JSON expression tree evaluated server-side before delivery:

```json
{
  "all": [
    { "field": "usd", "op": ">=", "value": 10000 },
    { "str_field": "dir", "op": "eq", "value": "Buy" },
    { "any": [
      { "str_field": "coin", "op": "eq", "value": "#7380" },
      { "str_field": "coin", "op": "eq", "value": "#7381" }
    ]}
  ]
}
```

* Groups: `all` (AND) / `any` (OR) with 1–32 children, `not`, max depth 8, max 256 nodes.
* Numeric leaves: `field` ∈ `px` `sz` `usd`/`notional`; `op` ∈ `< <= > >= == !=` (or word forms) plus `between` with `lo`/`hi`.
* String leaves: `str_field` ∈ `coin` `dir` `side`; `op` ∈ `eq` `ne` `starts_with`.
* On `PATCH`, `predicate: null` **clears** the predicate, omitting the field leaves it unchanged.

Combined with `scope: "market"` this is "deliver every Buy over $10k on these coins, network-wide" — no address list needed.

### Verifying deliveries

Each delivery carries `X-HL-Signature: t=<unix_secs>,v1=<hex>` where `v1 = HMAC-SHA256(signing_secret, "{t}.{raw_body}")`, plus `X-HL-Event-Id`, `X-HL-Event-Type`, and `X-HL-Delivery` headers.

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

function verify(sigHeader: string, rawBody: string, secret: string): boolean {
  const { t, v1 } = Object.fromEntries(sigHeader.split(",").map((p) => p.split("=")));
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false; // 5-min replay window
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  return timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}
```

Delivery payload: `{type, id, sub_id, user, builder?, coin, px, sz, notional, side, dir?, status?, event_id, reasons[]}` — `reasons` tells you *why* it matched (`wallet`, `builder`, or `predicate`). Resolve `coin` to a display name via [`/v1/markets/coin-labels`](/mute.sh/api/api-reference/markets.md#get-v1marketscoin-labels).

### Manage: GET · GET /:id · PATCH /:id · DELETE /:id · POST /:id/rotate-secret

All accept session or API key. `PATCH` takes any subset of the create fields (tier gates re-checked on the merged result) — set `status: "paused"` / `"active"` to pause/resume. `DELETE` returns `{"deleted": "<id>"}`. `rotate-secret` returns the subscription **with a fresh `signing_secret`** and invalidates the old one immediately (no dual-secret grace window — update your verifier first, then rotate).

### Enterprise: GET /:id/deliveries · POST /:id/replay

* **`deliveries`** — the failed-delivery log: `{"failed": [{sub_id, event_id, event_type, url, body, last_status, attempts, created_at, ttl}]}`. `body` is the exact JSON that was attempted, byte-identical. Entries expire after 14 days.
* **`replay`** — re-enqueues every logged failure (up to 1000) with its original body; returns `{"replayed": <count>}`.

Non-Enterprise calls get a `403` naming the feature.

### GET /v1/webhooks/usage

Your org's delivery counters for the current period:

```json
{ "period": "2026-07", "usage": { "matched": 0, "delivered": 0, "failed": 0, "bytes": 0 } }
```

`matched` = events that passed your filters; `delivered`/`failed` = HTTP outcomes; `bytes` = payload volume.

***

## Telegram: /v1/telegram/\*

Deliver webhook events to a Telegram chat instead of (or alongside) HTTP. Linking is **pairing-code only** — a chat can never be attached by guessing its id:

1. **`GET /v1/telegram/status`** → `{ "configured": true, "bot_username": "muteshbot", "deep_link": "https://t.me/muteshbot" }` — send the user to the bot.
2. The user DMs the bot `/start`; the bot replies with a single-use 8-character code (`XXXX-XXXX`, 5-minute TTL).
3. **`POST /v1/telegram/pair`** with `{ "code": "XXXX-XXXX" }` → `201 { "linked": true, "chat": { "chat_id": "…", "username": "…", "title": null, "type": "private" } }`. Wrong/expired codes are a `400` telling the user to `/start` again; pairing attempts are rate-limited to 5/minute.
4. Create a subscription with `channel: "telegram"` and that `telegram_chat_id`.

**`GET /v1/telegram/chats`** lists your org's linked chats (newest first); **`DELETE /v1/telegram/chats/:chatId`** unlinks — idempotent, returns `{ "removed": true|false }` rather than a 404.

***

## API keys: /v1/keys (dashboard session only)

Manage keys at [app.mute.sh](https://app.mute.sh) → API Keys. Calling these with an API key returns `403` (`"…this API key cannot manage organization settings."`) — that's the security model working, not a bug. All operations are audit-logged.

| Route                         | Behavior                                                                                                                                                                 |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `POST /v1/keys`               | body `{name?, expiresInDays?}` (1–366 days, omit for non-expiring). Returns the raw key **once**: `{apiKey, id, start, name, expiresAt}`                                 |
| `GET /v1/keys`                | metadata only: `{id, name, start, prefix, enabled, createdAt, lastRequest, expiresAt}` — secrets are never listable                                                      |
| `DELETE /v1/keys/:keyId`      | revoke → `{revoked: keyId}`                                                                                                                                              |
| `POST /v1/keys/:keyId/rotate` | creates the replacement **before** revoking the original (no coverage gap) → `{apiKey, id, start, name, rotatedFrom}`                                                    |
| `GET /v1/keys/usage`          | the org's live monthly usage vs quota — **pooled across all the org's keys**, same numbers the [`X-Quota-*` headers](/mute.sh/api/getting-started/rate-limits.md) report |

## Billing: /v1/billing (dashboard session only)

* **`POST /v1/billing/checkout`** — body `{tier: "growth"|"scale"}` → `{url}` for a Stripe-hosted checkout. Enterprise has no self-serve checkout (contact sales); the org and email are taken from your session, never the request body.
* **`POST /v1/billing/portal`** — `{url}` for the Stripe customer portal (manage payment method, invoices, cancel). `400 no billing account` if the org never checked out.

Tier changes (checkout completion, subscription updates, cancellations) apply automatically via Stripe webhooks — your `X-Tier` response header reflects the new tier on the next request; a canceled subscription downgrades to free at period end.


---

# 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://mute-sh.gitbook.io/mute.sh/api/api-reference/webhooks-telegram-and-account.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.
