> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tako.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhook deliveries

> The request Tako posts when a monitor fires, and how to verify it

export const MonitorsExperimental = () => <Warning>
    <strong>Monitors is experimental.</strong> Request and payload shapes can change without a deprecation window. Monitors isn't recommended for production workflows.
  </Warning>;

<MonitorsExperimental />

When a monitor fires, Tako posts one request to every [notification channel](/api-reference/notification-channels-create) listed on that monitor. This page describes that request: its headers, its body, how to verify the signature, and how Tako retries a delivery that fails.

Tako follows the [Standard Webhooks](https://www.standardwebhooks.com/) specification, so a library that verifies Standard Webhooks signatures verifies these.

## Headers

| Header              | Value                                                                                     |
| ------------------- | ----------------------------------------------------------------------------------------- |
| `webhook-id`        | The delivery id. Every retry of one delivery sends the same id, so use it to deduplicate. |
| `webhook-timestamp` | When Tako sent this attempt, in Unix seconds.                                             |
| `webhook-signature` | `v1,` followed by the base64 HMAC-SHA256 signature.                                       |
| `content-type`      | `application/json`                                                                        |
| `user-agent`        | `Tako-Webhooks/1`                                                                         |

## The request body

A channel whose `kind` is `webhook` receives JSON in this shape:

```json theme={null}
{
  "id": "4b1c2f2e-2a1b-4c7e-9f0a-6d3c1e5b8a90",
  "type": "monitor.fired",
  "created_at": "2026-09-12T20:05:00+00:00",
  "summary": "AAPL is up 3.2%, past your 3% threshold.",
  "monitor": {
    "id": "0d6f1e77-4b9a-4f0e-8a4d-2c9e7b1f3a55",
    "name": "AAPL big move",
    "type": "stocks.pct_change",
    "parameters": {
      "trading_item_id": 2590360,
      "window": "1d",
      "threshold": 3,
      "direction": "up"
    }
  },
  "firing": {
    "id": "9a7c5d31-8e2b-4a66-b0d7-5f4e3c2a1b09",
    "occurrence_key": "2026-09-12",
    "occurred_at": "2026-09-12T20:00:00+00:00",
    "trigger": "push"
  },
  "payload": {
    "ticker": "AAPL",
    "exchange": "XNAS",
    "window": "1d",
    "baseline_date": "2026-09-11",
    "baseline_close": 226.41,
    "price": 233.7,
    "percent_change": 3.2188
  }
}
```

| Field        | Type   | Description                                                                                                                                                                       |
| ------------ | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`         | string | The delivery id, matching the `webhook-id` header.                                                                                                                                |
| `type`       | string | The event type. Today the only value is `monitor.fired`.                                                                                                                          |
| `created_at` | string | When Tako recorded the firing, in ISO 8601.                                                                                                                                       |
| `summary`    | string | One sentence describing the firing, built from the monitor's parameters and the payload. Tako builds it at delivery time, so the wording can change without the payload changing. |
| `monitor`    | object | The monitor that fired.                                                                                                                                                           |
| `firing`     | object | The firing itself.                                                                                                                                                                |
| `payload`    | object | The values that caused the firing. Its fields depend on the monitor type.                                                                                                         |

`monitor` carries `id`, `name`, `type`, and `parameters`, which is the stored parameter object rather than the input you sent to [Create a monitor](/api-reference/monitors-create). A type that resolves a subject stores the resolved value, so a monitor you created with a ticker carries a `trading_item_id`.

`firing` carries `id`, `occurrence_key`, `occurred_at`, and `trigger`:

* `occurrence_key` identifies the occurrence. Tako records one firing per key per monitor, so a source that reposts the same occurrence doesn't fire twice.
* `occurred_at` is when the occurrence happened in the data. `created_at` is when Tako recorded it. The gap between them is however long the source took to report.
* `trigger` says what woke the monitor. `push`, meaning new data arrived, is the only value Tako sends today.

<Warning>
  **Verify the signature before you trust the body.** Anyone who learns your channel's URL can post to it. The signature is the only thing that proves a request came from Tako.
</Warning>

## Verify the signature

Tako signs the concatenation of the delivery id, the timestamp, and the raw request body, joined with periods:

```
{webhook-id}.{webhook-timestamp}.{raw body}
```

The key is the base64-decoded part of your channel's `secret` after the `whsec_` prefix. The signature is `v1,` followed by the base64 HMAC-SHA256 digest.

Sign the bytes you received, not a re-serialized object. Tako sends compact JSON with no spaces after separators, and any reformatting breaks the signature.

<CodeGroup>
  ```python Python theme={null}
  import base64
  import hashlib
  import hmac
  import time

  TOLERANCE_SECONDS = 5 * 60


  def verify(secret: str, headers: dict[str, str], raw_body: bytes) -> bool:
      timestamp = int(headers["webhook-timestamp"])
      if abs(time.time() - timestamp) > TOLERANCE_SECONDS:
          return False

      key = base64.b64decode(secret.removeprefix("whsec_"))
      signed = f"{headers['webhook-id']}.{timestamp}.".encode() + raw_body
      expected = "v1," + base64.b64encode(
          hmac.new(key, signed, hashlib.sha256).digest()
      ).decode()
      return hmac.compare_digest(expected, headers["webhook-signature"])
  ```

  ```javascript Node.js theme={null}
  import crypto from "node:crypto";

  const TOLERANCE_SECONDS = 5 * 60;

  export function verify(secret, headers, rawBody) {
    const timestamp = Number(headers["webhook-timestamp"]);
    if (Math.abs(Date.now() / 1000 - timestamp) > TOLERANCE_SECONDS) return false;

    const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
    const signed = Buffer.concat([
      Buffer.from(`${headers["webhook-id"]}.${timestamp}.`),
      rawBody,
    ]);
    const expected =
      "v1," + crypto.createHmac("sha256", key).update(signed).digest("base64");

    const received = Buffer.from(headers["webhook-signature"]);
    return (
      received.length === expected.length &&
      crypto.timingSafeEqual(Buffer.from(expected), received)
    );
  }
  ```
</CodeGroup>

Compare the signatures in constant time, and reject a `webhook-timestamp` far from the current time so an attacker can't replay an old delivery.

## How Tako retries

Tako allows 10 seconds for a response, and doesn't follow redirects.

| Response                                             | What Tako does                       |
| ---------------------------------------------------- | ------------------------------------ |
| `2xx`                                                | Marks the delivery delivered.        |
| `408`, `429`, or any `5xx`                           | Retries.                             |
| No response, such as a timeout or a connection error | Retries.                             |
| Any other status, including a `3xx` redirect         | Fails the delivery without retrying. |

A delivery gets 6 attempts: the first one, then retries after 1 minute, 5 minutes, 15 minutes, 1 hour, and 2 hours 45 minutes. Tako stops retrying a delivery more than 4 hours 30 minutes old, even when attempts remain.

Respond `2xx` as soon as you've stored the delivery, and do the slow work afterwards. A receiver that works for longer than 10 seconds gets retried, and receives the firing again.

After 2 consecutive failed deliveries, Tako pauses the channel and sets its `status_reason` to `delivery_failures`. Monitors that post to a paused channel keep evaluating and recording firings, and those firings reach no one. Fix the destination, then [resume the channel](/api-reference/notification-channels-update), which clears the consecutive-failure count.

To see how each delivery went, call [List a monitor's firings](/api-reference/monitors-firings) and read `deliveries`.

## Payload fields by monitor type

`payload` is a flat object of strings, numbers, booleans, and nulls. The fields depend on the monitor's `type`.

<AccordionGroup>
  <Accordion title="stocks.pct_change">
    | Field            | Type   | Description                                                                      |
    | ---------------- | ------ | -------------------------------------------------------------------------------- |
    | `ticker`         | string | The listing's ticker.                                                            |
    | `exchange`       | string | The exchange's ISO 10383 MIC.                                                    |
    | `window`         | string | The window you set: `1d`, `1w`, or `1m`.                                         |
    | `baseline_date`  | string | The session date of the reference close.                                         |
    | `baseline_close` | number | The reference close the move is measured from.                                   |
    | `price`          | number | The price that met the threshold.                                                |
    | `percent_change` | number | The move from `baseline_close` to `price`, in percent. Negative for a down move. |
  </Accordion>

  <Accordion title="stocks.crosses">
    | Field            | Type         | Description                                                        |
    | ---------------- | ------------ | ------------------------------------------------------------------ |
    | `ticker`         | string       | The listing's ticker.                                              |
    | `exchange`       | string       | The exchange's ISO 10383 MIC.                                      |
    | `currency`       | string, null | The exchange's currency. Null when the exchange has none recorded. |
    | `level`          | number       | The level you set.                                                 |
    | `crossed`        | string       | `above` or `below`, the direction of the crossing.                 |
    | `previous_date`  | string       | The session date of the prior close.                               |
    | `previous_close` | number       | The prior close, on the other side of the level.                   |
    | `price`          | number       | The price that crossed.                                            |
  </Accordion>

  <Accordion title="stocks.new_52w_extreme">
    | Field           | Type         | Description                                                        |
    | --------------- | ------------ | ------------------------------------------------------------------ |
    | `ticker`        | string       | The listing's ticker.                                              |
    | `exchange`      | string       | The exchange's ISO 10383 MIC.                                      |
    | `currency`      | string, null | The exchange's currency. Null when the exchange has none recorded. |
    | `broke`         | string       | `high` or `low`, the end of the range the price passed.            |
    | `extreme_close` | number       | The 52-week extreme the price passed.                              |
    | `extreme_date`  | string       | The session date of that extreme.                                  |
    | `price`         | number       | The price that passed it.                                          |
  </Accordion>

  <Accordion title="crypto.crosses">
    | Field              | Type         | Description                                                                                      |
    | ------------------ | ------------ | ------------------------------------------------------------------------------------------------ |
    | `ticker`           | string, null | The asset's ticker, such as `BTC`. Null when the asset carries none.                             |
    | `name`             | string       | The asset's name.                                                                                |
    | `coinmarketcap_id` | string       | The CoinMarketCap id, which you can pass back as a `coinmarketcap_id`.                           |
    | `level`            | number       | The level in USD you set.                                                                        |
    | `direction`        | string       | `above` or `below`, the direction you set.                                                       |
    | `price`            | number       | The price in USD that reached the level.                                                         |
    | `observed_at`      | string       | When the snapshot was taken, in ISO 8601.                                                        |
    | `grain`            | string       | The sampling cadence the snapshot came from, as an ISO 8601 duration: `PT5M`, `PT30M`, or `P1D`. |
  </Accordion>

  <Accordion title="sports.game_start">
    | Field        | Type         | Description                                                                    |
    | ------------ | ------------ | ------------------------------------------------------------------------------ |
    | `game_id`    | string       | The game's id, as [List sports games](/api-reference/sports-games) returns it. |
    | `league`     | string, null | The league's name.                                                             |
    | `team`       | string, null | The team you watch.                                                            |
    | `opponent`   | string, null | The other team.                                                                |
    | `home_team`  | string, null | The home team.                                                                 |
    | `away_team`  | string, null | The away team.                                                                 |
    | `start_time` | string       | The scheduled start, in ISO 8601.                                              |
  </Accordion>

  <Accordion title="sports.game_result">
    | Field            | Type         | Description                                                                                          |
    | ---------------- | ------------ | ---------------------------------------------------------------------------------------------------- |
    | `game_id`        | string       | The game's id, as [List sports games](/api-reference/sports-games) returns it.                       |
    | `league`         | string, null | The league's name.                                                                                   |
    | `team`           | string, null | The team you watch.                                                                                  |
    | `opponent`       | string, null | The other team.                                                                                      |
    | `home_team`      | string, null | The home team.                                                                                       |
    | `away_team`      | string, null | The away team.                                                                                       |
    | `team_score`     | integer      | The final score for the team you watch.                                                              |
    | `opponent_score` | integer      | The final score for the other team.                                                                  |
    | `outcome`        | string       | `wins` or `loses`, from the watched team's side. On a game-scoped monitor, it reads from the winner. |
    | `margin`         | integer      | The margin of the result, always positive.                                                           |
    | `winner`         | string, null | The winning team's name. Null when the feed carries no name.                                         |
    | `winner_id`      | string, null | The winning team's id, which you can pass back as a `team_id`.                                       |
  </Accordion>

  <Accordion title="sports.score_event">
    | Field            | Type         | Description                                                                    |
    | ---------------- | ------------ | ------------------------------------------------------------------------------ |
    | `game_id`        | string       | The game's id, as [List sports games](/api-reference/sports-games) returns it. |
    | `league`         | string, null | The league's name.                                                             |
    | `team`           | string, null | The team that scored. On a team-scoped monitor, that's the team you watch.     |
    | `opponent`       | string, null | The other team.                                                                |
    | `side`           | string, null | `home` or `away`, the side that scored.                                        |
    | `team_score`     | integer      | The score for `team`, after this score.                                        |
    | `opponent_score` | integer      | The score for the other team.                                                  |
    | `phase`          | string       | `live` or `final`, the state of the game when the score landed.                |
  </Accordion>

  <Accordion title="sports.odds_crosses">
    | Field                 | Type         | Description                                                                                             |
    | --------------------- | ------------ | ------------------------------------------------------------------------------------------------------- |
    | `game_id`             | string       | The game's id, as [List sports games](/api-reference/sports-games) returns it.                          |
    | `league`              | string, null | The league's name.                                                                                      |
    | `home_team`           | string, null | The home team.                                                                                          |
    | `away_team`           | string, null | The away team.                                                                                          |
    | `team`                | string, null | The team you named.                                                                                     |
    | `opponent`            | string, null | The other team.                                                                                         |
    | `market`              | string       | The market you set: `moneyline`, `spread`, or `total`.                                                  |
    | `market_type`         | string       | The feed's market, which can be more specific than `market`. A `moneyline` reads `moneyline` or `3way`. |
    | `side`                | string, null | `over` or `under` for a total. Null for a moneyline or a spread.                                        |
    | `book`                | string       | The sportsbook's display name.                                                                          |
    | `book_slug`           | string, null | The sportsbook's id, such as `draftkings`. Null when the feed carries no id.                            |
    | `level`               | number       | The level you set.                                                                                      |
    | `line`                | number, null | The handicap or total that was quoted. Null for a moneyline, which has no line.                         |
    | `price_american`      | number, null | The quoted price in American odds.                                                                      |
    | `price_decimal`       | number, null | The same price in decimal odds.                                                                         |
    | `open_line`           | number, null | The opening handicap or total. Null for a moneyline.                                                    |
    | `open_price_american` | number, null | The opening price in American odds.                                                                     |
    | `is_live`             | boolean      | True when the quote came from the live feed rather than the prematch one.                               |
  </Accordion>
</AccordionGroup>

<Note>
  The fields a type sends are its contract. Tako can add a field, which your receiver should ignore, and removing one is a breaking change. While monitors is experimental, these shapes can change without a deprecation window.
</Note>

## Slack workflow bodies

A channel whose `kind` is `slack_workflow` receives a flat body of strings instead, because a Slack Workflow Builder trigger rejects nested values:

```json theme={null}
{
  "summary": "AAPL is up 3.2%, past your 3% threshold.",
  "monitor_name": "AAPL big move",
  "monitor_type": "stocks.pct_change",
  "monitor_id": "0d6f1e77-4b9a-4f0e-8a4d-2c9e7b1f3a55",
  "occurred_at": "2026-09-12T20:00:00+00:00",
  "event_id": "9a7c5d31-8e2b-4a66-b0d7-5f4e3c2a1b09",
  "event_type": "monitor.fired"
}
```

This shape carries no `payload`, so a receiver that needs the values that caused the firing needs a `webhook` channel. Tako signs and retries both kinds the same way.

## What Tako accepts as a URL

[Create a notification channel](/api-reference/notification-channels-create) rejects a `url` that:

* Doesn't use `https`.
* Uses a port other than 443 or 8443.
* Embeds credentials, as in `https://user:pass@example.com/hook`.
* Has a host that doesn't resolve.
* Has a host that resolves to a loopback, link-local, multicast, private, reserved, or shared address.

Tako normalizes the URL it stores: it lowercases the host, drops the fragment, and gives a bare host a trailing slash. Read `url` in the response to see what Tako posts to.
