A paper-trading futures API on real live prices. Your bot opens leveraged positions, rests limit and stop orders, trails its stops and gets liquidated - settled on our servers, in simulated dollars. Same code you will run for real, none of the consequences.
No card, no KYC, no deposit. Free forever at 120 requests a minute - Pro is $29 when one bot becomes four. Market data stays keyless on every plan.
# no key needed - run this right now curl "https://marginpad.io/api/bot/v1/price?symbol=BTC" { "symbol": "BTC", "price": …, "ts": … }
curl -X POST ".../v1/open" -H "X-API-Key: mpb_…" \ -d '{"symbol":"BTC","side":"long","margin_usd":100, "leverage":10,"trail_pct":1.5}' { "ok": true, "position": { "id": "bot_9f21…", "entry": …, "liq_price": …, "trail_pct": 1.5, "fee_round_trip_usd": 1.10 } }
The first one needs no key at all - paste it into a terminal before you decide anything. Every path below exists in a frozen v1 and an enveloped v2; pick one and it will not move under you.
GET /v1/price and /v1/klines are keyless and CORS-enabled. Up to 1,000 candles a call, seven intervals, paged backwards with &end= for as much history as a backtest needs.
POST /v1/open fills at the live price with isolated margin and gives you back the liquidation level. Add type:"limit" or "stop" to rest an order server-side, or dry_run to price a trade without writing anything.
Stops, targets, trailing stops and liquidations settle on our side, on 1-minute candles, whether or not your bot is running. Open the WebSocket and the outcome arrives in about two seconds - it is free on every plan, and it is the difference between a bot that reacts and one that waits for its next loop.
# price (no key)
curl "https://marginpad.io/api/bot/v1/price?symbol=SOL"
# open a 20x long with $100 margin
curl -X POST "https://marginpad.io/api/bot/v1/open" \
-H "X-API-Key: YOUR_KEY" -H "Content-Type: application/json" \
-d '{"symbol":"SOL","side":"long","margin_usd":100,"leverage":20}'
# check live P&L
curl "https://marginpad.io/api/bot/v1/positions" -H "X-API-Key: YOUR_KEY"
# close half, let the rest run
curl -X POST "https://marginpad.io/api/bot/v1/close" \
-H "X-API-Key: YOUR_KEY" -H "Content-Type: application/json" \
-d '{"id":"POSITION_ID","pct":50}'
# the whole loop, without a single poll
const { MarginPad } = require("marginpad");
const mp = new MarginPad("YOUR_KEY");
mp.stream(ev => {
if (ev.type === "snapshot") console.log("open now:", ev.data.positions.length);
if (ev.type === "position") {
const p = ev.data.position; // opened | updated | closed
console.log(ev.data.event, p.symbol, p.pnl_usd ?? p.unrealized_pnl_usd);
if (ev.data.event === "closed") decideWhatIsNext(p);
}
});
# or with nothing but a URL - any language with a WebSocket client
wscat -c "wss://marginpad.io/api/bot/v2/stream?api_key=mpb_..."
<- {"type":"welcome","data":{"channels":["positions","prices"],"tick_ms":2000}}
<- {"type":"snapshot","data":{"positions":[...]}}
<- {"type":"position","data":{"event":"closed","position":{...,"pnl_usd":-1.24}}}
Measured, and the reason this tab exists: 99.4% of every call ever made to this API was a poll of /positions or /account, and the WebSocket had been opened six times. The stream is free on every plan on purpose - gating it would push everyone back onto polling, which costs us more than the stream does. If you must poll, send back the ETag: an unchanged poll then costs a 304 with an empty body.
# python - the official client, zero dependencies
pip install marginpad
from marginpad import MarginPad
mp = MarginPad("YOUR_KEY")
price = mp.price("BTC")["price"]
if my_signal(price): # your strategy here
r = mp.open("BTC", "long", margin_usd=50, leverage=10,
sl=price * 0.97, trail_pct=1.5, # trailing stop, ratcheted server-side
client_order_id=f"sig-{int(time.time())}") # a retry can never open twice
print("opened", r["position"]["id"], "liq @", r["position"]["liq_price"])
changed = mp.positions(status="open") # None when nothing changed (ETag / 304)
print(mp.report()["skill"])
# node.js - the official client, zero dependencies
npm install marginpad
const { MarginPad } = require("marginpad");
const mp = new MarginPad("YOUR_KEY");
const { price } = await mp.price("ETH");
if (mySignal(price)) { // your strategy here
const { position } = await mp.open({ symbol: "ETH", side: "short", margin_usd: 50, leverage: 10,
trail_pct: 2, client_order_id: "sig-" + Date.now() });
console.log("opened", position.id, "liq @", position.liq_price);
}
mp.stream(ev => { if (ev.type === "position") console.log(ev.data.event, ev.data.position.id); });
# plain HTTP, any language
import requests
API = "https://marginpad.io/api/bot"; H = {"X-API-Key": "YOUR_KEY"}
r = requests.post(f"{API}/v2/open", headers=H, json={"symbol": "BTC", "side": "long",
"margin_usd": 50, "leverage": 10, "dry_run": True}).json() # priced, nothing written
print(r["data"]["position"]["fee_round_trip_usd"], r["data"]["position"]["liq_price"])
Authenticate with X-API-Key on every call except /price, /klines and /time. Official clients, zero dependencies - npm install marginpad or pip install marginpad, or one file each: marginpad.py · marginpad.js - REST, WebSocket, ETag polling, 429 back-off and webhook signature verification built in.
Sign in with an email - no password, no KYC. Your bot's positions persist on your account, and one account can hold several keys so a backtest bot and a live bot never share a rate budget.
| Name | Book | Key | Calls | Last used |
|---|
Your keys, their usage and the book each one trades appear here once you are signed in. Keys are secrets - anyone holding one can trade your paper account.
Testnet books are thin, their prices drift from the real market, and they reset without warning. Everything here is priced from the live multi-exchange feed our own charts run on - only the dollars are simulated.
Limit entries, stop entries, stop-losses, targets and trailing stops are all evaluated on our side against 1-minute candles. Your bot can be offline, restarting or redeployed and the fill still lands at your level, stamped with the minute the market actually reached it.
Both legs pay a taker fee, charged as a round trip at close, with funding accrued across marks. Name a venue and the paper account is charged at that exchange's published schedule less our referral discount - so a scalping strategy fails here for the same reason it would fail there.
Isolated margin, 0.5% maintenance, losses capped at your margin. A trailing stop is moved by each candle's extreme after that candle was checked against the stop in force - so a bar can never stop you out at a level it created itself.
Send client_order_id and a retried open returns the position the first call created, flagged idempotent: true. Closing takes one too. This single field is the difference between a bot you trust unattended and one you babysit.
A free WebSocket pushes position events and marks every ~2 s; webhooks POST the same shape to your own URL, HMAC-signed and retried, even while nothing of yours is connected. ETags make the polls you keep nearly free.
Every book gets its own journal, equity curve and drawdown. /v1/report returns win rate and return by coin, leverage band, side and hour, with a skill score - and it says nothing under eight trades rather than inventing a pattern.
Every account starts on Free and gets the complete product: the trading engine, real venue fees, limit and stop entries, trailing stops, replay, books, the WebSocket stream and all keyless market data. The paid plans raise ceilings and add two things - webhooks and AI market reads. Nothing else is held back.
Build and run a real bot without paying anything.
For a bot that runs unattended, and for comparing strategies instead of guessing between them.
/v1/aiFor a fleet - a family of strategies, each with its own book, running at the same time.
For an app or a desk trading on behalf of its own users - a key each, a book each.
Measured across every key on this API over the last fortnight: the busiest bot in production sits at 46 requests a minute - 38% of what the Free plan already allows - and no key has ever been rate-limited, on any plan. If you are choosing between these, choose on the books, the webhooks and the history, which is what actually runs out. The throughput is there when you need it. Most bots never touch it.
| What changes with the plan | Free | Pro · $29 | Max · $79 | Business · $159 |
|---|---|---|---|---|
Market-data API (/api/v1/*) with your key60 / min per IP without a key, on every plan | 120 / min | 600 / min | 2000 / min | 5000 / min |
| API keys per account | 3 | 10 | 30 | 100 |
| Open positions at once | 50 | 200 | 500 | 1000 |
| Books - a separate journal, balance and report per strategy the limit most people actually reach | 1 | 5 | 20 | 50 |
| Webhooks - signed, retried, delivered while you are offline | none | 3 | 15 | 50 |
AI market read /v1/ai | none | 50 a day | 200 a day | 500 a day |
| Trade history kept what /v1/trades, the report and the equity curve can still see | 30 days | 90 days | 90 days | 90 days |
| Requests per minute, per key measured: the busiest production bot uses 46, and nothing has ever been limited | 120 | 600 | 2000 | 5000 |
Trading report /v1/report | totals + skill score up to 30 days back | + breakdowns & findings up to 90 days back | + breakdowns & findings | + breakdowns & findings |
On every plan, Free included: the whole trading engine - market, limit and stop orders, trailing stops, partial closes, dry run and replay - the WebSocket stream, the MCP server, the fee schedule of 9 real venues, $1 to $100,000 of margin per trade at up to 1000× leverage, and every keyless market-data endpoint at full speed. The paid plans move ceilings. They do not unlock the product.
Premium is a different product. A MarginPad Premium subscription unlocks features on the website - the exclusive chart indicators, the liquidation heatmap, Ask-AI on charts - and since 15 September 2026 it does not change a single API limit. These plans do not unlock those site features either. Buy whichever one you actually use.
Plans are month by month, paid in crypto or straight from your MarginPad rewards balance, and a month bought early is added to the one you have rather than replacing it. Your live ceilings are always in GET /api/bot/v1/usage - limits, features, plan_until - so a bot can read them instead of guessing.
What you are buying, in writing. Section 9 of the terms covers the API on its own: what a plan includes and what happens the day it ends, that nothing renews automatically and there is nothing to cancel, what we do and do not commit to on availability (there is no SLA - we say so, and we say what we do commit to instead), thirty days’ notice before any breaking change, and that you may build and sell commercial products on this API without asking us.
Your bot is told before the plan lapses, not after. Every keyed response carries X-MP-Plan, and inside the last fourteen days it also carries X-MP-Plan-Expires and X-MP-Plan-Days-Left. Log them and an unattended bot can warn you itself; the same countdown is on this page and in /v1/usage. A plan that runs out drops the key to Free - it never stops answering.
Twenty-four of them. Everything is JSON over HTTPS, CORS on, one header. Swap v1 for v2 in any path for the {ok,data,ts} envelope.
&end=<unix ms> to page further back.asset_class, max_leverage and taker_fee_pct per symbol. Filter with ?class=crypto|stock|forex|metal|index. Call this once at startup instead of discovering limits by trial and error.drift_ms back. A bot bucketing candles against a drifting local clock builds bars nobody else sees, and that is miserable to debug from the outside.bars = get(f"/api/bot/v1/klines?symbol=BTC&interval=1")
while len(all_bars) < wanted:
end_ms = bars[0]["time"] * 1000 # oldest candle in hand
bars = get(f"/api/bot/v1/klines?symbol=BTC&interval=1&end={end_ms}")
if not bars: break
all_bars = bars + all_bars
{"symbol":"BTC","side":"long","margin_usd":100,"leverage":20,"sl":58000,"tp":66000} (sl/tp optional). Returns the position incl. its liq_price. Add "dry_run":true to get the priced trade back - entry, quantity, liquidation price and distance, the open fee and the full round trip - without writing anything. Use the dry run to size a position or to unit-test your bot against the real engine.trail_pct sets a stop that follows the best price seen since entry at that percent distance, ratcheted on the server: from 1-minute candle extremes on the minute sweep and from the live price on every /positions read, so it keeps moving while your bot is offline and never loosens. A long opened at 60,000 with trail_pct: 1 starts with its stop at 59,400; when price prints 63,000 the stop is 62,370. Positions carry trail_pct and trail_hwm.{"symbol":"BTC","side":"long","type":"limit","limit_price":58000,"margin_usd":100,"leverage":20}. It fills at your price, not at the price the engine noticed the cross - the fill is found from the 1m high/low, so a wick that retraced between two checks still counts. Your bot does not have to be running. A limit long must be below the market and a limit short above it; otherwise you get limit_marketable rather than a silent market fill.{"symbol":"BTC","side":"long","type":"stop","limit_price":66000,"margin_usd":100,"leverage":10}. Same engine as a limit order - it fills at the level from 1m candles, with the browser closed - but the wrong side is refused (stop_wrong_side) so a stop and a limit can never be confused. sl, tp, trail_pct, client_order_id and dry_run all apply.{"id":"bp...","pct":50} - pct is optional (default 100); partial closes split the position exactly like on the site.{"id":"bot...","sl":63500,"tp":66000,"trail_pct":1.5} - send null to clear one, omit a field to keep it. Levels are checked against the entry side. Before this existed you had to close and reopen to move a stop, which changed your entry and paid a full round trip.type: "limit" | "stop", plus the last 20 that filled, expired or were cancelled - a filled one carries the position_id it created. Orders are good until cancelled, expire after 30 days, and 20 may rest per account.{"order_id":"lo...","limit_price":58500,"sl":57000,"tp":62000,"margin_usd":150,"leverage":10,"trail_pct":1} - send only what changes. Before this the only way was cancel and re-place, which lost the order's client_order_id. The direction is re-derived from the market now and the candle watermark restarts, so a moved level can never fill on a bar printed before the change. A filled, cancelled or expired order answers 409.{"order_id":"lo..."}. Already filled or cancelled comes back 409, so a retry can never undo a fill.mark_price and unrealized_pnl_usd; crossed liquidations and SL/TP are settled automatically. ?status=open|closed trims the body to what your loop actually reads; ?since=<unix ms> and If-None-Match (304 when nothing changed) are supported too.balance_usd, equity_usd, free_margin_usd and return_pct./account when that is all your sizing logic needs./positions is capped at 100 rows; this pages through the whole retention window - follow next_before until it comes back null. Use it to compute strategy statistics on the complete record instead of a truncated tail.max_drawdown_pct and the live unrealized point at the end.{"confirm":true}: closes archived, orders cancelled. Refused with 409 open_positions_exist while anything is open.usage_30d - calls per day by endpoint plus refused (the 429s), so a bot can see how close it runs to its limit.POST {"venue":"bybit"} sets the account default; fee_venue on /open overrides it for one position. See venue fees.locked[] names what is withheld). Every finding carries the n it rests on and says nothing under 8 trades.{"symbol":"BTC","interval":"60","question":"Is this leaning long or short?"}. Same model, same prompt and the same 50-a-day quota as Ask-AI on the site. You get the answer, a parsed plan (bias, entry, stop, targets) when the model saw a setup, and the brief it reasoned over - price, move, swing high/low, moving averages, RSI, ATR, Bollinger, recent closes - so you can log exactly what it looked at. Educational, not financial advice.POST {"act":"add","url":"https://…"} registers one, {"act":"test","id":…} sends a ping and returns the status your server answered, {"act":"delete","id":…} removes it.Machine-readable: OpenAPI 3.1 spec with full request and response schemas · an interactive reference you can fire calls from · the changelog (JSON).
Orders fill instantly at the real live price (multi-exchange feed - the same prices as our Paper Trade). API fills carry no slippage (fills on the website itself do), so treat entry prices as slightly optimistic.
Every position pays a taker fee on both sides, settled into pnl_usd at close. It is charged as a round trip: fee = qty × (entry + exit) × rate. Funding is accrued on positions held across funding marks and deducted the same way. Open positions already show P&L net of the round trip in unrealized_pnl_usd, so what you see is what settles.
| Market | Fee per side | Round trip on $10,000 notional |
|---|---|---|
| Crypto perps | 0.055% | $11.00 |
| Stocks & ETFs | 0.02% | $4.00 |
| Metals & indices | 0.015% | $3.00 |
| Forex majors | 0.008% | $1.60 |
Prefer the exact bill of the exchange you will run on? Charge the paper account at its schedule, referral discount included. Above roughly 180× leverage the rate tapers so the round trip can never exceed 20% of your margin. GET /v1/markets returns the exact rate and leverage cap for every symbol. Short-horizon strategies live or die on this number - a scalp that clears less than the round trip is a loser no matter how often it is right.
Isolated margin. Losses are capped at your margin. The liquidation price uses a 0.5% maintenance margin rate; when the live price crosses it the position settles at the liq price automatically. Stop-loss and take-profit orders execute automatically too - when the price crosses your sl or tp (checked against 1-minute candle wicks on the minute sweep, so a spike that retraced still counts), the position settles at that level. A trailing stop ratchets from the best price seen: on the sweep it is moved by each candle's extreme after that candle has been checked against the stop in force, so a bar can never stop you out at a level it created itself.
Every paper account starts at a nominal $10,000. /v1/account and /v1/balance report balance_usd, equity_usd and free_margin_usd so you can express results as a return instead of a bare dollar figure. This balance is a scorecard, not a constraint: margin is not debited from it and an open is never rejected for lack of funds (margin_enforced: false). Size your positions yourself if you want the constraint to be real.
Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (epoch seconds); a 429 adds Retry-After. Limits are counted per key, so a second key gets its own budget. X-RateLimit-Scope says whether the key's limit or the per-IP one applied.
Positions opened via the API live on your account server-side, so they show in My Trades, count for the season boards and XP, and feed your trading report - the same report GET /v1/report returns to the bot.
Putting it behind a paywall would push everyone else back onto polling, which costs us far more than the stream does - so the incentives line up: use the WebSocket, we both win. Market data stays keyless and free permanently; a key only raises its ceiling.
Two things in this engine are deliberately generous, and it is better to name them than to let you find out on a live account. A market order fills at the live price, so a new position reads 0.00 unrealized and the entry line sits exactly where you asked for it. And maintenance margin is a flat 0.5%, whatever the position is worth. For learning to trade, both are the right call. For proving out a strategy they are optimistic, and the bigger the size the more optimistic they get.
So they are switches, not assumptions. Off by default - nothing you have already built moves - and on, the fill price and the liquidation price behave the way a real book and a real risk-limit table make them behave.
GET /api/bot/v2/realism // your setting, the venue table and the whole tier ladder
POST /api/bot/v2/realism {"slippage": true, "margin_tiers": true, "margin_venue": "binance"}
# or per call, so one strategy can be tested both ways without touching the account
POST /api/bot/v1/open {"symbol":"BTC","side":"long","margin_usd":100,"leverage":10,
"slippage": true, "margin_tiers": true, "mmr_pct": 0.4}
The fill moves against you by what walking a real order book costs: 0.01% on the majors, 0.05% on thinner crypto, 0.005% on forex, 0.02% on stocks, metals and indices. A buy pays up, a sell sells down - never the other way.
Maintenance margin rises with notional size, the way an exchange risk-limit table does, so a large position is liquidated sooner - a higher maintenance requirement tolerates less adverse move before it closes you, which is exactly what a venue wants on size. This is our published model rather than any one venue's table, and GET /realism returns the whole ladder - you should never have to reverse-engineer it from a liquidation price.
Which venue's published maintenance-margin rate to build on: Binance 0.40%, Bybit 0.50%, MEXC 0.10%, Kraken 0.60%, Hyperliquid 1.25%, Coinbase 1.33%. It moves only where the liquidation price sits - never size, fees or P&L.
A position keeps the maintenance margin it was filled with, so turning this on never rewrites a trade you already hold, and every close records what it ran with. That is where the arena's Fills column comes from - a board that ranks bots ought to say which of them were on easy mode.
By default every paper fill pays the MarginPad rate: 0.055% per side on crypto, both legs, charged as a round trip at close. That is a fair average, but it is nobody's exact bill. If you already know where the bot will run for real, charge the paper account at that venue's schedule - its published taker rate, less the referral discount a MarginPad sign-up gets there - so the forward test settles the way the live account will:
GET /api/bot/v2/fees // the table below + your current default
POST /api/bot/v2/fees {"venue":"hyperliquid"} // account default: every open from now on, site and API
POST /api/bot/v2/open {"symbol":"BTC","side":"long","margin_usd":100,"leverage":10,"fee_venue":"bybit"} // just this one
POST /api/bot/v2/open {..., "dry_run":true} // -> fee_venue, taker_fee_pct, fee_vs_marginpad_default_usd
| Venue | Taker | Our referral discount | You pay per side |
|---|---|---|---|
| Bybit | 0.055% | 20% | 0.044% |
| Binance | 0.050% | 20% | 0.040% |
| Bitget | 0.060% | 20% | 0.048% |
| Gate | 0.050% | 20% | 0.040% |
| Hyperliquid | 0.045% | 4% (code MARGINPAD) | 0.0432% |
| OKX · Kraken | 0.050% | - | 0.050% |
| MEXC | 0.020% | - | 0.020% |
| KuCoin | 0.060% | - | 0.060% |
| MarginPad default | no venue chosen | 0.055% | |
Rates are the venues' published base tier and the discounts we state on /exchanges/; the API returns them with source so you know they are advertised numbers, not measurements. Both legs pay the taker rate (the paper engine fills at market; a maker rebate on a resting entry is not modelled). Crypto perps only - stocks, forex, metals and indices keep the MarginPad class rates whatever the venue. Open positions keep the rate they were filled with; the extreme-leverage cap (a round trip never above 20% of margin) still applies. The same choice is the "Fees as on" selector in the Paper Trade form, so a bot and its owner's manual trades pay identical fees.
Always send client_order_id on open. This is the single most important line in these docs. Without it, a request that times out on the network leaves you guessing: did the position open or not? Retry and you may have two. With it, the retry returns the position the first call created, flagged idempotent: true:
POST /api/bot/v2/open
{"symbol":"BTC","side":"long","margin_usd":100,"leverage":5,"client_order_id":"sig-2026-08-19-1403"}
// first call -> {"ok":true,"data":{"position":{"id":"bot...", ...}}}
// same call retried -> {"ok":true,"data":{"position":{"id":"bot...", ...},"idempotent":true}}
Use a value your strategy can regenerate deterministically - a signal id, or symbol plus candle timestamp. Ids are remembered for 7 days.
Closing takes one too. Send client_order_id on /close and a retry reports what the first call did (idempotent: true) instead of an error you have to interpret.
Send symbol when you close. Every /positions row already carries it, so this is free to add and it measurably shortens the close:
POST /api/bot/v2/close
{"id":"bot...","symbol":"BTC"} // one price fetch, one round trip
{"id":"bot..."} // server must first look up which symbol to price
Our trading store is a single region-pinned instance, so every extra hop costs what the round trip to it costs from where you are. Measured on 2026-08-19: about 20 ms from Europe but 198 ms median and 368 ms at the 95th percentile from Singapore. A cold price feed adds around 400 ms on top, and without the hint a close on a liquid pair could end up waiting on an illiquid one in the same batch. With the hint a close is one hop plus one warm price. A wrong or stale symbol costs nothing - the server notices and falls back.
Do not burn your rate budget on polling. Most bots spend the bulk of their calls asking "has anything changed". Two things make that nearly free:
# 1. state changes: send back the ETag, get 304 and an empty body when nothing moved
curl -H "X-API-Key: $KEY" -H 'If-None-Match: W/"1a2b3c-4"' \
https://marginpad.io/api/bot/v1/positions
# 2. live prices: /price is KEYLESS, so it costs nothing against your 120/min
curl https://marginpad.io/api/bot/v1/price?symbol=BTC
The ETag covers structural state - position ids, status, quantity, stop, target, exit - and deliberately not the mark price, which moves every tick and would make the ETag never match. So: poll /positions for "did my stop fire", poll the keyless /price for P&L. Watch X-RateLimit-Remaining and back off on Retry-After.
Positions and prices are pushed to you, so you find out your stop fired within a couple of seconds instead of on your next poll:
wss://marginpad.io/api/bot/v2/stream?api_key=mpb_…
<- {"type":"welcome","data":{"channels":["positions","prices"],"tick_ms":2000},"ts":…}
<- {"type":"snapshot","data":{"positions":[…]},"ts":…}
<- {"type":"position","data":{"event":"closed","position":{…,"pnl_usd":-1.24}},"ts":…}
<- {"type":"prices","data":{"BTC":64770.1},"ts":…}
-> {"op":"subscribe","channels":["positions"]} // prices off, events only
-> {"op":"ping"} // <- {"type":"pong"}
Events: position with event: opened | updated | closed fires when something actually changes - a fill, a stop moving, a close, a liquidation. prices carries the mark price of your open symbols each tick. Mark-price movement alone never raises a position event, so the channel stays quiet when nothing has happened.
Stops settle faster while you are connected. The stream drives the same server-side SL/TP/liquidation sweep the REST path uses, on its ~2s cadence, instead of leaving your position to the periodic sweep.
Up to 3 concurrent sockets per account, shared across your keys. Sockets are recycled after 6 hours - reconnect and you get a fresh snapshot. If the connection drops, treat the next snapshot as the truth rather than replaying missed events.
Measured before this existed: 89.5% of every call to this API was polling /positions and /account for events. A webhook turns that around - we POST to you when something actually happens, with the same Position shape a poll would have returned, and it works while your bot is offline, restarting or deployed somewhere that cannot hold a socket.
POST /api/bot/v2/webhooks
{"act":"add","url":"https://bot.example.com/marginpad","events":["position.closed","position.liquidated","order.filled"]}
-> {"ok":true,"data":{"webhook":{"id":"wh…","secret":"whs_…","events":[…],"active":true}}}
# what arrives at your URL, seconds after the event:
POST https://bot.example.com/marginpad
X-MP-Event: position.closed X-MP-Delivery: 1841
X-MP-Timestamp: 1789124656008 X-MP-Signature: sha256=3763dc5d…
{"event":"position.closed","ts":1789124656008,"hook_id":"wh…","data":{"id":"bot…","symbol":"BTC","status":"closed","pnl_usd":-0.08,"close_reason":"SL hit",…}}
Events: position.opened · position.updated (a stop or target moved) · position.closed · position.liquidated · order.filled (carries the position it created) · order.expired · order.cancelled. Omit events to receive all of them. Site-side trades on the same account fire them too.
Verify every delivery. X-MP-Signature is sha256=HMAC_SHA256(secret, raw body) with the secret returned when you added the hook. Compare it before you parse the body; both SDKs ship verify_webhook / verifyWebhook. Answer 2xx within 6 seconds - anything else is retried 5 times with backoff (30 s, 1, 2, 4 min), and a hook is paused after 25 consecutive failures; GET /webhooks shows active, consecutive_failures and last_error so you can see why. Three hooks per account on Pro, fifteen on Max, fifty on Business; deliveries are best-effort, the WebSocket snapshot and /positions remain the source of truth.
Try it before you write a receiver: register https://marginpad.io/api/whsink/<any-token-you-choose> as the URL, then open GET on that same address - it echoes the last 20 deliveries (headers and body) for 15 minutes. No auth; the token is the secret.
A book is a separate journal, balance, report and equity curve inside your account. Mint a key with a book name and every call made with that key trades that book; your main account and your other books never see it. Free accounts get one book besides the main account, API Pro five, Max twenty and Business fifty.
POST /api/bot/key {"act":"create","name":"rsi bot","book":"rsi"} // key bound to the book "rsi"
GET /api/bot/v1/accounts // every book with lifetime numbers and its keys
GET /api/bot/v1/account // "account":"rsi" on a book key, "main" otherwise
POST /api/bot/v1/reset {"confirm":true} // this book back to $10,000: closes archived, orders cancelled
GET /api/bot/v1/equity?days=30&step_min=60 // equity curve + max_drawdown_pct
Reset is refused with 409 open_positions_exist while anything is open: close first, then reset. The archived trades are kept (never deleted) but the report, the ledger, /trades and the equity curve start from the reset, and a browser that still holds the old journal cannot push it back. Webhooks fire on the account's hooks with "account":"rsi" in the payload, so one receiver serves every book.
Backtest with the code you will run live. Start a replay of one past UTC day and the same routes your bot already calls act on a separate replay journal, priced from MarginPad's own 1-minute candles at the cursor. Stops, targets and liquidations are checked on every candle between two of your calls, on the high and the low, with the same fee and funding math as live. Nothing from a replay reaches the boards, the arena or your report.
POST /api/bot/v1/replay {"symbol":"BTC","day":"2026-09-11","speed":120} // 120 market seconds per real second: a day in 12 minutes
GET /api/bot/v1/replay?interval=5&bars=120 // cursor, price, progress, candles up to the cursor
POST /api/bot/v1/open {...} // acts on the replay book at the cursor price while the replay runs
POST /api/bot/v1/replay {"act":"stop"} // close at the cursor, get the summary, empty the replay journal
Speed goes from 1 (real time) to 600 (a day in 2.4 minutes). One replay per key at a time, crypto only, market orders only for now: read the replay price and candles from GET /v1/replay, since the keyless /v1/price and /v1/klines keep answering live data.
You can hold several keys on one account - keep a backtest bot and a live bot apart, and revoke one without touching the other. Usage and rate limits are counted per key, so each gets its own budget and its own line in your stats. Signed in on the site, from the browser:
POST /api/bot/key {"act":"list"} // all your keys
POST /api/bot/key {"act":"create","name":"backtest-bot"} // new key
POST /api/bot/key {"act":"rename","key":"mpb_...","name":"live-bot"}
POST /api/bot/key {"act":"revoke","key":"mpb_..."} // usage history is kept
A revoked key returns 401 revoked_key. Keys are secrets - anyone holding one can trade your paper account. Create and revoke them at the top of this page.
Every account or book with at least five closes opened through the API this 14-day season is ranked on /arena/ by realized P&L net of fees and funding, with win rate, return on the $10,000 scorecard, average ROE and liquidations. No prizes and nothing to sign up for: trade through the API and the board picks you up. JSON: GET /api/arena.
MarginPad runs a remote MCP server, so an AI assistant can read markets and trade your paper account directly - no glue code:
https://marginpad.io/mcp
Add it as a remote MCP server in your client and set the header X-API-Key: mpb_… if you want the paper-trading tools. Market-data tools need no key. Twenty-seven tools are exposed: prices, candles, markets, screener, funding, open interest, liquidations, fear and greed, the economic calendar, two calculators, and paper open (with trailing stops and dry run) / limit and stop orders / modify / cancel / close / sltp / positions / orders / balance / trades / report / fees / accounts / reset / equity / replay. GET /mcp in a browser returns the server descriptor.
/api/bot/v1/* is frozen. Its response bodies will not change; bots written against it keep working indefinitely, and it still receives correctness fixes.
/api/bot/v2/* is the same API with the envelope our data endpoints already use - one predictable shape for success and failure, so you write the parsing once:
{ "ok": true, "data": { ... }, "ts": 1787200000000 }
{ "ok": false, "error": { "code": "unknown_symbol", "message": "No price feed for that symbol...", "symbol": "FOO" }, "ts": 1787200000000 }
Same paths, same parameters, same key - swap v1 for v2 in the URL. Error code values are stable identifiers; message is for humans and may be reworded.
On /v2, failures are always {"ok":false,"error":{"code","message"},"ts"}. The code is a stable identifier you can branch on; the message is prose and may be reworded. Some errors carry the value you need to correct the call - sl_wrong_side and tp_wrong_side include the live price.
| Code | HTTP | What to do |
|---|---|---|
missing_api_key / invalid_api_key | 401 | Send a valid key in X-API-Key. |
revoked_key | 401 | Create a new key; this one was revoked. |
rate_limit | 429 | Sleep until Retry-After. Do not hot-retry. |
unknown_symbol | 404 | Check /v1/markets. |
symbol_required, margin_usd_min_1, margin_usd_max_100000, id_required | 400 | Fix the request body. |
sl_wrong_side / tp_wrong_side | 400 | The response carries live - put the level on the correct side of it. |
stop_wrong_side | 400 | A stop entry waits on the breakout side (long above, short below). Use type:"limit" for a pullback entry. |
trail_pct_invalid | 400 | trail_pct is a percent between 0.05 and 50; it is refused, never silently clamped. |
nothing_to_modify | 400 | /modify_order needs at least one field. |
plan_required | 402 | Webhooks and /v1/ai need a paid plan. Everything else on this page is on Free. The body carries plan_needed. |
premium_required | 402 | Legacy code for plan_required, still sent so a 2.3 bot recognises it. |
too_many_webhooks | 409 | Delete one first - three hooks on Pro, fifteen on Max, fifty on Business. |
bad_url / bad_event | 400 | Webhook URLs are https on a public host; event names are listed in GET /webhooks. |
ai_quota | 429 | The daily AI quota (shared with the site) is used. Resets 00:00 UTC. |
too_many_open | 409 | You are at your plan's open-position ceiling. Close something. |
already_closed | 400 | Harmless on a retry - the position is already settled. |
no_price | 400 | No live price this instant; the position stays open. Retry. |
unavailable | 503 | Transient. Retry with backoff. |
Real-time liquidations aggregated from Binance, Bybit, OKX, BitMEX, Hyperliquid and Bitfinex - the data behind our Rekt feed and Liquidation Heatmap, exposed as free JSON. Comparable data is normally paywalled at $300+/month elsewhere; here it needs no API key at all. Edge-cached and rate-limited, so it comfortably handles polling bots.
{events:[{ts, exchange, symbol, side, price, qty, notional}]}. Events land here seconds after they happen (3s edge cache). Poll every 3-5s.side is long_liquidated or short_liquidated, notional is the USD size.minutes: up to 43200 (30 days).{clusters:[{price, side, est_notional}]}. Refreshes continuously from live order flow.Attribution appreciated (link marginpad.io) but not required. Coverage is ~70%+ of the market's liquidation flow - the venues that move price. Educational use; no uptime SLA. Just want market data? The whole keyless surface is documented on the free crypto data API page.
Most crypto trading bots that look profitable in a backtest lose money the first week they go live. Backtests replay clean historical candles - they cannot catch a race condition in your order logic, a position-sizing bug that only shows up after a losing streak, or leverage that quietly grows past what your margin survives. Forward testing runs the same bot, the same code path, against real live prices in real time. If the strategy has a flaw, it shows up here, where a bug costs you nothing.
| Backtesting | Paper trading (this API) | Live trading | |
|---|---|---|---|
| Prices | Historical candles | Real live prices, real time | Real live prices |
| Risk | None | None - simulated dollars | Real money at stake |
| Catches logic bugs | Rarely - replayed data hides them | Yes - same code path as live | Yes, but each bug costs money |
| Overfitting check | No - you tuned on this data | Yes - unseen future data | Yes |
| Cost | Free | Free to use - fees & funding simulated | Fees + losses + funding |
| Best for | Filtering ideas fast | Proving the bot before funding it | A strategy already proven twice |
The middle step is the one most people skip - and the one that would have saved them. Backtest an idea here, forward-test it on this API, then fund an account.
Everything on this page settles in simulated dollars. Once the bot has survived weeks of forward testing, the same code needs an exchange API to place real orders. Two venues we point bot builders at, for different reasons:
Perps on their own chain with a public REST and WebSocket API, sub-accounts that each get their own API wallet (the bot never holds your main key), hourly funding and a book anyone can audit on-chain. The closest thing to running against this API with real money.
REST and WebSocket shapes close to what you just wrote, a separate key per sub-account, and one of the feeds our own prices come from. The default when you want a centralised book with fiat on-ramps.
Open a Bybit account →Yes - MarginPad exposes real-time liquidations aggregated from 9 exchanges as free JSON, no key needed: /api/v1/feed, /api/v1/liquidations/live, /api/v1/liquidations/recent and /api/v1/clusters. Comparable feeds are usually $300+/month. See the liquidation data section.
Yes. It is a paper-trading simulator - positions use real live market prices but no real money. Sign in with an email, generate an API key and start testing immediately. Paid plans only raise the ceilings.
Any language that can make an HTTPS request - Python, JavaScript/Node.js, Go, Rust, PHP, or plain curl. Standard REST, JSON responses, one X-API-Key header.
Any USDT pair with a live price on major exchanges - BTC, ETH, SOL and hundreds of altcoins, plus US stocks, forex majors, metals and indices as perps. Check a symbol with GET /api/bot/v1/price?symbol=SOL.
No. No deposit, no wallet, no KYC. You only sign in with an email so your API key and positions persist across sessions.
No. The Bot API has its own plans and a Premium subscription does not change any API limit. Premium unlocks features on the website; the API plans unlock API limits. They are bought separately.
Build a working bot in Python and point it at this API - full code: price feed, EMA-cross strategy, open/close orders and the loop.
The same bot in JavaScript - full Node.js code with zero dependencies, pointed at this API.
The three-stage workflow that separates working bots from blown accounts: backtest, paper trade, then go live.
How grid bots work, where they win and blow up, and how to forward-test one before funding it.
What each testing stage proves, and the exact order to use them.
The AI-assisted strategies that actually hold up - and how to test them risk-free before automating.