Developer documentation 

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.

Base URL
https://api.cupelar.com

Authentication

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
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.

Run reports and publishing fair values require an active Cupelar membership when billing is enabled. This reference is available without signing in.

Permissions

A key carries only the permissions ticked when it was made. Demo shares the live permission because both drive an authenticated Kalshi credential.

PermissionWhat it allows
readReading your strategies, runs, positions, ledgers and results
strategies:writeCreating your own strategies; editing and deleting remain session-only
runs:manageCreating and controlling shadow runs, versioning policies and clearing their kill switch
fair-values:writePublishing, updating and retiring your strategy’s fair values
orders:shadowSubmitting orders to a shadow run
orders:liveSubmitting orders to a demo or live run

Needs a signed-in session

Creating or managing keys, editing or deleting existing strategies, managing demo/live runs, and publishing a track record. Shadow-run setup accepts the explicit runs:manage scope; strategy creation accepts strategies:write. Existing keys do not gain these permissions automatically.

Works with any of your keys

Turning the kill switch on and cancelling a resting order. Your code can stop new orders without a human at a browser. Policy defaults and run controls also accept any valid key.

Per key, reads refill at 10 requests/second with a burst of 30; writes refill at 2 requests/second with a burst of 5. HTTP 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 risk

Simulated 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 funds

Orders 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 money

Real 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
A demo run needs a demo credential_id; a live run needs a production one. Cupelar rejects a mismatch. Shadow runs do not need a Kalshi credential.
Demo results cannot be published as a track record. Shadow results can qualify because they simulate fills against the real production book.

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.

POST/strategiesstrategies:write
cURL
curl -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.

POST/forward-runsruns:manage (shadow) · membership
cURL
curl -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
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.

GET/forward-runs/policy-defaultsAny key
KeyDefaultWhat it does
min_edge_cents2Minimum edge, net of fees, for automatic execution
kelly_multiplier0.25Fraction of full Kelly to stake; capped at 1
auto_executefalseWhether Cupelar trades your published fair values for you
signal_lag_seconds0Minimum age of a published fair value before acting on it
allowed_series[]Allowed series tickers; an empty list allows all series
bankroll_cents100000Starting bankroll; set from bankroll_cents when opening a run
max_bankroll_pct_per_position5Maximum percentage of the bankroll in one market; capped at 100
max_position_contracts100Cap on the resulting position, including outstanding orders
max_position_notional_cents10000Per-position notional cap, in cents
daily_notional_cap_cents50000Maximum submitted notional per UTC day
max_orders_per_day50Maximum submitted orders per UTC day
max_open_positions25Maximum open or committed market-side positions
fee_coefficient0.07Fee coefficient used by the fill simulation
latency_ticks1Cents the price moves against you before a simulated fill
queue_haircut0.5Share of displayed size a simulated order can reach; capped at 1
max_book_age_ms5000Reject 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_seconds300Recorded marking cadence in seconds
exit_rule"hold_to_settlement"Recorded exit rule
settlement_fee_cents0Fee charged per non-void shadow position settlement, in cents
PUT/forward-runs/{run_id}/policySession · membership
A policy update is a complete configuration, not a patch: omitted keys take their defaults. Read the current run’s policy.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

POST/forward-runs/{run_id}/ordersOrder scope

Requires 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

FieldTypeRequired / defaultDescription
client_order_idstringRequired1–128 characters. Unique within the run. Reusing it returns the original order, even if the body changes.
tickerstringRequiredKalshi 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.
countintegerRequiredNumber of contracts, from 1 to 1,000,000.
limit_price_centsintegerRequiredLimit price in whole cents, from 1 to 99.
time_in_forcestringOptional"immediate_or_cancel" (default) or "good_till_canceled".
post_onlybooleanDefault: falseOnly place a maker order; an immediately crossing order is refused.
expiration_tsinteger | nullOptionalUnix 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_atdatetime | nullOptionalWhen your strategy decided. ISO 8601 with an explicit timezone.
signal_revision_idinteger | nullOptionalUse revision_id from POST /signals or PUT /signals/{id} to retain predicted edge. Ownership, strategy and market are checked.
cURL
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.

Check 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.
Position caps include current holdings, committed unfilled orders, and this order. Demo and live orders reserve their possible cost when submitted; reconciliation records actual fills and releases unspent funds.
GET/forward-runs/{run_id}/orders?limit=200read · membership

Poll 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.

JSON · HTTP 409
{
  "detail": {
    "reason": "kill_switch",
    "message": "This run's kill switch is on."
  }
}
ReasonMeaning
insufficient_cashThe run’s available bankroll cannot cover the order and fees.
max_orders_per_dayThe per-day order cap is spent.
daily_notional_capThe per-day notional cap is spent.
max_position_contractsThe resulting position exceeds the contract cap.
max_position_notionalThe resulting position exceeds the notional cap.
max_bankroll_pctThe market would hold too much of the bankroll.
max_open_positionsThe run has reached its open-position limit.
series_not_allowedThe market is outside the policy’s allowed series.
run_not_runningStart the shadow run with runs:manage or a signed-in session before submitting orders.
kill_switchThe run’s kill switch is on.
live_disabledLive execution is disabled in this deployment (HTTP 403).
Simulation or exchange failures can instead return an order with HTTP 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
StatusWhat to do
401Supply a valid, unexpired key or session token. Revoked and invalid keys share the same error.
402An active membership is required for this endpoint.
403Check the required scope, session-only restriction, or whether live execution is enabled.
404The resource does not exist or is not owned by your account.
422Correct the request fields. Validation errors may include a list in detail.
429Wait 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.

POST/signalsfair-values:write · membership
cURL
curl -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.

Enable automatic execution on a shadow run with runs:manage or a signed-in session. Demo/live controls require a session. The worker acts on the run’s policy and mode; the publishing key only needs 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.

GET/forward-runs/{run_id}/summary

Orders, fill rate, staked, fees, profit and return with a confidence range

GET/forward-runs/{run_id}/positions

Current holdings, valuations and mark_status for each position

GET/forward-runs/{run_id}/ledger

Completed positions, with predicted edge beside realized results

GET/forward-runs/{run_id}/equity-curve

Equity over time, including the count of stale or unavailable marks

GET/forward-runs/{run_id}/calibration

How published fair values scored against outcomes and the market

GET/forward-runs/{run_id}/export.csv

The ledger as CSV, ready to download and score yourself

cURL
curl "https://api.cupelar.com/forward-runs/7/summary" \
  -H "Authorization: Bearer $CUPELAR_KEY"
The summary sets 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.
Calibration evaluates published revisions against outcomes and the market’s price at publication. 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

POST/forward-runs/{run_id}/publishSession · membership

Attach 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.

Demo runs never qualify. Publication returns 409 with reason demo_not_publishable. Use a shadow or live run for a publishable track record.
GET/forward-runs/public/{run_id}Public

Run 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.

GET/forward-runs/by-owner/{owner_key}/{slug}/controlOwner · any key
GET/forward-runs/public/by-owner/{owner_key}/{slug}Public

The public execution manifest is at /forward-runs/public/{run_id}/manifest. Neither public endpoint needs authentication.

Detach with 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

POST/forward-runs/{run_id}/kill-switch?on=trueAny key

Block 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
curl -X POST "https://api.cupelar.com/forward-runs/7/kill-switch?on=true&reason=manual-stop" \
  -H "Authorization: Bearer $CUPELAR_KEY"
The kill switch blocks new orders. It does not cancel existing resting orders or close positions. Cancel outstanding orders separately and check their final status.
DELETE/forward-runs/{run_id}/orders/{client_order_id}Any key

Find 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.