# API reference

Base URL: `https://uselayer.sh`. All responses are JSON.

## Authentication

Every endpoint except `/v0/health` needs a key from your [dashboard](https://uselayer.sh/dashboard):

```
authorization: Bearer lyr_your_key
```

`x-api-key: lyr_your_key` also works. Each key allows **60 requests per minute**.

No key? Agents can pay for each call in USDC on Base instead: see [Pay per call (x402)](https://uselayer.sh/docs/pay-per-call.md).

Every endpoint and its inputs as an OpenAPI 3.1 file, for a coding agent or a client generator: [`/v0/openapi.json`](https://uselayer.sh/v0/openapi.json).

---

## GET /v0/matches

Every live match, soonest event first (by day, then by Kalshi's event time within the day, so 15-minute crypto windows lead their day). Without `from`, a match whose event ended over 12 hours ago is left out, even though both venues still have it open: a game that was never played or is waiting on a result, an election not yet certified. Send `from` with a past day to see those. Use it to discover what Layer can answer. For example, one weekend's NFL matches: `?category=sports&q=nfl&from=<Friday>&to=<Sunday>`, with the dates as `YYYY-MM-DD`. A filter it can't read, such as `from=Oct 5`, returns `400 bad_request`.

| Query | Default | Meaning |
|---|---|---|
| `limit` | `50` | 1–200; a number outside that is clamped, and anything else means 50. |
| `offset` | `0` | Skip this many matches first, to page past `limit`. Send the last answer's `next_offset`. |
| `category` | all | Only matches in that category, e.g. `sports`. A category with no live matches yet returns `count: 0`. |
| `series` | — | Only these Kalshi series, comma-separated, any case, up to 20: e.g. `KXBTC15M,KXETH15M` for the 15-minute Bitcoin and Ethereum windows. Use a match's `kalshi.series`. A series Kalshi has never used returns `400 bad_request`. |
| `from` | — | Only events on or after this day (`YYYY-MM-DD`). Without it, events that ended over 12 hours ago are left out: "ended" is the later of the two venues' `event_time`, and a match with no `event_time` stays. |
| `to` | — | Only events on or before this day. |
| `q` | — | Words that must all appear, ignoring case, somewhere in either venue's event title, question or outcome, or in Kalshi's series ticker or Polymarket's event slug (so league names such as `nfl` work). E.g. `chiefs` or `fed december`. Up to 200 characters; only the first 10 words are used. For crypto, a coin's name finds every market on that coin: `bitcoin`, `ethereum`, `solana`, `ripple` (the tickers `btc`, `eth`, `sol`, `xrp` search the text like any word). |
| `venue` | `polymarket_us` | Which venue the matches are with: `polymarket_us` (Polymarket US, polymarket.us), `polymarket` (polymarket.com), `hyperliquid` (Hyperliquid's outcome markets) or `novig` (Novig's sports game lines). |
| `prices` | `true` | `false` leaves out live prices and the best trade, for a faster answer. See [Live prices](#live-prices) below. |

**Response**

| Field | Meaning |
|---|---|
| `count` | Number of matches returned. |
| `total` | Number of matches for these filters, on every page. |
| `limit`, `offset` | The page size and offset used. A `limit` above 200 is answered with 200. |
| `has_more` | `true` when there are matches after this page. |
| `next_offset` | The `offset` for the next page, or `null` on the last. Page with it until `has_more` is `false`. |
| `hint` | Only when `count` is `0`: why, and what to send instead. E.g. an `offset` past the end, a word no match contains, the filter that emptied the list, or another `venue` that has matches. |
| `matches[].kalshi` | The Kalshi market (see *Market object*). |
| `matches[].polymarket_us` | The Polymarket US market. With `venue=polymarket` this field is `matches[].polymarket` instead, with `venue=hyperliquid` it's `matches[].hyperliquid`, and with `venue=novig` it's `matches[].novig`. |
| `matches[].category` | Layer's category for the event. |
| `matches[].confidence` | 0–1. `1` when a person approved the match. |
| `matches[].basis` | `identical` or `equivalent_with_caveats`. |
| `matches[].caveats` | Which rules differ (see [Overview](https://uselayer.sh/docs/index.md)). |
| `matches[].caveat_notes` | Always `{}` for now. The notes quote the venues' rules, and the venues' terms keep their wording inside Layer, so show the `caveats` codes. The [Python SDK](https://uselayer.sh/docs/sdk.md#rule-flag-notes) writes the notes on your machine from the rules it reads (`m.caveat_notes`). |
| `matches[].tier` | `human_verified` or `auto_verified`. |
| `attribution` | Where the data comes from (see *Using Layer data*). |
| `checked_at` | When this answer was produced. |

### Live prices

Every account gets each match's live prices and best trade, read from both venues when you ask. So do `GET /v0/match` and `POST /v0/match`. Send `prices=false` (in the body for `POST /v0/match`: `"prices": false`) to skip them for a faster answer. A settled match has none. To trade, your code still reads order books and places orders with your own venue keys: the [Python SDK](https://uselayer.sh/docs/sdk.md) does it for you.

**Want live order books?** For NBA, NFL, NHL and NCAAF game winners on Kalshi and Polymarket US, the [Stream](https://uselayer.sh/docs/stream.md) (Beta) pushes each change to the best prices over a WebSocket. For other markets, Layer doesn't stream books yet. If you'd use every venue's full order book and each change to it, over one WebSocket in one format, email [david@uselayer.sh](mailto:david@uselayer.sh?subject=Live%20order%20books) and say what language your bot is written in. Other ideas we're weighing are on [Feature requests](https://uselayer.sh/docs/feature-requests.md).

| Field | Meaning |
|---|---|
| `prices.as_of` | When the prices were read (ISO 8601, UTC). |
| `prices.kalshi`, `prices.polymarket_us` (or `polymarket`, `hyperliquid`, `novig`) | Each venue's best prices: `yes` and `no`, each with a `bid` and an `ask` of `{price, size}`, in dollars and contracts. `null` for a side with no orders, or a venue that didn't answer. |
| `best_trade` | Buying YES on one venue and NO on the other at the best asks, the cheaper way round, after both taker fees: `kalshi` and the other venue's leg (`buy`, `price`, `size`), then `contracts`, `cost`, `fees`, `payout`, `net_profit`, `net_profit_per_contract`, `return_pct`, `profitable`, `expected_payout_at`, `latest_payout_at`, `days_held`, `return_per_day_pct` and `annualized_return_pct`. The same math as `POST /v0/profit`. `null` when a side has no ask. |
| `best_trade.event_passed` | `true` only when the expected payout time has gone. The result is probably known, so prices can show a large gap that isn't a real trade: `profitable` is `false` and the per-day figures are `null`, whatever the prices say. |
| `best_trade.event_started` | `true` only once the event has started and its expected payout time hasn't gone: for a game, from the start until the money is expected back. Prices move within seconds then. |
| `best_trade.stale_prices` | `true` only when the event has started and a venue's prices were over 5 seconds old when Layer read them. Both venues answer price requests from a cache (Kalshi for up to 15 seconds, Polymarket US for up to 30), so during a game one venue's price can be from before a move and the other's from after it, which shows a gap that was never there to trade. `profitable` is then `false`, whatever the prices say; the other figures are still worked out. Ask again for a fresher read, or use the [Stream](https://uselayer.sh/docs/stream.md) for live books. |
| `best_trade.price_age_seconds` | How old each venue's prices were when Layer read them, in seconds, by venue: `{"kalshi": 0, "polymarket_us": 17}`. `0` is straight from the venue. Only for venues that say (Kalshi and Polymarket US). |
| `prices_unavailable` | On the answer, only when a venue didn't answer: the venues whose prices are missing. The matches are still served. |

Hyperliquid matches get `prices.hyperliquid` but `best_trade: null`: Layer doesn't work out Hyperliquid's fees yet. Novig matches get `prices.novig` and a `best_trade` with a `novig` leg. Novig charges takers 0.06 × price × (1 − price) per contract: on a game line only once the game has started, so a trade before the game has no Novig fee, and on a season future always. If Layer can't read the market's fee or whether its game has started, `best_trade` is `null`. Novig's book holds only bids, so the ask on an outcome is 1 minus the best bid on the other outcome; sizes are in $1 contracts, like Kalshi's (one Novig contract pays 1¢).

**Hyperliquid.** `venue=hyperliquid` covers games (winners, soccer results with each team and the draw, a game's totals and spreads), season winners, crypto prices at a set time and IPO dates. There are no player props. If a venue's matches are ever switched off, asking for them here, on `/v0/match` or on `/v0/settlements` answers `403 venue_not_served`. See [How matching works](https://uselayer.sh/docs/matching.md#hyperliquid).

**Novig.** `venue=novig` covers sports only: game lines (moneylines, spreads, totals and soccer's three-way result, each team and the draw) and season futures (champions, conference and division winners, playoff qualifiers), each team its own contract. There are no player props or half-game lines. If Novig's matches are ever switched off, asking for them here, on `/v0/match` or on `/v0/settlements` answers `403 venue_not_served`. See [How matching works](https://uselayer.sh/docs/matching.md#novig).

---

## GET /v0/match

The twin of one market on the other venue.

| Query | Required | Meaning |
|---|---|---|
| `venue` | yes | `kalshi`, `polymarket`, `polymarket_us`, `hyperliquid` or `novig` — the venue of the id you pass. |
| `market_id` | yes | Kalshi: the market ticker. Polymarket: the `conditionId`, market slug, or numeric id. Polymarket US: the market slug; for a market with two named sides, `<slug>:long` or `<slug>:short`. Hyperliquid: the coin of one side of an outcome, so `#14960` is side 0 of outcome 1496. In a URL, send its `#` as `%23`: `market_id=%2314960`. Novig: `<marketId>:<outcomeId>`, or Novig's `outcomeId` alone. |
| `prices` | no | `false` leaves out live prices, for a faster answer (see [Live prices](#live-prices)). |
| `with` | no | For a Kalshi market: which venue to find its twin on, `polymarket_us` (the default), `polymarket`, `hyperliquid` or `novig`. For a Polymarket, Polymarket US, Hyperliquid or Novig market, leave it out: the twin is always on Kalshi. Naming the market's own venue is allowed and changes nothing; naming another one is a `400` (see below). |

**From one Polymarket to the other.** Layer pairs each Polymarket, Hyperliquid and Novig with Kalshi, not with each other, so there's no direct lookup from a polymarket.com market to its Polymarket US twin (or back). It takes two steps, through Kalshi:

```bash
# 1. The polymarket.com market's Kalshi match: matched_market.market_id is the Kalshi ticker
curl -H "Authorization: Bearer $LAYER_API_KEY" "https://uselayer.sh/v0/match?venue=polymarket&market_id=<conditionId>"
# 2. That Kalshi market's twin on Polymarket US
curl -H "Authorization: Bearer $LAYER_API_KEY" "https://uselayer.sh/v0/match?venue=kalshi&market_id=<matched_market.market_id>&with=polymarket_us"
```

If step 1 has no match, there's no Polymarket US twin to find either. Swap the venues to go from Polymarket US to polymarket.com (`with=polymarket` in step 2).

**Match found**

| Field | Meaning |
|---|---|
| `source_market` | The market you asked about. |
| `matched_market` | Its twin on the other venue. |
| `side` | Always `same`: YES on one pays like YES on the other. |
| `tier`, `confidence`, `basis`, `caveats`, `caveat_notes` | As in `/v0/matches`. |
| `match_reason` | Who approved it: `approved by a reviewer` or `auto-approved`. |
| `prices`, `best_trade` | As in `/v0/matches` (see [Live prices](#live-prices)). |
| `settlement` | Only once both markets have settled: how each venue paid out and whether they agreed (see *Settlement object* under `GET /v0/settlements`). A market that was in a live match when it settled is still answered after it closes, with this field. |
| `attribution` | Where the data comes from (see *Using Layer data*). |
| `checked_at` | When this answer was produced. |

**No match** — `matched_market` is `null` and `reason` says why. `source_market` is still the market you asked about, or `null` when Layer doesn't know the id or can't tell which side you mean (`specify_outcome`).

| `reason` | Meaning |
|---|---|
| `not_indexed` | Layer doesn't know this id, or the market has closed without a match that was live when it settled. |
| `no_candidate` | Layer found nothing on the other venue that could be the same bet. |
| `pending_review` | A possible match exists but hasn't been approved yet. |
| `rejected` | A possible match was checked and is not the same bet. |
| `outcome_unmapped` | The events match, but this particular outcome has no twin. |
| `rules_changed_pending_review` | A venue edited its rules since approval; re-checking. |
| `specify_outcome` | This Polymarket market has two named sides; pass `<conditionId>:0` or `<conditionId>:1` (Polymarket US: `<slug>:long` or `<slug>:short`). |

### Market object

| Field | Meaning |
|---|---|
| `venue` | `kalshi`, `polymarket`, `polymarket_us`, `hyperliquid` or `novig`. |
| `market_id` | The id to use with the venue's own API. For Hyperliquid, the side's coin (`#14960`). For Novig, `<marketId>:<outcomeId>`: Novig's market, then the outcome (the side) within it. |
| `group_id` | The venue's event id (Kalshi event ticker, Polymarket event slug). For Hyperliquid, Layer's own id for the outcome, question, game or price time the market is in (`price-BTC-20261103-2000`: BTC at 8:00 PM UTC on Nov 3, 2026). For Novig, `<eventId>#<kind>`: Novig's game and the kind of line (`moneyline`, `spreads` or `totals`). |
| `url` | The market's page on the venue, where it's traded. Link to it when you show the market. For Hyperliquid it's the Outcomes page (`https://app.hyperliquid.xyz/outcomes`), not a page for that market. For Novig it's `https://novig.com/events/<outcomeId>`, which opens novig.com with that game and side in the order slip. |
| `series` | Kalshi: the market's series ticker, one league and one market type, e.g. `KXNFLGAME`. Kalshi's `GET /series/<series>` gives its `fee_multiplier` for `POST /v0/profit`. Polymarket and Polymarket US: the event's series, one league and season with every market type, e.g. `nfl-2026`. Each Polymarket names its own series (Polymarket US calls the same NFL games `nfl-2025`), and the names change by season. Hyperliquid: the competition, e.g. `nba` (a season winner too), or `fomc`, `price-btc`, `ipo-anthropic`. Novig: the league, e.g. `nba` or `champions-league`. Always present for Kalshi; for the others, left out when the venue gives none. |
| `slug`, `yes_token_id` | Polymarket only: the market slug and the CLOB token id for YES — what you trade with. Polymarket US markets have `slug` only (without the `:long` / `:short` side). |

A market object carries ids and links, not the venue's own wording. For a market's title, question, times and settlement sources, read it from the venue with its `market_id`, on your machine.

---

## GET /v0/settlements

How each match settled on both venues, and whether they agreed: Layer's track record, newest first. Only matches that were live in the API when they settled are listed. Use it to check a match after the fact, or to settle your own positions. For example, everything that settled since your last call: `?since=2026-10-01T12:00:00Z` (pass the `checked_at` of your last answer).

To see the record for one kind of market, send `series` or `category`. A Kalshi series is one league and one market type, so `?series=KXNFLGAME` is every NFL game-winner match, and the answer adds `scope_totals`: how many of those settled and how many agreed. For a whole league, every market type, send the Polymarket side's series instead: `?polymarket_series=nfl-2025` (Polymarket US's NFL series).

| Query | Default | Meaning |
|---|---|---|
| `venue` | `polymarket_us` | Which venue: `polymarket_us` (Polymarket US), `polymarket` (polymarket.com), `hyperliquid` or `novig`. |
| `since` | — | Only settlements Layer recorded at or after this day (`YYYY-MM-DD`, from midnight UTC) or time (ISO 8601 with a time zone). |
| `outcome` | all | `agreed`, `disagreed` or `edge_case`. |
| `series` | all | Only matches whose Kalshi market is in this series, e.g. `KXNFLGAME` (the `series` field of the Kalshi *Market object*). Any case works. A Kalshi series with nothing settled yet returns `count: 0`. A name Kalshi doesn't have is a `400`, and so is a Polymarket series (like `nfl-2026`): send that as `polymarket_series`. |
| `polymarket_series` | all | Only matches whose Polymarket market is in this series, e.g. `nfl-2026` (the `series` field of the Polymarket *Market object*). One league and season, every market type. Any case works. Use this venue's own name: `nfl-2026` is polymarket.com's, and Polymarket US's is `nfl-2025`. With `venue=hyperliquid`, send the competition, e.g. `nba`; with `venue=novig`, the league, e.g. `nba`. A name this venue doesn't have is a `400`, and the hint says if another venue has it. |
| `category` | all | Only matches in this category, as in `/v0/matches`: `sports`, `politics`, `economics`, `crypto`, `finance`, `climate`, `tech`, `culture`, `world` or `other`. |
| `limit` | `50` | 1–200; a number outside that is clamped, and anything else means 50. |
| `offset` | `0` | Skip this many settlements first, to page past `limit`. |

**Response**

| Field | Meaning |
|---|---|
| `count` | Number of settlements returned. |
| `totals` | Every settlement of a live match with this venue, whatever the filters: `settled`, `agreed`, `disagreed`, `edge_case`. |
| `scope_totals` | Only when you send `series`, `category` or `polymarket_series`: the same four counts for that kind of market only (all of them, when you send more than one), whatever `since`, `outcome`, `limit` and `offset` are. A settlement whose Kalshi market Layer has no event for can't be placed in a `series` or `category`, and one whose Polymarket market has none can't be placed in a `polymarket_series`. It counts in `totals` but never in that `scope_totals` or narrowed list. |
| `settlements[].kalshi` | The Kalshi market (see *Market object*). |
| `settlements[].polymarket_us` | The Polymarket US market. With `venue=polymarket` this field is `settlements[].polymarket` instead, with `venue=hyperliquid` it's `settlements[].hyperliquid`, and with `venue=novig` it's `settlements[].novig`. |
| `settlements[].settlement` | How it settled (see *Settlement object*). |
| `attribution` | Where the data comes from (see *Using Layer data*). |
| `checked_at` | When this answer was produced. |

### Settlement object

| Field | Meaning |
|---|---|
| `results` | What each venue paid on its side of the match: `yes`, `no` or `void` (refunded, e.g. a cancelled game). Keyed by venue: `{ "kalshi": "yes", "polymarket": "yes" }`, or `polymarket_us`, `hyperliquid` or `novig` in place of `polymarket`. A Hyperliquid side that paid only part of its $1 (half, when Hyperliquid settles an outcome 50-50) is `void`. Novig grades each outcome WIN (`yes`) or LOSS (`no`); a refund (PUSH) or a settlement at Novig's fair market value (FMV, what it pays when it voids a market) is `void`. Layer reads Novig's grades with a read-only Novig key, so a Novig match only settles here once that key is in place. |
| `outcome` | `agreed`: both paid the same. `disagreed`: one paid Yes and the other No, so the match was wrong; Layer pulls it from the API and fixes the cause. `edge_case`: one venue voided the market while the other resolved it, from the venues' different settlement policies (see the [Overview](https://uselayer.sh/docs/index.md)). |
| `explanation` | Why the two differed, in plain English, or `null` when they agreed. |
| `settled_at` | When Layer saw both results (ISO 8601, UTC). Layer checks every 6 hours, so this is up to about 6 hours after the later venue settled. |

```bash
curl -H "Authorization: Bearer $LAYER_API_KEY" "https://uselayer.sh/v0/settlements?venue=polymarket_us&since=2026-10-01"
```

Layer's record on NFL game winners with Polymarket US:

```bash
curl -H "Authorization: Bearer $LAYER_API_KEY" "https://uselayer.sh/v0/settlements?venue=polymarket_us&series=KXNFLGAME&limit=1"
```

```json
{
  "count": 1,
  "totals": { "settled": 4053, "agreed": 4052, "disagreed": 1, "edge_case": 0 },
  "scope_totals": { "settled": 2, "agreed": 2, "disagreed": 0, "edge_case": 0 },
  "settlements": [ { "kalshi": { "series": "KXNFLGAME", "...": "..." }, "polymarket_us": { "...": "..." }, "settlement": { "outcome": "agreed", "...": "..." } } ],
  "attribution": "...",
  "checked_at": "2026-10-02T18:00:00.000Z"
}
```

Layer's record on every NFL market type with polymarket.com (game winners, spreads, totals and player props), by its series name:

```bash
curl -H "Authorization: Bearer $LAYER_API_KEY" "https://uselayer.sh/v0/settlements?venue=polymarket&polymarket_series=nfl-2026&limit=1"
```

```json
{ "count": 1, "totals": { "settled": 5405, "agreed": 5405, "disagreed": 0, "edge_case": 0 }, "scope_totals": { "settled": 294, "agreed": 294, "disagreed": 0, "edge_case": 0 }, "...": "..." }
```

---

## POST /v0/match

Look up many markets in one call: up to 50, from any venue, mixed freely. The whole call counts as **one request** against your key's 60 per minute.

```bash
curl -X POST -H "authorization: Bearer $LAYER_KEY" \
  -H "content-type: application/json" \
  -d '{"markets":[{"venue":"kalshi","market_id":"KXFEDDECISION-26OCT-C25"},{"venue":"polymarket","market_id":"afcq-ben-mau-2026-09-29-ben"}]}' \
  "https://uselayer.sh/v0/match"
```

**Request body**

| Field | Meaning |
|---|---|
| `markets` | 1–50 lookups. |
| `markets[].venue` | `kalshi`, `polymarket`, `polymarket_us`, `hyperliquid` or `novig`, as in `GET /v0/match`. |
| `markets[].with` | Optional, as in `GET /v0/match`: for a Kalshi market, which venue to find its twin on. On a Polymarket, Polymarket US, Hyperliquid or Novig market it can only name that market's own venue; another one is a `400`, since Layer pairs each of them with Kalshi. To go from one Polymarket to the other, send the market without `with`, then send its `matched_market.market_id` as `{"venue":"kalshi","market_id":"…","with":"polymarket_us"}` (or `"polymarket"`) in a second call. |
| `markets[].market_id` | Any id `GET /v0/match` accepts. |
| `prices` | Optional: `false` leaves out live prices, for a faster answer. |

**Response**

| Field | Meaning |
|---|---|
| `count` | Number of results, the same as the number of markets you sent. |
| `results` | One per market, in the order you sent them. Each has the `venue` and `market_id` you sent, plus exactly the fields `GET /v0/match` returns: a match, or `matched_market: null` with a `reason`. |
| `attribution`, `checked_at` | As in `GET /v0/match`, once for the whole answer. |

A market Layer doesn't know comes back as `not_indexed` in its place in `results`; it doesn't fail the call. The call fails with `400 bad_request` if the body is malformed or an entry's `with` names a venue it can't, and `detail` names the entry (for example `markets.3.venue`). It also fails as a whole (`403 venue_not_served` or `503 venue_unavailable`) when a venue it touches is switched off or its matches aren't served.

---

## POST /v0/profit

Is buying one side on each venue profitable once fees are paid? You send the prices; Layer applies each venue's official fee schedule. Buying YES on one venue and NO on the other, for the same matched outcome, pays exactly $1 per contract however it settles. So the trade is profitable when the two prices plus both fees come to less than $1 per contract.

The trade is Kalshi plus one other venue: send `kalshi` and exactly one of `polymarket` or `polymarket_us`. Hyperliquid and Novig aren't priced here yet.

```bash
curl -X POST -H "authorization: Bearer $LAYER_KEY" \
  -H "content-type: application/json" \
  -d '{"contracts":100,"kalshi":{"price":0.42},"polymarket":{"price":0.55,"fee_rate":0.05}}' \
  "https://uselayer.sh/v0/profit"
```

**Request body**

| Field | Default | Meaning |
|---|---|---|
| `contracts` | required | Contracts bought on each venue, a whole number up to 1,000,000. |
| `kalshi.price` | required | What you pay per contract on Kalshi, in dollars (`0.42` is 42¢). Up to 6 decimal places. |
| `kalshi.market_id` | — | Optional. The Kalshi ticker of a live match (`kalshi.market_id` in `/v0/matches`). Layer then fills in the fees and `days_held` you leave out: see *Send the match id instead of the fees* below. |
| `kalshi.role` | `taker` | `taker` if your order fills against the book, `maker` if it rests first. |
| `kalshi.fee_type` | `quadratic` | The series' `fee_type` from Kalshi's API: `quadratic`, `quadratic_with_maker_fees` or `quadratic_with_combo_maker_fees`. Only the last two charge makers. Filled in from `market_id`. |
| `kalshi.fee_multiplier` | `1` | The series' `fee_multiplier` from Kalshi's API, 0–10. Some series use `0.5`, and fee-free ones use `0`. Filled in from `market_id`. |
| `polymarket.price` | required | What you pay per share on Polymarket, in dollars. |
| `polymarket.role` | `taker` | Polymarket charges takers only, so `maker` pays no fee. |
| `polymarket.fee_rate` | — | The market's `feeSchedule.rate` from Polymarket's API. Send this or `category`, or send `kalshi.market_id` and Layer fills it in. |
| `polymarket.category` | — | Uses Polymarket's default rate for the category: `crypto` 0.07; `sports`, `economics`, `culture`, `weather`, `other` 0.05; `finance`, `politics`, `mentions`, `tech` 0.04; `geopolitics` (or `world`) 0. `climate` is accepted as `weather`. |
| `polymarket.exponent` | `1` | The market's `feeSchedule.exponent`, a whole number 1–4. Filled in from `market_id`. |
| `polymarket_us.price` | required | What you pay per contract on Polymarket US, in dollars. Buying NO at X is a YES order at 1 − X there; send X. |
| `polymarket_us.role` | `taker` | `taker` pays the fee; `maker` gets Polymarket US's rebate. |
| `polymarket_us.fee_coefficient` | `0.095` | The market's `feeCoefficient` from Polymarket US's API: its taker rate. Polymarket US's markets report the Tier 1 rate; send your own tier (0.085, 0.075 or 0.0695 from $10K, $50K or $250K of trailing 30-day taker volume) if you trade more. Filled in from `market_id`. |
| `settles_at` | — | Optional. When the money comes back: a date (`2026-10-04`, read as 00:00 UTC) or a time with an offset (`2026-10-04T17:00:00Z`), in the future. A good estimate is the later of the two markets' event times, plus a few hours for the results to be confirmed; read them from each venue (the SDK's `client.matches()` fills them in). A market's close time is the latest it can be: for a game, often days later than you'll really be paid. Or send `kalshi.market_id` and Layer works out `days_held` itself. Adds return per day to the answer. |
| `days_held` | — | Optional. The same thing as a number of days (above 0, up to 3,650; `0.5` is 12 hours). Send `settles_at` or `days_held`, not both. |

`price` is the price of the side you buy on that venue. Pass the YES price on one venue and the NO price on the other.

**Response**

```json
{
  "contracts": 100,
  "kalshi": { "price": 0.42, "role": "taker", "cost": 42, "fee": 1.71, "fee_rate": 0.07, "fee_multiplier": 1, "fee_type": "quadratic" },
  "polymarket": { "price": 0.55, "role": "taker", "cost": 55, "fee": 1.2375, "fee_rate": 0.05, "exponent": 1 },
  "payout": 100,
  "spread": 0.03,
  "gross_profit": 3,
  "fees": 2.9475,
  "net_profit": 0.0525,
  "return_pct": 0.05,
  "profitable": true
}
```

| Field | Meaning |
|---|---|
| `kalshi.cost`, `polymarket.cost` | Price × contracts on each venue. The second leg is under the venue you sent: `polymarket` or `polymarket_us`, with its `fee_coefficient` in place of `fee_rate` and `exponent`. A Polymarket maker shows `fee_rate: 0`; a Polymarket US maker shows the rebate as `fee_coefficient: -0.0125` until 2026-10-19, and `0` from then on (see below). |
| `kalshi.fee`, `polymarket.fee` | Each venue's fee for the order, in dollars. |
| `payout` | $1 × contracts: what the pair pays whichever way it settles. |
| `spread` | Per contract, before fees: $1 − Kalshi price − Polymarket price. |
| `gross_profit` | Payout − both costs, before fees. |
| `fees` | Both fees together. |
| `net_profit` | Payout − both costs − both fees. Negative means you'd lose money. |
| `return_pct` | Net profit as a percentage of everything you pay (costs plus fees). |
| `profitable` | `true` when `net_profit` is above zero. |

**Return per day of capital held**

A 2¢ edge that ties your money up for a month is a worse trade than the same edge paid out on Sunday. Send `settles_at` or `days_held` and the answer adds three fields:

```json
{ "days_held": 4, "return_per_day_pct": 0.0131, "annualized_return_pct": 4.79 }
```

| Field | Meaning |
|---|---|
| `days_held` | Days your money is tied up: `days_held` as you sent it, or from now until `settles_at`, to 2 decimal places. |
| `return_per_day_pct` | `return_pct` ÷ `days_held`, to 4 decimal places. |
| `annualized_return_pct` | `return_per_day_pct` × 365: simple, not compounded. |

Both are worked out from the unrounded return, so compare opportunities by these rather than redoing the sum from the rounded `return_pct`. A losing trade gives negative numbers. For a very short hold the annualized figure gets large fast; treat it as a way to rank trades, not a promise you can repeat the trade every day. The money is only counted as tied up until settlement: the time a venue takes to pay out or to let you withdraw isn't included.

**Send the match id instead of the fees**

Pricing a match from `/v0/matches`? Send its Kalshi ticker as `kalshi.market_id` next to the two prices, and Layer fills in what it knows about the pair:

```bash
curl -X POST -H "authorization: Bearer $LAYER_KEY" \
  -H "content-type: application/json" \
  -d '{"contracts":100,"kalshi":{"price":0.42,"market_id":"KXFEDDECISION-26OCT-C25"},"polymarket":{"price":0.55}}' \
  "https://uselayer.sh/v0/profit"
```

- **Kalshi:** the series' `fee_multiplier` and `fee_type`.
- **Polymarket:** the market's own `fee_rate` and `exponent` (its `feeSchedule`).
- **Polymarket US:** the market's own `fee_coefficient`.
- **`days_held`:** from now until the match's expected payout: the later of the two venues' event times plus 6 hours, never past the later closing time, and at least 1 day.

The Polymarket leg you send picks the match: `polymarket` for the international one, `polymarket_us` for the US one. Anything you send yourself is used as you sent it, so you can still pass `settles_at`, `days_held` or any fee to override Layer's. The answer is the same as sending every value yourself, plus two fields:

```json
{
  "match": {
    "kalshi": "KXFEDDECISION-26OCT-C25",
    "polymarket": "0x5f3c…",
    "expected_payout_at": "2026-10-28T18:00:00.000Z",
    "latest_payout_at": "2026-10-28T18:00:00.000Z"
  },
  "filled_in": ["kalshi.fee_multiplier", "kalshi.fee_type", "polymarket.fee_rate", "polymarket.exponent", "days_held"]
}
```

| Field | Meaning |
|---|---|
| `match` | The pair Layer priced: each market's id, and when the money is expected back and the latest it can be (`null` when neither venue gives a time). |
| `filled_in` | The fields Layer filled in. Anything not listed is what you sent, or the default. |

Layer reads the fees from each venue when you call, so the answer uses the fees in force right then. If the ticker has no match on the Polymarket you sent (live now, or live when it settled), the answer is `404 no_match`, with the reason and how to check the ticker. If a venue doesn't answer when Layer asks for a fee, or Polymarket gives no fee schedule for the market and you sent no `category`, the answer is `503 fees_unavailable`. Try again, or send that fee yourself. If Polymarket's fee can't be read and you sent `category`, Layer uses the category's rate and leaves `polymarket.fee_rate` out of `filled_in`.

**How fees are worked out**

- **Kalshi:** `multiplier × rate × contracts × price × (1 − price)`, rounded up to the next cent. The rate is 0.07 for takers. Makers pay 0.0175, or 0.035 on combo series, but only on series whose `fee_type` includes maker fees. This follows [Kalshi's fee schedule](https://kalshi.com/docs/kalshi-fee-schedule.pdf) and matches its published fee table. Members whose balance is kept to $0.0001 may pay up to 1¢ less per order.
- **Polymarket:** `contracts × fee_rate × (price × (1 − price))^exponent`, rounded to 5 decimal places, takers only ([Polymarket's fees](https://docs.polymarket.com/trading/fees)).
- **Polymarket US:** `fee_coefficient × contracts × price × (1 − price)`, rounded to the nearest cent with ties to even. Since 2026-11-06 at 1 PM ET the taker coefficient is tiered by the taker's trailing 30-day volume: 0.095 under $10,000, 0.085 to $50,000, 0.075 to $250,000 and 0.0695 above. Polymarket US's markets report 0.095, so Layer prices at that unless you send your own `fee_coefficient`. Makers get a rebate of `0.0125 × contracts × price × (1 − price)`, shown as a negative fee, until 2026-10-19. From then on Polymarket US pays a maker rebate only to makers with $10M or more of maker volume the month before, so Layer shows no rebate. This follows [Polymarket US's fee schedule](https://docs.polymarket.us/fees) and matches its published 100-lot table. Its volume rebates, paid weekly, aren't included.

You send the prices, so no venue data is served: `/v0/profit` keeps working when a venue's data is switched off (see *Errors*). With `kalshi.market_id` it reads the match, so it's paused like `/v0/match`.

The answer assumes each order fills in full at the price you send, as a single order. It leaves out deposit and withdrawal costs, rebates, and any price movement while you trade. Fees change: if a venue's schedule differs from this, the venue's schedule wins.

---

## POST /v0/size

How much of a gap can you actually trade? The best prices often have only a few dollars behind them. You send the order book for the side you'd buy on each venue, and the smallest profit per contract you'll accept. Layer walks both books from the best price down and returns the largest size that still clears it after both venues' fees, your average price on each venue, and the slippage against the best price.

The trade is Kalshi plus one other venue, as in `/v0/profit`: send `kalshi` and exactly one of `polymarket` or `polymarket_us`. Hyperliquid and Novig aren't sized here yet.

```bash
curl -X POST -H "authorization: Bearer $LAYER_KEY" \
  -H "content-type: application/json" \
  -d '{"min_edge":0.01,"kalshi":{"asks":[{"price":0.42,"size":100},{"price":0.43,"size":300}]},"polymarket_us":{"asks":[{"price":0.52,"size":150},{"price":0.55,"size":500}]}}' \
  "https://uselayer.sh/v0/size"
```

**Request body**

| Field | Default | Meaning |
|---|---|---|
| `kalshi.asks` | required | The asks for the side you buy on Kalshi: a list of `{"price": 0.42, "size": 100}`, in any order, up to 500 levels. Prices in dollars with up to 6 decimal places; sizes above 0, up to 10,000,000, with up to 6 decimal places. |
| `kalshi.fee_multiplier` | `1` | The series' `fee_multiplier` from Kalshi's API, as in `/v0/profit`. |
| `polymarket.asks` | required | The asks for the side you buy on Polymarket (that outcome token's book). |
| `polymarket.fee_rate`, `polymarket.category`, `polymarket.exponent` | — | As in `/v0/profit`: send `fee_rate` or `category`. |
| `polymarket_us.asks` | required | The asks for the side you buy on Polymarket US. |
| `polymarket_us.fee_coefficient` | `0.095` | As in `/v0/profit`. |
| `min_edge` | `0` | The smallest profit per contract after fees you'll take, in dollars (`0.01` is 1¢). With `0`, every contract that makes anything counts. |
| `max_contracts` | — | Optional cap on the answer, a whole number up to 1,000,000. |
| `settles_at`, `days_held` | — | Optional, as in `/v0/profit`: adds return per day to the answer. |

Send the asks for the side you'd buy: YES on one venue and NO on the other. Where a venue lists only bids, turn them round: a bid of `b` for YES is an ask of `1 − b` for NO, with the same size. Kalshi's order book (`GET /markets/{ticker}/orderbook`) lists YES bids and NO bids, so Kalshi's YES asks are `1 −` each NO bid.

**Response**

```json
{
  "contracts": 100,
  "limited_by": "min_edge",
  "min_edge": 0.01,
  "edge_at_best": 0.019236,
  "edge_at_last": 0.019236,
  "kalshi": {
    "best_price": 0.42, "average_price": 0.42, "slippage": 0, "slippage_cost": 0,
    "cost": 42, "fee": 1.71, "fee_rate": 0.07, "fee_multiplier": 1,
    "fills": [{ "price": 0.42, "contracts": 100, "cost": 42, "fee": 1.71 }]
  },
  "polymarket_us": {
    "best_price": 0.52, "average_price": 0.52, "slippage": 0, "slippage_cost": 0,
    "cost": 52, "fee": 2.37, "fee_coefficient": 0.095,
    "fills": [{ "price": 0.52, "contracts": 100, "cost": 52, "fee": 2.37 }]
  },
  "payout": 100,
  "cost": 94,
  "fees": 4.08,
  "slippage_cost": 0,
  "net_profit": 1.92,
  "net_profit_per_contract": 0.0192,
  "return_pct": 1.96,
  "profitable": true
}
```

Here the next Kalshi level (43¢) would earn only 0.91¢ a contract, below the 1¢ asked for, so the answer stops at 100. Leave `min_edge` out and it takes 50 more at 43¢: 150 contracts, $0.50 of slippage on Kalshi, $2.37 net.

| Field | Meaning |
|---|---|
| `contracts` | The largest whole number of contracts, bought on each venue, where every contract clears `min_edge`. `0` when even the best prices don't. |
| `limited_by` | What stopped it: `min_edge` (the next contract earns too little), `kalshi_book`, `polymarket_book` or `polymarket_us_book` (you sent no more levels), or `max_contracts`. |
| `edge_at_best` | Profit per contract after fees at both best prices. Negative means there's no trade at any size. |
| `edge_at_last` | Profit per contract after fees on the last contract counted; `null` when `contracts` is `0`. |
| `kalshi.fills`, … | Each level you'd fill at: its price, the contracts taken there, their cost and fee. |
| `average_price` | Cost ÷ contracts on that venue; `null` when `contracts` is `0`. |
| `slippage` | Average price − best price, per contract. |
| `slippage_cost` | What the slippage costs in dollars, per venue and in total: cost − best price × contracts. |
| `cost`, `fees`, `payout`, `net_profit`, `return_pct`, `profitable` | As in `/v0/profit`, for `contracts` at these fills. `cost` is both legs before fees. |
| `net_profit_per_contract` | `net_profit` ÷ `contracts`. |

**How the size is worked out**

- Layer pairs the cheapest remaining level on each venue and asks whether a contract there earns at least `min_edge` (and more than 0) after both taker fees. If it does, it takes as many as both levels have; if not, it stops. Books only get worse further down, so the first pair that fails ends the walk.
- That decision uses the edge before rounding. The totals then bill each fill (each level) the way the venue does, with its rounding (see *How fees are worked out* above). So `net_profit` can differ from the edges by a cent or two, mostly from Kalshi rounding each fill up to the cent.
- The answer is in whole contracts, so both legs pay out the same. A fractional level on either venue can be used in part.
- Every order is priced as a taker: walking the book means your order fills against it.

It's a snapshot: the answer is only as good as the books you send, and books move while you trade. It assumes each level fills in full for you, ahead of anyone else. You send the books, so no venue data is served: `/v0/size` keeps working when a venue's data is switched off.

---

## GET /v0/quote

How much of a matched market can you trade right now? Send a Kalshi ticker. Layer reads the full order book on both venues, tries the trade both ways round (Kalshi YES with Polymarket NO, and the reverse), and returns the better one: the most contracts that still clear `min_edge` after both venues' taker fees. It's [`POST /v0/size`](#post-v0-size) without having to fetch and send the books. Kalshi with Polymarket US by default, or polymarket.com with `venue=polymarket`.

```bash
curl -H "Authorization: Bearer $LAYER_API_KEY" "https://uselayer.sh/v0/quote?market_id=<kalshi.market_id>&min_edge=0.01"
```

**Query parameters**

| Param | Required | Meaning |
|---|---|---|
| `market_id` | yes | The Kalshi ticker of a live match: `kalshi.market_id` in `GET /v0/matches?venue=polymarket_us`. |
| `venue` | no | `polymarket_us` (default) or `polymarket` (polymarket.com). The answer's second leg, and its keys in `match` and `books_as_of`, use this name. |
| `min_edge` | no | The smallest profit per contract after fees you'll take, in dollars. `0.01` is 1¢. Default: anything above 0. |
| `max_contracts` | no | Caps the answer, a whole number from 1 to 1,000,000. |

**Response**

The fields of [`POST /v0/size`](#post-v0-size), without the price-by-price `fills`, plus:

| Field | Meaning |
|---|---|
| `match` | The two market ids, `basis`, `caveats`, `expected_payout_at` and `latest_payout_at`. |
| `kalshi.market_id`, `kalshi.side`, `polymarket_us.market_id`, `polymarket_us.side` | Which market and which side (`yes` or `no`) to buy on each venue. |
| `limited_by` | What stopped the size: `min_edge`, `max_contracts`, `kalshi_book`, `polymarket_us_book`, `no_orders` when neither way round has an ask on both venues, or `event_passed` when the expected payout time has gone and the result is probably known. With `no_orders` or `event_passed`, `kalshi`, `polymarket_us` and `edge_at_best` are `null`. |
| `books_as_of` | When each venue produced the book Layer read. |
| `event_started`, `stale_prices` | As on a match's `best_trade`: `event_started` is `true` once the event has started, and `stale_prices` is `true` when it has and a book was over 5 seconds old (see `books_as_of`). `profitable` is then `false`; the sizes and prices are still worked out. Ask again for a fresher read. |

`contracts: 0` is a normal answer: there's nothing to trade at your `min_edge` right now.

**Errors:** `404 no_match`, `422 match_settled`, `422 market_not_open` (a market is closed or halted), `503 books_unavailable` (a venue didn't answer), `503 fees_unavailable` (polymarket.com gave no fee schedule), and the same `503 venue_unavailable` and `403 venue_not_served` as `/v0/match`.

It's a snapshot and doesn't promise a fill: books move while you trade, and if only one order fills you're left holding one side. It's priced as a taker on both venues.

## GET /v0/sports/spreads

Beta. The spread on every NBA, NFL, NHL and college football game that's live or starts within the next 24 hours, matched between Kalshi and Polymarket US. Only game winners matched as `identical` are included. Each game is priced the way [`GET /v0/quote`](#get-v0-quote) prices a match: Layer reads both venues' books, tries both teams both ways round (Kalshi YES with Polymarket US NO, and the reverse), and keeps the trade closest to a profit after both venues' taker fees. Games are sorted by `edge_cents`, highest first, so the list isn't empty when nothing is profitable.

```bash
curl -H "Authorization: Bearer $LAYER_API_KEY" "https://uselayer.sh/v0/sports/spreads?league=nba"
```

**Query parameters**

| Param | Required | Meaning |
|---|---|---|
| `league` | no | `nba`, `nfl`, `nhl` or `ncaaf`. Leave it out for all four. |

**Response**

```json
{
  "as_of": "2026-11-10T22:00:00.123Z",
  "games": [{
    "event_id": "KXNBAGAME-26NOV10MIAUTA",
    "market_id": null,
    "league": "nba",
    "title": "Miami vs Utah — MIA vs UTA (Nov 10)",
    "question": "Miami vs Utah: Miami wins",
    "starts_at": "2026-11-11T02:00:00.000Z",
    "status": "upcoming",
    "kalshi": { "ticker": "KXNBAGAME-26NOV10MIAUTA-MIA", "yes_bid": 0.74, "yes_ask": 0.75, "ask_size": 25910.93, "as_of": "2026-11-10T21:59:58.000Z" },
    "polymarket_us": { "slug": "aec-nba-mia-uta-2026-11-10:long", "yes_bid": 0.75, "yes_ask": 0.76, "ask_size": 354725.36, "as_of": "2026-11-10T21:59:41.000Z" },
    "direction": "kalshi_yes_pmus_no",
    "edge_cents": -3.09,
    "arbitrage": false,
    "contracts": 0,
    "net_dollars": 0
  }]
}
```

| Field | Meaning |
|---|---|
| `event_id` | The game: its Layer event id, or the Kalshi event ticker while it has none. |
| `market_id` | The Layer market the row prices (one team's win), or `null` while it has none. |
| `question` | The question both venues' prices are for. Each game has one per team; the row shows the one with the better trade. |
| `starts_at`, `status` | When the game starts, and `live` once it has, `upcoming` before. |
| `kalshi.ticker`, `polymarket_us.slug` | The market on each venue. The Polymarket US slug ends in `:long` or `:short`: which side of its market is this row's YES. |
| `yes_bid`, `yes_ask`, `ask_size` | Each venue's best prices and the contracts offered at the ask, for YES on `question`. Only for accounts that see live prices, as in [`GET /v0/matches`](#live-prices). |
| `as_of` | When each venue produced the book Layer read. |
| `direction` | The better way round: `kalshi_yes_pmus_no` buys YES on Kalshi and NO on Polymarket US; `pmus_yes_kalshi_no` the reverse. |
| `edge_cents` | Profit per contract at both best asks, after both taker fees, in cents. Below 0 means the trade loses money. |
| `arbitrage` | `true` when the trade makes money after fees on at least one whole contract. |
| `contracts`, `net_dollars` | How many contracts clear a positive edge at the books' depth, and the net profit on them in dollars, as `POST /v0/size` works them out. |

Books are read at most every 5 seconds per game, so `as_of` can be up to 5 seconds old. Spreads that clear fees often close quickly, so a profitable row may be gone by the time you read it.

**Errors:** `400` for an unknown `league`, `503 books_unavailable` (a venue didn't answer), and the same `503 venue_unavailable` and `403 venue_not_served` as `/v0/match`.

It's a snapshot and doesn't promise a fill, as with `/v0/quote`.

## GET /v0/sports/lead

Beta. Over the last `days` days Layer has processed: for Kalshi and Polymarket US game winners in the NBA, NFL, NHL and college football, which venue moved first, how often the other followed, and how long it took, plus how often spreads that cleared both venues' fees appeared and how long they stayed open. Computed each night from Layer's recordings of both venues' order books.

```bash
curl -H "Authorization: Bearer $LAYER_API_KEY" "https://uselayer.sh/v0/sports/lead?league=nba&days=7"
```

| Param | Required | Meaning |
|---|---|---|
| `league` | no | `nba`, `nfl`, `nhl` or `ncaaf`. Leave it out for all four. |
| `days` | no | 1 to 30, default 7, ending on the last day processed. A day is the games that started on it, US Eastern. |

Fields and definitions: [Sports spreads and lag](https://uselayer.sh/docs/sports.md#get-v0-sports-lead). **Errors:** `400` for an unknown `league` or a `days` outside 1 to 30.

## GET /v0/sports/events

Beta. One day's recorded spreads and moves, one by one with their times, to line up against your own bot's logs.

```bash
curl -H "Authorization: Bearer $LAYER_API_KEY" "https://uselayer.sh/v0/sports/events?league=nba&date=2026-11-09"
```

| Param | Required | Meaning |
|---|---|---|
| `league` | no | `nba`, `nfl`, `nhl` or `ncaaf`. Leave it out for all four. |
| `date` | yes | `YYYY-MM-DD`: games that started that day, US Eastern. |

`status` says whether Layer has processed that day (`done`); before then the lists are empty. Fields: [Sports spreads and lag](https://uselayer.sh/docs/sports.md#get-v0-sports-events). **Errors:** `400` for an unknown `league` or a missing or malformed `date`.

---

## Webhooks

Instead of polling, register an https URL and Layer POSTs to it when a match changes:

| Event | When |
|---|---|
| `match.live` | A match goes live in the API (it now shows in `/v0/matches`). |
| `match.pulled` | A live match comes out of the API. `data.reason` says why: `rules_changed` (a venue edited its rules; Layer re-checks it and sends `match.live` again if it's still the same bet), `settled_differently` (the venues paid differently on one line, so the whole game is re-checked) or `rejected` (it wasn't the same bet). |
| `match.settled` | A match that was live has settled on both venues. `data.settlement` says how (see *Settlement object*). |

Each event is one line of a game, like one row of `/v0/matches`. Events go out within about a minute of Layer seeing the change. Layer looks for new matches every hour and for settlements every 6 hours, so that's how fresh they are. Crypto windows are faster: Layer checks for them every minute, and a new 15-minute window's `match.live` usually goes out about 30 seconds after the window opens (see [Crypto windows](https://uselayer.sh/docs/matching.md#crypto-windows)).

### POST /v0/webhooks

Register an endpoint. You can have up to 5.

| Field | Default | Meaning |
|---|---|---|
| `url` | required | Where to send events. Must be `https` and reachable from the internet. |
| `events` | all three | Which events to send, e.g. `["match.live", "match.settled"]`. |
| `venues` | all | Matches with which venues: any of `"polymarket"`, `"polymarket_us"`, `"hyperliquid"` and `"novig"`. An endpoint registered before a venue was added, with `venues` left out, keeps the venues there were then (before Hyperliquid, the two Polymarkets only; before Novig, no Novig): register it again to add the new ones. |
| `description` | — | A note for yourself, up to 200 characters. |

```bash
curl -X POST https://uselayer.sh/v0/webhooks \
  -H "Authorization: Bearer $LAYER_API_KEY" -H "content-type: application/json" \
  -d '{"url":"https://example.com/layer-webhook","venues":["polymarket_us"]}'
```

```json
{
  "id": "whe_3f2a9c1d0b7e4a65",
  "url": "https://example.com/layer-webhook",
  "events": ["match.live", "match.pulled", "match.settled"],
  "venues": ["polymarket_us"],
  "description": null,
  "created_at": "2026-10-01T18:00:00.000Z",
  "secret": "whsec_…",
  "next": "POST /v0/webhooks/whe_3f2a9c1d0b7e4a65/test sends a signed test event to it now."
}
```

**Save `secret` now.** It's shown only in this answer, and you need it to check that a request came from Layer. To change it, delete the endpoint and register it again. An endpoint gets only events that happen after you register it.

### GET /v0/webhooks

Your endpoints, each with how its deliveries are going: `delivered`, `pending` (waiting for a retry) and `failed` (gave up) counts, and `last_attempt` (`at`, `status`, `error`, `ok`), as `{ "endpoints": [ … ] }`.

### GET /v0/webhooks/{id}

One endpoint and its last 20 deliveries, newest first: `event_id`, `type`, `test`, `status` (`pending`, `delivered` or `failed`), `attempts`, `last_attempt_at`, `last_status` (your server's HTTP status, or `null` if it didn't answer), `last_error` and `next_attempt_at`.

### DELETE /v0/webhooks/{id}

Stops sending to it. Deliveries still waiting for a retry are dropped. Answers `{ "deleted": true, "id": "…" }`.

### POST /v0/webhooks/{id}/test

Sends one signed test event now and answers with how your server replied: `delivered`, `status`, `error`, `ms` and `sent` (the exact payload). With no body it sends `webhook.test`. With `{"type":"match.live"}` (or `match.pulled`, `match.settled`) it sends a real recent example of that event from your endpoint's venues, so you can test your handler on the real shape. Test payloads have `"test": true` and aren't retried.

```bash
curl -X POST https://uselayer.sh/v0/webhooks/whe_3f2a9c1d0b7e4a65/test \
  -H "Authorization: Bearer $LAYER_API_KEY" -d '{"type":"match.settled"}'
```

### Payload

```json
{
  "id": "evt_1042",
  "type": "match.live",
  "timestamp": "2026-10-01T18:30:12.000Z",
  "data": {
    "kalshi": { "venue": "kalshi", "market_id": "KXNFLGAME-26OCT04DALHOU-DAL", "…": "…" },
    "polymarket_us": { "venue": "polymarket_us", "market_id": "nfl-dal-hou-2026-10-04:long", "…": "…" },
    "category": "sports",
    "tier": "auto_verified",
    "confidence": 0.97,
    "basis": "identical",
    "caveats": [],
    "caveat_notes": {}
  }
}
```

| Field | Meaning |
|---|---|
| `id` | The event's id. Every retry of it has the same `id`, so use it to ignore duplicates. |
| `type` | `match.live`, `match.pulled`, `match.settled`, or `webhook.test`. |
| `timestamp` | When Layer saw the change (ISO 8601, UTC). Events can arrive out of order; use this to order them. |
| `test` | `true` on events sent by the test endpoint; absent otherwise. |
| `data` | The match, in the same shape as a `/v0/matches` row: `kalshi` and `polymarket` (or `polymarket_us`, `hyperliquid` or `novig`) market objects (see *Market object*), `category`, `tier`, `confidence`, `basis`, `caveats`, `caveat_notes`. `match.pulled` adds `reason`; `match.settled` adds `settlement`. A `webhook.test` event carries `{ "message": "…" }` instead. |

### Checking the signature

Layer signs every request the [Standard Webhooks](https://www.standardwebhooks.com) way, so its libraries verify it (`npm install standardwebhooks`, `pip install standardwebhooks`). Each request has three headers:

| Header | Meaning |
|---|---|
| `webhook-id` | The event's `id`. |
| `webhook-timestamp` | When this attempt was sent, in Unix seconds. Refuse requests more than 5 minutes old. |
| `webhook-signature` | `v1,` then the base64 HMAC-SHA256 of `<webhook-id>.<webhook-timestamp>.<raw body>`, keyed with your secret (the part after `whsec_`, base64-decoded). |

```js
import { Webhook } from "standardwebhooks";

const wh = new Webhook(process.env.LAYER_WEBHOOK_SECRET); // whsec_…
// In your handler, with the raw request body as a string (not re-serialized JSON):
const event = wh.verify(rawBody, {
  "webhook-id": req.headers["webhook-id"],
  "webhook-timestamp": req.headers["webhook-timestamp"],
  "webhook-signature": req.headers["webhook-signature"],
}); // throws if the signature or timestamp is wrong
```

### Answering and retries

Answer with any `2xx` within 10 seconds, and do slow work after you answer. Anything else (another status, a redirect, a timeout, or no answer) is retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours and 10 hours, then Layer gives up on that event. That's 8 attempts over about 28 hours, and `GET /v0/webhooks/{id}` shows each one.

Events aren't sent while a venue is switched off or its matches aren't served (`venue_unavailable`, `venue_not_served` below). They also aren't sent to an account with no active API key.

---

## GET /v0/health

No key needed. Is the API up and can it reach its database?

```json
{
  "ok": true,
  "database": { "reachable": true, "ms": 62 },
  "venues": {
    "kalshi": { "open_events": 12587, "open_markets": 125458, "last_pull": "…" },
    "polymarket": { "open_events": 42935, "open_markets": 364647, "last_pull": "…" },
    "polymarket_us": { "open_events": 4032, "open_markets": 65561, "last_pull": "…" },
    "hyperliquid": { "open_events": 73, "open_markets": 167, "last_pull": "…" },
    "novig": { "open_events": 1154, "open_markets": 18405, "last_pull": "…" }
  },
  "live_market_pairs": 14,
  "live_market_pairs_by_venue": { "polymarket": 14, "polymarket_us": 9 },
  "checked_at": "…"
}
```

`venues` lists every venue Layer pulls, Hyperliquid's outcome markets and Novig's game lines included (for Novig, `open_events` counts one per game and kind of line). `live_market_pairs` counts live matches with polymarket.com; `live_market_pairs_by_venue` counts them for each Polymarket, and doesn't count Hyperliquid or Novig yet.

---

## Errors

| Status | `error` | When |
|---|---|---|
| 400 | `bad_request` | Missing or invalid query parameters or body; `detail` says which field. |
| 401 | `unauthorized` | No key, or the key is unknown or revoked. `detail` says which, and `hint` where to get a key and how to send it. |
| 403 | `venue_not_served` | Matches with that venue (`venue`) aren't available right now, such as Hyperliquid's or Novig's while they're switched off. |
| 404 | `no_match` | `POST /v0/profit`: the `kalshi.market_id` you sent has no live match on the Polymarket you sent. `detail` gives the reason, `hint` how to check it. |
| 404 | `not_found` | `/v0/webhooks/{id}`: no endpoint with that id on your account. |
| 409 | `too_many_endpoints` | `POST /v0/webhooks`: you already have 5 endpoints. Delete one first. |
| 409 | `no_example` | `POST /v0/webhooks/{id}/test`: there's no event of that type on the endpoint's venues to send as an example yet. |
| 429 | `rate_limited` | Over 60 requests/minute. Wait `retry-after` seconds. |
| 503 | `fees_unavailable` | `POST /v0/profit` with `kalshi.market_id`: a venue didn't answer when Layer asked for a market's fee. Try again, or send that fee yourself. |
| 503 | `venue_unavailable` | Data from a venue is temporarily switched off (`venues` lists which), so matches are paused. |
| 503 | `database_not_configured` / `database_unreachable` | Layer can't reach its database. |

Every 400 says what's wrong and how to fix it:

```json
{
  "error": "bad_request",
  "detail": "body: send settles_at or days_held, not both",
  "hint": "Send one or the other: \"settles_at\": \"2026-10-14\" if you know when the market settles, or \"days_held\": 4 if you know how many days your money is tied up.",
  "docs": "https://uselayer.sh/docs/api"
}
```

| Field | Meaning |
|---|---|
| `detail` | What's wrong, field by field (`kalshi.price: …`), separated by `;` when there's more than one problem. Stable enough to match on. |
| `hint` | How to fix it, in plain English with a working example. Show it to whoever wrote the call. Its wording can change. |
| `docs` | This page. |

---

## Using Layer data

Every `/v0/matches`, `/v0/match` and `/v0/settlements` answer includes an `attribution` line. Layer serves which markets are the same bet and how matches settled. Live prices, order books and orders go straight between your code and each venue, under your own account with that venue, and trading happens on each venue. When you show a market to your users, credit the venue and link to its `url`. Use Layer data inside your own product; don't republish it as a standalone feed or aggregator. The full terms are at [/terms](https://uselayer.sh/terms).
