# Webhooks: line move, steam, market open, settle

> Independent. Not affiliated with, endorsed by or sponsored by Novig.

Canonical: https://novigapi.com/docs/endpoints/webhooks
Facts checked: 2026-11-12

novigapi.com webhooks POST a signed JSON event when a Novig line moves past your threshold, steam hits, a market opens or a market settles. Production and Scale plans only, up to 5 endpoints. Events are queued every minute from the collector's records. Novig's own lifecycle channel needs an account, checked 12 Nov 2026. (Source: [Novig docs: Lifecycle channel](https://docs.novig.com/api/streaming/lifecycle.md))

## What it returns

Webhooks are for alerts you do not want to poll for. `POST /api/v1/webhooks` with a URL and the events you want; the answer holds a `whsec_` secret, shown once. We POST a JSON body signed with that secret. `GET /api/v1/webhooks` lists your endpoints, `DELETE /api/v1/webhooks/{id}` removes one and `POST /api/v1/webhooks/{id}/test` queues a test delivery.

Event types

| Type | Fires when |
| --- | --- |
| line_move | The collector records a move in an outcome's price of at least `filters.min_delta_pp` percentage points (default 2). |
| steam | An outcome's fair price moves at least 4 percentage points within 2 minutes with traded volume behind it (a `steam` row in [line moves](https://novigapi.com/docs/endpoints/line-moves)). |
| market_open | A new market appears in Novig's catalog for your filters. |
| settle | A market settles, with the winning outcome. |


## Parameters

Query and path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| url | string (body) | Yes | Public HTTPS endpoint (port 443 or 8443, no credentials) that answers 2xx within 5 seconds. |
| events | string[] (body) | Yes | Any of `line_move`, `steam`, `market_open`, `settle`. |
| filters.league | string (body) | No | Only events in this league. |
| filters.event_id | string (body) | No | Only events for this game. |
| filters.market_id | string (body) | No | Only events for this market. |
| filters.player | string (body) | No | Only events for this player slug. |
| filters.min_delta_pp | number (body) | No | Line-move threshold in percentage points, above 0 and at most 100. Default 2. |


## Request

```bash
curl -s -X POST "https://novigapi.com/api/v1/webhooks" \
  -H "Authorization: Bearer nv_..." \
  -H "Content-Type: application/json" \
  -d '{"url":"'"$WEBHOOK_URL"'","events":["line_move","market_open"],"filters":{"league":"NBA","min_delta_pp":3}}'
```

## Response

The body of a `market_open` delivery for a real market: Cooper Flagg 11.5 rebounds plus assists, as listed on 12 Nov 2026. The `first_seen` time is illustrative.

```json
{
  "id": "mo_01a1d0c2-91db-7c31-b15f-b328fa4be683",
  "type": "market_open",
  "created_at": "2026-11-12T17:23:56.000Z",
  "data": {
    "market_id": "01a1d0c2-91db-7c31-b15f-b328fa4be683",
    "event_id": "01a1cac7-5c83-7e41-ba84-afe1af0c1ca6",
    "league": "NBA",
    "market_type": "REBOUNDS_ASSISTS",
    "description": "Cooper Flagg 11.5 REBOUNDS_ASSISTS",
    "strike": 11.5,
    "player": "cooper-flagg",
    "starts_at": "2026-11-13T01:05:00.000Z",
    "first_seen": "2026-11-12T17:20:41Z"
  }
}
```

## Fields

Response fields

| Field | Type | Description |
| --- | --- | --- |
| id | string | Unique event id (also in the `X-Webhook-Id` header). Deliveries can repeat; dedupe on this. |
| type | string | `line_move`, `steam`, `market_open`, `settle`, or `test` from the test route (also in `X-Webhook-Event`). |
| created_at | string (ISO 8601) | When the event was queued. |
| data | object | `market_open`: market_id, event_id, league, market_type, description, strike, player, starts_at, first_seen. `line_move` and `steam`: market_id, outcome_id, event_id, league, player, description, at, from_price, to_price, delta_pp, kind. `settle`: market_id, event_id, league, player, description, winning_outcome_id, close_price, settled_at. |
| X-Webhook-Signature (header) | string | `t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>">` keyed with your endpoint secret. `X-Webhook-Timestamp` repeats `t`. |


## Credit cost

Webhook calls and deliveries cost no credits. Webhooks are included on Production (5 endpoints) and Scale (5 endpoints); Free and Developer get 403 `UPGRADE_REQUIRED` when registering one.

## Python

```python
# Flask receiver that checks the signature
import hashlib, hmac, os, time
from flask import Flask, request, abort

SECRET = os.environ["NOVIGAPI_WEBHOOK_SECRET"].encode()  # the whsec_ secret from registration
app = Flask(__name__)

@app.post("/novig")
def novig():
    parts = dict(p.split("=", 1) for p in request.headers["X-Webhook-Signature"].split(","))
    signed = parts["t"].encode() + b"." + request.get_data()
    expected = hmac.new(SECRET, signed, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, parts["v1"]) or abs(time.time() - int(parts["t"])) > 300:
        abort(401)
    event = request.get_json()
    print(event["type"], event["data"]["description"])
    return "", 204
```

## JavaScript

```javascript
// Express receiver: verify against the raw body, not parsed JSON
import crypto from "node:crypto";
import express from "express";

const app = express();
app.post("/novig", express.raw({ type: "application/json" }), (req, res) => {
  const parts = Object.fromEntries(req.get("X-Webhook-Signature").split(",").map((p) => p.split("=")));
  const expected = crypto.createHmac("sha256", process.env.NOVIGAPI_WEBHOOK_SECRET).update(parts.t + "." + req.body).digest("hex");
  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
  if (!fresh || !crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1))) return res.sendStatus(401);
  const event = JSON.parse(req.body);
  console.log(event.type, event.data.description);
  res.sendStatus(204);
});
app.listen(3000);
```

## Notes and edge cases

- Answer 2xx within 5 seconds and do work afterwards. A failed delivery is retried after 30 s, 2 min, 10 min, 30 min, 2 h and 6 h (7 attempts in all), then dropped. An endpoint that fails 20 deliveries in a row is disabled.
- Reject signatures more than 5 minutes off to stop replays.
- Redirects are not followed, and a host that resolves to a private address is refused at send time.
- Events are queued from the collector's records by a job that runs every minute, so a delivery usually lands within a minute or two of the change on Novig.

## Sources

- [Novig docs: Lifecycle channel](https://docs.novig.com/api/streaming/lifecycle.md)
- [Novig docs: Event lifecycle](https://docs.novig.com/api/concepts/event-lifecycle.md)
- [Novig API docs](https://docs.novig.com)

---

Get a free API key: https://novigapi.com/sign-up . Docs: https://novigapi.com/docs . MCP: https://novigapi.com/api/mcp . Operated by Stunning Couches LLC. Independent. Not affiliated with, endorsed by or sponsored by Novig.
