API Reference
Drive a forward run from your own code: place orders against real books, publish your model’s fair values, and read back what it made. Every run declares up front whether it is spending simulated, demo or real money.
https://api.cupelar.comAuthentication
Create a key in the web app under API keys. The token is shown once and cannot be recovered — if you lose it, rotate the key. Keys expire after 90 days by default and a year at most.
curl "https://api.cupelar.com/forward-runs" \
-H "Authorization: Bearer $CUPELAR_KEY"Set CUPELAR_KEY to your cup_sk_… token in your local environment. Send it as a Bearer token, or in an X-API-Key header. Keep the key in server-side code, out of browser bundles and source control.
Permissions
A key carries only the permissions ticked when it was made. Demo shares the live permission because both drive an authenticated Kalshi credential.
| Permission | What it allows |
|---|---|
read | Reading your strategies, runs, positions, ledgers and results |
strategies:write | Creating your own strategies; editing and deleting remain session-only |
runs:manage | Creating and controlling shadow runs, versioning policies and clearing their kill switch |
fair-values:write | Publishing, updating and retiring your strategy’s fair values |
orders:shadow | Submitting orders to a shadow run |
orders:live | Submitting orders to a demo or live run |
429 includes Retry-After in seconds. Wait before retrying.The three modes
A run is opened in one mode and stays in it. The mode declares whose money is moving, and every result the run produces is read against it.
shadow
No money at riskSimulated orders against the real production book, with modeled fees and conservative fills. Positions are marked against the book and settled on the real outcome.
- Funds
- None at risk
- Prices
- Production book
- Permission
orders:shadow- Publishable
- Yes
demo
Mock fundsOrders on Kalshi’s demo exchange using a demo credential and mock funds. Use this to test execution end to end; demo prices and liquidity are not production prices.
- Funds
- Demo balance
- Prices
- Demo exchange
- Permission
orders:live- Publishable
- Never
live
Real moneyReal orders with your money through a connected production credential. Execution must be enabled for the deployment and requires the live-order permission.
- Funds
- Your money
- Prices
- Production book
- Permission
orders:live- Publishable
- Yes
credential_id; a live run needs a production one. Cupelar rejects a mismatch. Shadow runs do not need a Kalshi credential.Open a run
Create and start a shadow run using a key with runs:manage, or use Forward tests. Then use its ID and an execution key to submit orders and read results. The mode is fixed at creation. New runs begin in draft until you start them.
Create a strategy and shadow run with an API key
Select read, strategies:write and runs:manage when creating a setup key. Existing keys keep their original scopes; create a new key to opt in. Keep management permissions off execution-only keys. Cognito access tokens still work for interactive requests.
/strategiesstrategies:writecurl -X POST "https://api.cupelar.com/strategies" \
-H "Authorization: Bearer $CUPELAR_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Mention model",
"status": "draft",
"visibility": "private",
"series": [
"KXUST5AD"
]
}'Save the returned strategy ID. Read your strategies using GET /strategies or GET /strategies/{id} with the read scope. Editing and deleting existing strategies still require a signed-in session.
/forward-runsruns:manage (shadow) · membershipcurl -X POST "https://api.cupelar.com/forward-runs" \
-H "Authorization: Bearer $CUPELAR_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Mention model, shadow",
"mode": "shadow",
"strategy_id": 12,
"bankroll_cents": 100000,
"policy": {
"min_edge_cents": 3,
"kelly_multiplier": 0.25,
"auto_execute": true
}
}'Set CUPELAR_KEY to your setup API key. Replace strategy_id with a strategy you own, or omit it for a standalone run. The response is 201 with the run’s id, status, balances and initial policy.
Then start it, replacing 7 with the returned run ID:
curl -X POST "https://api.cupelar.com/forward-runs/7/status?state=running" \
-H "Authorization: Bearer $CUPELAR_KEY"name is required (1–128 characters). mode defaults to shadow; bankroll_cents defaults to 100000 and must be positive. Demo and live run management remains session-only and requires your connected credential_id.Policy defaults
Every policy key and its default. Retrieve the deployed defaults with any valid key. Changing a policy creates a new version; orders continue to point to the version they ran under. Use PUT /forward-runs/{id}/policy with runs:manage for shadow runs, or a signed-in session.
/forward-runs/policy-defaultsAny key| Key | Default | What it does |
|---|---|---|
min_edge_cents | 2 | Minimum edge, net of fees, for automatic execution |
kelly_multiplier | 0.25 | Fraction of full Kelly to stake; capped at 1 |
auto_execute | false | Whether Cupelar trades your published fair values for you |
signal_lag_seconds | 0 | Minimum age of a published fair value before acting on it |
allowed_series | [] | Allowed series tickers; an empty list allows all series |
bankroll_cents | 100000 | Starting bankroll; set from bankroll_cents when opening a run |
max_bankroll_pct_per_position | 5 | Maximum percentage of the bankroll in one market; capped at 100 |
max_position_contracts | 100 | Cap on the resulting position, including outstanding orders |
max_position_notional_cents | 10000 | Per-position notional cap, in cents |
daily_notional_cap_cents | 50000 | Maximum submitted notional per UTC day |
max_orders_per_day | 50 | Maximum submitted orders per UTC day |
max_open_positions | 25 | Maximum open or committed market-side positions |
fee_coefficient | 0.07 | Fee coefficient used by the fill simulation |
latency_ticks | 1 | Cents the price moves against you before a simulated fill |
queue_haircut | 0.5 | Share of displayed size a simulated order can reach; capped at 1 |
max_book_age_ms | 5000 | Reject simulated fills against a book older than this |
entry_book_source | "live" | Recorded entry-book source |
maker_policy | "through_only" | Recorded policy for simulated resting fills |
mark_cadence_seconds | 300 | Recorded marking cadence in seconds |
exit_rule | "hold_to_settlement" | Recorded exit rule |
settlement_fee_cents | 0 | Fee charged per non-void shadow position settlement, in cents |
/forward-runs/{run_id}/policySession · membershippolicy.config, merge your changes, then submit the full object. Unknown keys are dropped.For numeric fields, numeric strings are converted ("3" becomes 3); non-numeric strings fall back to the field’s default, and negative numbers are clamped to zero.
Supply allowed_series as a list, such as ["KXTEST"]. A bare string is split into individual characters: "KXTEST" becomes ["K", "X", "T", "E", "S", "T"], which can cause unexpected series_not_allowed refusals.
The recorded assumptions entry_book_source, maker_policy and exit_rule are converted to strings without checking supported values. An arbitrary value can be stored and included in the policy hash without changing execution behavior. These coercions do not produce a validation error. Use the documented values and review the returned config before using the policy.
Include bankroll_cents when merging: omitting it resets the policy’s sizing bankroll to 100000. This does not reset the run’s cash balance or its recorded starting bankroll.
Read the full version history at GET /forward-runs/{run_id}/policies with read and membership. Recorded assumptions describe the policy; the worker’s deployment schedule controls when work actually runs.
Place an order
/forward-runs/{run_id}/ordersOrder scopeRequires orders:shadow on a shadow run, or orders:live on a demo or live run. The run’s mode decides where the order goes. Use a run you have opened and started.
Body parameters
| Field | Type | Required / default | Description |
|---|---|---|---|
client_order_id | string | Required | 1–128 characters. Unique within the run. Reusing it returns the original order, even if the body changes. |
ticker | string | Required | Kalshi market ticker, 1–128 characters. Use a currently tradable market. |
side | "yes" | "no" | Default: "yes" | The contract side. To take the other side of a market, buy NO. |
action | "buy" | "sell" | Default: "buy" | You can only sell contracts the run actually holds. |
count | integer | Required | Number of contracts, from 1 to 1,000,000. |
limit_price_cents | integer | Required | Limit price in whole cents, from 1 to 99. |
time_in_force | string | Optional | "immediate_or_cancel" (default) or "good_till_canceled". |
post_only | boolean | Default: false | Only place a maker order; an immediately crossing order is refused. |
expiration_ts | integer | null | Optional | Unix expiry time in seconds. Sent to Kalshi for demo/live orders. For shadow resting orders, the worker checks expiry before subsequent fills; the initial fill simulation does not check it. |
decision_at | datetime | null | Optional | When your strategy decided. ISO 8601 with an explicit timezone. |
signal_revision_id | integer | null | Optional | Use revision_id from POST /signals or PUT /signals/{id} to retain predicted edge. Ownership, strategy and market are checked. |
curl -X POST "https://api.cupelar.com/forward-runs/7/orders" \
-H "Authorization: Bearer $CUPELAR_KEY" \
-H "Content-Type: application/json" \
-d '{
"client_order_id": "my-strategy-001",
"ticker": "YOUR_MARKET_TICKER",
"side": "yes",
"action": "buy",
"count": 5,
"limit_price_cents": 20
}'Replace YOUR_MARKET_TICKER with a tradable market. Use a new client_order_id for each new intent; reuse the same ID after a timeout to retrieve the original order. Both new submissions and retries return HTTP 200.
status, filled_count, fills and reject_reason in the response. A successful HTTP request does not mean the order filled. An unknown status may mean the exchange accepted it; do not submit a new ID to retry./forward-runs/{run_id}/orders?limit=200read · membershipPoll the order list to follow execution. limit accepts 1–1000 and defaults to 200.
Why an order is refused
Admission failures normally return HTTP 409 with a machine-readable detail.reason. These common reasons let your code distinguish a limit from a stopped run.
{
"detail": {
"reason": "kill_switch",
"message": "This run's kill switch is on."
}
}| Reason | Meaning |
|---|---|
insufficient_cash | The run’s available bankroll cannot cover the order and fees. |
max_orders_per_day | The per-day order cap is spent. |
daily_notional_cap | The per-day notional cap is spent. |
max_position_contracts | The resulting position exceeds the contract cap. |
max_position_notional | The resulting position exceeds the notional cap. |
max_bankroll_pct | The market would hold too much of the bankroll. |
max_open_positions | The run has reached its open-position limit. |
series_not_allowed | The market is outside the policy’s allowed series. |
run_not_running | Start the shadow run with runs:manage or a signed-in session before submitting orders. |
kill_switch | The run’s kill switch is on. |
live_disabled | Live execution is disabled in this deployment (HTTP 403). |
200, status: "rejected" and a reject_reason such as stale_book, no_book or exchange_rejected. Inspect the order body as well as the HTTP status.Authentication, validation and rate-limit errors
| Status | What to do |
|---|---|
401 | Supply a valid, unexpired key or session token. Revoked and invalid keys share the same error. |
402 | An active membership is required for this endpoint. |
403 | Check the required scope, session-only restriction, or whether live execution is enabled. |
404 | The resource does not exist or is not owned by your account. |
422 | Correct the request fields. Validation errors may include a list in detail. |
429 | Wait for the Retry-After interval before making another request. |
Auto-execute fair values
Publish a fair value, link the strategy to a running forward run, and enable auto_execute in its policy. Each worker cycle compares eligible values with the book, checks edge net of fees, and sizes orders by fractional Kelly within the run’s caps.
/signalsfair-values:write · membershipcurl -X POST "https://api.cupelar.com/signals" \
-H "Authorization: Bearer $CUPELAR_KEY" \
-H "Content-Type: application/json" \
-d '{
"strategy_id": 12,
"ticker": "YOUR_MARKET_TICKER",
"side": "yes",
"fair_value_cents": 65,
"note": "Latest model estimate"
}'strategy_id, ticker and fair_value_cents are required. Fair value is always on the YES axis, from 0 to 100, even if side is no. Publishing the same strategy and ticker updates its current value and records a new revision. The response includes revision_id; pass it as signal_revision_id when submitting your own forward-run order to preserve its forecast and predicted edge.
fair-values:write. A running live strategy can place real orders when you publish new values.Use PUT /signals/{signal_id} with the same body shape to update a signal, or DELETE /signals/{signal_id} to retire it. Both require fair-values:write and membership. Decisions use the permanent revision history so the numbers acted on stay reconstructible.
Reading the results
All of these endpoints require the read scope and an active membership when billing is enabled. Monetary fields ending in _cents are in cents; ratios such as roi and fill_rate are fractions.
/forward-runs/{run_id}/summaryOrders, fill rate, staked, fees, profit and return with a confidence range
/forward-runs/{run_id}/positionsCurrent holdings, valuations and mark_status for each position
/forward-runs/{run_id}/ledgerCompleted positions, with predicted edge beside realized results
/forward-runs/{run_id}/equity-curveEquity over time, including the count of stale or unavailable marks
/forward-runs/{run_id}/calibrationHow published fair values scored against outcomes and the market
/forward-runs/{run_id}/export.csvThe ledger as CSV, ready to download and score yourself
curl "https://api.cupelar.com/forward-runs/7/summary" \
-H "Authorization: Bearer $CUPELAR_KEY"insufficient_sample: true and leaves the confidence range null below 30 completed position cycles across three distinct close dates. When available, intervals resample whole days rather than treating every market as independent.beats_market is true only when the whole confidence interval favors the model; it may be null when there is insufficient evidence.Publish a track record
/forward-runs/{run_id}/publishSession · membershipAttach a run to your strategy’s public page and finalize it. Publication freezes its policy and execution record, with an integrity fingerprint and a public execution manifest.
Qualifies when
- At least 30 completed position cycles
Closing a position in several fills does not multiply the sample size.
- Across at least 3 distinct close dates
Markets closing on the same day can share what moved that day.
- At least 14 days since the run started
The age is measured from started_at, not creation.
- No policy change in the last 24 hours
The terms must have stood still long enough to read the results against.
- A strategy attached and no orders still executing
Cancel or finish outstanding orders and wait for exchange reconciliation.
409 with reason demo_not_publishable. Use a shadow or live run for a publishable track record./forward-runs/public/{run_id}PublicRun responses include a stable owner_key and a name-based slug. Website links use /runs/{owner_key}/{slug}, or /runs/public/{owner_key}/{slug} for published records. Existing numeric links redirect; numeric API endpoints remain supported.
/forward-runs/by-owner/{owner_key}/{slug}/controlOwner · any key/forward-runs/public/by-owner/{owner_key}/{slug}PublicThe public execution manifest is at /forward-runs/public/{run_id}/manifest. Neither public endpoint needs authentication.
DELETE /forward-runs/{run_id}/publish using a session. The removal is dated and the permanent public record remains. A finalized run cannot restart; open a new run to continue testing.Stopping a run
/forward-runs/{run_id}/kill-switch?on=trueAny keyBlock new order submissions with any of your credentials, including a read-only API key. Turning the switch off requires runs:manage for shadow runs, or a signed-in session. No active membership is needed to use the kill switch or cancel an order.
curl -X POST "https://api.cupelar.com/forward-runs/7/kill-switch?on=true&reason=manual-stop" \
-H "Authorization: Bearer $CUPELAR_KEY"/forward-runs/{run_id}/orders/{client_order_id}Any keyFind your runs without a membership through GET /forward-runs/control/list, or inspect a known run with GET /forward-runs/{run_id}/control. Both accept any valid key.
A shadow-management key or signed-in session can set POST /forward-runs/{run_id}/status?state=paused or state=stopped. A paused run can resume. A stopped run cannot restart.