Changelog
Every change to the operator contract is listed here, newest first, with its date.
Additive changes (a new endpoint, a new optional field, a new response field, a new event type, a new error code) arrive under /v1 without notice and are listed here. A breaking change is never published under /v1. It opens beside it as /v2.
The entry for each deprecated item names the item, its replacement and the Sunset date when one is set. The responses of a deprecated endpoint carry the Deprecation, Sunset and Link headers, and Link points to this page. We announce an item at least 90 days before the release that removes it. /v1 stays available for at least 12 months after /v2 is generally available, and we announce the Sunset date at least 180 days in advance.
2026-11-09
An open quote and three new risk refusals. All of them are additive and stay under /v1. Nothing is deprecated.
GET /v1/position-quote. The new endpoint tells you what an open of one stake would get now, before you send it. Pass market_slug, outcome_id and stake. The response carries shares, price, payout and price_after. price is the execution price: the average price that the stake pays, the price the position opens at and the number that max_price bounds. Set max_price from it, not from the price in the Market Feed, which is the price before your stake. The quote moves no money and holds no price. It is refused with the error that an open of the same stake would get, except the user and balance checks, which only the open makes. It takes the same credentials as POST /v1/positions and runs on the default rate limit tier.
Risk rules on a market’s pool. We can now set three rules on the pool of a market. Each one is off by default, and we tell you at least 7 days before a rule is turned on for the first time. Each one refuses an open with a 409, applies nothing and never refuses a close:
- A price impact cap refuses an open that would move the price of its outcome by more than the cap in one trade. The new error code is
price_impact_exceeded.details.max_stakeis the largest stake that fits at the current price, anddetails.capis the cap. - A payout cap per user refuses an open that would take what one user is paid on one outcome above the cap. It returns
stake_limit_exceededwith the newlimit_typevaluemax_payout_per_user. Herelimitis a payout andremainingis the stake that still fits. - A price spike hold pauses your pool on a market whose price moved more than the rule allows within 60 seconds. Opens then return
market_suspendedwithdetails.reasonset toprice_spike. A hold of your own pool that a risk limit started now names its reason too,utilization. A suspension of the market itself keepsdetailsatnull.
limit_type and reason are open sets: new values can arrive without a new API version. The Error codes page lists the values and what to do with one that you do not know.
2026-11-07
An optional site on positions, orders and widget sessions. If you run several sites or brands on one account, name the site in the new optional site field of POST /v1/positions, POST /v1/orders and POST /v1/widget/sessions: 1 to 32 characters from a-z, 0-9 and -. Every position and limit order that a widget session makes takes the site of the session, and a site sent in such a request must match it. The reports activity_summary, position_transaction_detail and settlement_report take a site filter. A request without site behaves as before. The change is additive and stays under /v1.
2026-11-06
Test mode and live mode, side by side. Every account now has a test mode and a live mode from the day it is created, and both work at the same time on one address. The key decides the mode: pmk_test_ keys reach test mode and pmk_live_ keys reach live mode. There is no separate sandbox address any more. Test data is fully separate from live data, so a test key gets 404 not_found for a live resource and a live key gets the same for a test one. Test mode and live mode explains the model.
Your existing pmk_sandbox_ keys keep working. We read them as test keys, and you do not need to change them. New test keys start with pmk_test_.
Approving live access no longer ends test mode. When our team approves it, your first live key is issued and your test keys keep working, so you can go on testing after you go live. A live key asked for before the approval returns the new error code live_access_not_approved (HTTP 409). The Go-live checklist lists what we check first.
Every webhook body carries a new field, livemode: true for a live event and false for a test event. Each mode has its own webhook address and its own secret, and test events go only to the test address. This is an additive change: if your receiver ignores unknown fields, it keeps working. We still recommend that you check livemode before you act on an event, as the Webhook guide shows.
You can delete all your test data from the settings screen of the tenant portal, in test mode. Test mode is then empty again, while your test keys, your test webhook settings and your account settings stay. Live data is never touched.
A new page, Test and live mode, puts the whole model in one place: what each mode keeps for itself, what the account shares, and how going live works.
2026-11-04
Six changes that shipped earlier and that this page did not list on the day they shipped. Each one names its date. All of them are additive and stay under /v1. Nothing is deprecated.
2026-10-06: the market.trade realtime event. The market channel carries one market.trade event for an anonymous trade on that market, with the body {type, market_slug, outcome_id, amount_bucket, side}. The same body also arrives on the ticker channel, as an object and not as a list. The event names no user and no position, and it has no sequence and no epoch, so it does not take part in the gap check. Realtime events has the details. If you read the channels yourself, ignore an event type that you do not know.
2026-10-08: a malformed id in the path returns 404 not_found. Until then, a path id that did not parse as the type of the route, for example GET /v1/positions/abc, returned a plain-text 400 with no JSON body. It now returns 404 not_found in the error envelope, the same answer as a well-formed id with no resource behind it. Since 2026-10-30 the Market Feed answers the same way, for example for a slug with invalid percent-encoding. If your client handled the plain-text 400, branch on not_found instead.
2026-10-12: a minimum stake per position. A market can now have a minimum stake for one position. The market detail of the Market Feed (GET /v1/markets/{slug}) has a new field, stake_limits.min_stake_per_position, which is null when the market has no minimum. A stake below the minimum returns the new error code stake_below_minimum (HTTP 409), and nothing is applied. POST /v1/positions and POST /v1/orders both check it. Read the minimum from the feed and check the stake before you send the request.
2026-10-12: the platform_fee_statement report type. POST /v1/reports accepts the new report_type value platform_fee_statement. The report has one row for each closed UTC month whose first day is in the period you request, with period_start, period_end, currency, ggr, loss_carried_in, loss_carried_out, fee_bps, fee_amount and computed_at. A closed month never changes, so the report always agrees with the settlement screen of the tenant portal. The running month is not in it, and the report takes no filter. The key needs the read:reports scope.
2026-10-12: your operator book in the reconciliation statement. The reconciliation_statement for the total of your tenant has a second section for your operator book, with the new row kinds aggregate_opening_balance, aggregate_entry and aggregate_closing_balance. The statement for one user does not change. Reconciliation explains the rows and their signs. A statement that we generated before this date does not have the section, so request the period again to get it.
2026-10-30: a malformed query string returns 400 validation_failed. Until then, a query string that did not parse, for example one with a repeated key such as ?status=open&status=closed, returned a plain-text 400 with no JSON body. The Operator API routes that take a query string and the Market Feed now return 400 validation_failed in the error envelope. When we can name the key, details is a list with one {field, reason} object. Otherwise details is null. On other codes details is an object, so its shape depends on the code. A single shape, an object on every code, would break clients that read the list, so it is a /v2 change. Under /v1 both forms stay as they are.
2026-10-08
Two changes to the rate limits. No endpoint, field or event changes, and nothing is deprecated.
PUT /v1/users/{external_user_id} has a tier of its own, user_sync, with a tenant limit of 3,000 requests per minute. Until now it shared the balance_sync tier and its 450 per minute, so a burst of user syncs could use up the budget of your balance syncs. balance_sync keeps 450 per minute for POST /v1/balance-sync and POST /v1/balance-sync/batch.
Market Feed reads that arrive with a widget session token no longer count against the tenant limit of the feed_read tier. They count against the end-user limit of 60 per minute only. Reads that you send with your API key keep the tenant limit of 600 per minute, and your widget traffic can no longer use it up. The Rate limits page shows every tier.
2026-10-07
A new error code for POST /v1/positions. It is additive and arrives under /v1. Nothing is deprecated.
idempotency_key_in_progress (HTTP 409) means that an earlier request with the same Idempotency-Key is still being processed, for example when your client timed out and sent the retry while the first attempt was still running. Nothing is applied by the request that gets it, and its price is not checked. The response carries a Retry-After header, which is 1 second today. When the earlier request timed out or its connection dropped, the code can last for up to 30 seconds, until we know that request is over.
Wait for the Retry-After period and send the request again with the same Idempotency-Key and the same body. Once the first request has finished, the retry returns the position it opened, or is evaluated as a new request if it opened none. Do not send the retry with a new key: a new key is a second request, and it can open a second position next to the first one. The embeddable widget already retries this way.
2026-10-06
Two new webhook events, saved markets in the widget and two new fields in the widget configuration. All of them are additive and arrive under /v1. Nothing is deprecated.
Two new events tell you when a widget session opens or closes a position. position.opened arrives when a user opens a position in the widget, and position.closed arrives when a user closes one. You did not send those requests, so until now you could only learn of the debit or the credit by reading the position again. Each event arrives once per position. A repeated close sends nothing more.
You get no event for a request that you send yourself. A position that you open or close with your tenant API key sends neither event, because you already hold the response. A limit order that fills still sends order.filled, and it does not send position.opened.
position.opened carries the position id, external_user_id, market_slug, outcome_id, the stake, the price at open and open_transaction_id. position.closed carries the same identifiers, the payout that the user received, the close_price and the settlement_transaction_id of the closing transaction. occurred_at is the time of the open or the close. The Webhook guide has the full event bodies. As for any new event type, answer an event that your receiver does not know with 2xx and ignore the body.
Saved markets. Three new routes let a widget user save markets from the market card. GET /v1/widget/saved-markets lists the saved markets, newest save first. PUT /v1/widget/saved-markets/{market_slug} saves one, and DELETE /v1/widget/saved-markets/{market_slug} removes one. Both writes are idempotent. A save beyond the per-user limit returns the new error code saved_markets_limit_reached (HTTP 409), with the limit in details.limit. The routes take the widget session token, never your API key, and they run on a new rate limit tier, saved_markets. The widget calls them itself, so you have nothing to build.
GET /v1/widget/config has two new response fields. brand_name is your display name, the name that your players know your site by. market_url_template is the address of a market page on your site, with one {slug} placeholder, and the share button of the widget links to it. You set the template in the tenant portal, on the market view screen. Each field is null until it has a value.
2026-10-05
Void corrections. We can now correct a market that we voided by mistake, and a market that we resolved when it should have been voided. This entry was published as notice ahead of the release, and that release has shipped: we send the events below from today. Both changes are additive and arrive under /v1. Nothing is deprecated.
Two new events. position.void_corrected arrives for each position that a void correction moves from voided to settled_win or settled_loss, or from settled_win or settled_loss to voided. A position that the correction leaves in its state sends nothing. market.void_corrected arrives once for each market that moved between voided and resolved, with the previous and the new status and outcome.
The set of position states still does not change, but voided is no longer final for a position that a market void refunded. A moved position never receives a second position.settled or position.voided, and a moved market never receives a second market.resolved or market.voided. When a correction takes back a refund or a payout that the user has already spent, we post correction_shortfall_credit first, as for a settlement correction. The same rules as above apply: events have no order, so read the position again with GET /v1/positions/{id} when one arrives. The Webhook guide already shows the two event bodies.
2026-10-04
Settlement corrections. We can now correct a market that we resolved to the wrong outcome. This entry is notice ahead of the release that posts the new ledger value below. All three changes are additive and arrive under /v1. Nothing is deprecated.
The entry_type set of GET /v1/transactions is open. The set can gain values without a new API version, and we list each new value on this page on the day it ships, with no notice period. If a transaction has an entry_type that you do not know, keep it. Read its signed amount and store or show the raw entry_type text. Do not fail, drop or retry the row. The sign rule of amount holds for every value, so the sum of your amount values still matches the balance of the user. An integration that fails on an unknown value is out of contract. Every other enum in the API stays closed.
New value correction_shortfall_credit. A correction can take a payout back from a user who has already spent it. When the balance of the user is below that payout, we first post a correction_shortfall_credit for the difference, and then the reversal. The amount of the new transaction is always positive. It lifts the balance to exactly the payout that the reversal takes back, so the balance never goes below zero. The reversal keeps the type settlement_credit, carries a negative amount, and its reverses_transaction_id points at the original. GET /v1/transactions returns the new value as a transaction of its own, and you can filter on it with ?entry_type=correction_shortfall_credit.
Two new events. position.settlement_corrected arrives for each position that a correction moves between settled_win and settled_loss. A position that the correction leaves in its state sends nothing. market.resolution_corrected arrives once for each market whose resolved outcome we replaced.
The set of position states does not change, but settled_win and settled_loss are no longer final. A correction can move a position from one to the other, and position.settled is never sent twice for one position. The new event is how you learn of the move. A moved position keeps its first settled_at. Events have no order, so market.resolution_corrected can arrive before the position.settlement_corrected events that it announces. When one of these events arrives, read the position again with GET /v1/positions/{id} and store the state and payout from that response, so that a late event never replaces a newer state. The Webhook guide has the full event bodies.
2026-09-27
Limit orders. Four new endpoints are additive and arrive under /v1. Nothing is deprecated.
POST /v1/orders places a limit order on one outcome of a market. The order fills when the price falls to limit_price or below, and the fill opens a position. Placing an order moves no money, and an order never fills in the request that places it. GET /v1/orders and GET /v1/orders/{id} read orders, and DELETE /v1/orders/{id} cancels an open order. With your API key, the calls need the write:positions scope and name the user. The widget calls them with its session token. When an order fills, we send order.filled. The API reference has the schemas, and the Webhook guide has the event body.
2026-09-26
The developer portal is live. The /v1 contract is as this portal describes it. No item is deprecated.