API reference#
Base URL: https://uselayer.sh. All responses are JSON.
Authentication#
Every endpoint except /v0/health needs a key from your dashboard:
authorization: Bearer lyr_your_keyx-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).
Every endpoint and its inputs as an OpenAPI 3.1 file, for a coding agent or a client generator: /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 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). |
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 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 does it for you.
Want live order books? For NBA, NFL, NHL and NCAAF game winners on Kalshi and Polymarket US, the Stream (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 and say what language your bot is written in. Other ideas we're weighing are on Feature requests.
| 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 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.
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.
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). |
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:
# 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). |
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). |
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. |
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:
curl -H "Authorization: Bearer $LAYER_API_KEY" "https://uselayer.sh/v0/settlements?venue=polymarket_us&series=KXNFLGAME&limit=1"{
"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:
curl -H "Authorization: Bearer $LAYER_API_KEY" "https://uselayer.sh/v0/settlements?venue=polymarket&polymarket_series=nfl-2026&limit=1"{ "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.
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.
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
{
"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:
{ "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:
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_multiplierandfee_type. - Polymarket: the market's own
fee_rateandexponent(itsfeeSchedule). - 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:
{
"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 whosefee_typeincludes maker fees. This follows Kalshi's fee schedule 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). - 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 ownfee_coefficient. Makers get a rebate of0.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 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.
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
{
"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_profitcan 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 without having to fetch and send the books. Kalshi with Polymarket US by default, or polymarket.com with venue=polymarket.
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, 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 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.
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
{
"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. |
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.
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. 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.
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. 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).
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. |
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"]}'{
"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.
curl -X POST https://uselayer.sh/v0/webhooks/whe_3f2a9c1d0b7e4a65/test \
-H "Authorization: Bearer $LAYER_API_KEY" -d '{"type":"match.settled"}'Payload#
{
"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 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). |
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 wrongAnswering 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?
{
"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:
{
"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.