# Every alert as signed JSON to your own server.

> Paste one https address and every level alert, fired alert line and order alert is POSTed to it with the price, the levels of the book at that second and a signature only you can check. Wire it into a bot, a journal or a dashboard.

Source: https://firmtape.com/features/live/webhooks

LIVE.

![The webhook block on the Level alerts page with its address, signing secret and test button](https://archive.firmtape.com/press/features/alerts/destinations.69afa157.webp)

**Why:** every alert as data your own code can act on. **Where:** Account, Level alerts, **Webhook**.

## How to use it

1. Paste one `https://` address and press **Save**.
2. Press **Send test event** to post an event of type `test`.
3. Open **Signing secret** and keep it on your server; verify every request with it.
4. **Rotate secret** replaces it; the old one stops verifying at once.

## What it measures

Every event on your list (the same symbols, events and holds as Telegram), every fired alert line and every order alert is POSTed to the address as JSON, signed with HMAC-SHA256 over the timestamp header, a dot and the raw body.

```
POST https://your.server/firmtape
Content-Type: application/json
X-FirmTape-Event: flip_cross
X-FirmTape-Event-Id: evt_3f9a0c6e21d4b7a8e5f10c2d
X-FirmTape-Timestamp: 1760034725
X-FirmTape-Signature: sha256=<hex HMAC-SHA256 of "<timestamp>.<raw body>">

{"version": 1, "id": "evt_3f9a0c6e21d4b7a8e5f10c2d", "type": "flip_cross",
 "symbol": "SPX", "level": {"kind": "flip", "value": 6715.5}, "price": 6712.25,
 "book": "volume", "ts": "2026-10-09T14:32:05-04:00", "day": "2026-10-09",
 "levels": {"flip": 6715.5, "call_wall": 6750, "put_wall": 6700, "put_support": 6700},
 "text": "SPX crossed below the flip 6,715.50", "direction": "below"}
```

| Field | What it holds |
| --- | --- |
| type | `flip_cross`, `wall_break`, `regime_change`, `wall_move`, `flip_move`, `level_near`; for an alert line `price_alert` (a price line) or `level_line` (a line that follows a level); for an order rule's match `order` (its `rule` and `order`: time, structure, legs, expiry, size, premium, points from the flip and a wall, never a side); or `test` |
| level | `kind` is `flip`, `call_wall`, `put_wall` or, for a regime change, `net_gamma`; `value` the level; a moved level adds `from` |
| levels | The book's `flip`, `call_wall` and `put_wall` at the event. Deprecated: `put_support`, the same number as `put_wall`, is sent until 2026-11-30; read `put_wall` |
| direction | `above` or `below` for a cross or an approach, `up` or `down` for a move, `positive` or `negative` for a regime |
| ts, day | When the event was found, ISO 8601 in New York time with its offset, and the ET trading day |
| id | Stable: every retry of one event carries the same id, so drop one you have seen |
| also | `threshold_points` for `flip_move` and `level_near` (SPX points); `from` and `basis` for ES, NQ and RTY; `levels_asof` for a ticker's five-minute book |
| alert | An alert line's own: `id`, `kind` (`price` or `level`), `level` (`flip`, `call_wall`, `put_wall`), `price`, `mode` (`once` or `repeat`) |

**Verify every request**: recompute the signature over the timestamp header, a dot and the raw body, compare in constant time, and refuse a timestamp more than five minutes old. The POSITIONS webhook is signed the same way; Geo's webhooks sign theirs differently, and the [developer reference](https://firmtape.com/docs/developers/webhooks) has each one and the single format all three move to. Until 2026-10-11 the POSITIONS webhook signed the raw body alone; that signature is still sent, until 2026-11-30, as `X-FirmTape-Signature-V0`.

```
// Node
const crypto = require("crypto");
function verify(rawBody, headers, secret) {
  const t = headers["x-firmtape-timestamp"], sig = headers["x-firmtape-signature"] || "";
  if (!t || Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
  const want = "sha256=" + crypto.createHmac("sha256", secret).update(t + "." + rawBody).digest("hex");
  return sig.length === want.length && crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(want));
}

# Python
import hmac, hashlib, time
def verify(raw_body: bytes, headers, secret: str) -> bool:
    t = headers.get("X-FirmTape-Timestamp", "")
    if not t or abs(time.time() - int(t)) > 300: return False
    want = "sha256=" + hmac.new(secret.encode(), t.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(want, headers.get("X-FirmTape-Signature", ""))
```

## Limits

Answer with any 2xx within three seconds. No answer, a 429 or a 5xx is tried again after one and four seconds, three tries in all; redirects are not followed. Ten failed events in a row switch the webhook off, and the account page shows why; saving the address switches it back on. Up to thirty events a minute per account. An address that resolves to a private, loopback or link-local address is refused.

## Related

- [LIVE](https://firmtape.com/features/live)
- [LIVE: Where alerts go](https://firmtape.com/docs/live#destinations)
- [LIVE: Level alerts on a watchlist](https://firmtape.com/docs/live#level-alerts)
- [ORDERS: Order rules](https://firmtape.com/docs/orders#order-rules)

## Questions

**Can I receive alerts without Telegram?**

Yes. The webhook, push and Telegram are independent destinations; turn on any of them.

**How do I test my endpoint?**

Press Send test event on the Level alerts page. It posts an event of type test with the same headers and signature.

**What happens if my server is down?**

The event is tried three times in all. After ten failed events in a row the webhook switches off until you save the address again.

**Does the payload say which way to trade?**

No. It carries the event, the price and the levels of the book at that second. An order event carries the order's structure and size, never a side.
