A Python crypto trading bot API integration on a prop account differs from one on an exchange in a way that matters: every order your script sends is checked against the account’s rules before it fills. This tutorial builds a small bot on the QATS Trader API with the requests library, using the endpoint paths and headers as the API reference documents them.
You will create a sandbox key, read the account, check an instrument, place an order safely, handle refusals and timeouts, then move to a paid evaluation, which on QATS is simulated.
Python crypto trading bot API basics: what the QATS Trader API is
The Trader API is a REST interface, version 1, for a trader’s own accounts: market data, account state, orders, positions, fills and a durable event stream. It uses the terminal’s execution engine, the accepted plan rules and the same risk controls, and it has no access to payouts, billing or personal data.
It is not a Binance or Bybit clone: exchange SDKs built around signed API-key headers are not drop-in compatible, because QATS uses a Bearer token. prop.qats.us/developers also publishes an OpenAPI description and a Postman collection.
Prop firm API policies vary widely. In our comparison, checked 2026-10-29, HyroTrader lists “Allowed via API; HFT and copy trading banned”, Topstep “API bots allowed via TopstepX (not on Live Funded)” and BitFunded “Banned”.
Step 1: Create a sandbox account and key
- Sign up and verify your email; the sandbox needs no challenge purchase.
- Open Sandbox account → Keys & connections → Trading API, pick Crypto or TradFi and a Binance or Bybit price venue, and create it.
- Choose Create sandbox key, name it after the bot and pick Read and trade; you can restrict it to your server’s IP address.
- Save the key once in a secret store. It cannot be shown again and never belongs in code, a URL or a log.
| Environment | Base URL | Key prefix |
|---|---|---|
| Sandbox (virtual funds) | https://prop.qats.us/api/sandbox/v1 | qats_sandbox_ |
| Purchased accounts | https://prop.qats.us/api/v1 | qats_trade_ |
Step 2: Authenticate and read the account first
Every request carries the key as a Bearer token. Start with a read, and check that the response names the sandbox before anything trades:
import os, uuid, requests
BASE = "https://prop.qats.us/api/sandbox/v1"
H = {"Authorization": "Bearer " + os.environ["QATS_TOKEN"]} # qats_sandbox_… key
acct = requests.get(BASE + "/trading/account", headers=H, timeout=20).json()
print(acct["environment"], acct["risk"]["riskSummary"]["lossBuffer"])
The response carries balance, equity, the accepted rules, the risk session and the key’s permissions; risk.riskSummary holds lossBuffer (equity minus the loss floor) and dailyLossRemaining. Read limits and request budgets (capabilities.rateLimits) from here instead of hard-coding them.
Step 3: Check the instrument and size the trade yourself
spec = requests.get(BASE + "/trading/instruments", params={"symbol": "BTCUSDT"}, headers=H, timeout=20).json()["instruments"][0]
With a symbol, /trading/instruments returns the tick size, quantity step, minimum quantity and lot size, plus orderCapacity, the largest order each side could take now. Round sizes to quantityStep; invalid increments are refused.
There is no per-trade loss limit, and an entry is admitted without a stop. Size each trade from your risk per trade and stop distance; orderCapacity is a margin ceiling, not a target.
Step 4: Place an order with an Idempotency-Key
Every write needs an Idempotency-Key header, your own unique ID for that command, saved before you send it. A write without one is refused with IDEMPOTENCY_KEY_REQUIRED.
cid = str(uuid.uuid4()) # store it before sending
order = {"symbol": "BTCUSDT", "side": "BUY", "type": "MARKET", "quantity": 0.001, "bracket": {"enabled": True, "stopTicks": 100, "targetTicks": 200}}
r = requests.post(BASE + "/trading/orders", json=order, headers={**H, "Idempotency-Key": cid}, timeout=20)
A 201 means filled at once, 202 resting. The bracket attaches a stop and target in whole ticks; the values here are illustrative, so work out your own from the stop distance and tickSize. POST /trading/positions/{id}/protection sets or moves them later with stopPrice and targetPrice; /trading/positions/{id}/close and /trading/flatten exit.
Add referencePrice, the price your signal saw, to a market order and the fill is refused with PRICE_REFERENCE_MISMATCH beyond maxDeviationPct, 0.1% by default.
Step 5: Handle refusals, rate limits and timeouts
Errors share one shape: error.code, error.message and optional error.details. Branch on error.code, never on the message text, and never resend a refused order in a loop.
After a timeout, call GET /trading/commands/{id} with the original ID and the same key: confirmed returns the recorded result, and unconfirmed is not proof of failure. Never retry an uncertain trade under a fresh ID; an exact retry replays the recorded result instead of trading twice.
Rate limits count every entry that reaches the quota check, even refused ones, while exits have a separate budget. Honour Retry-After and back off.
| Response | Meaning | What the bot should do |
|---|---|---|
| 422 DAILY_LOSS_LIMIT, PERSONAL_DAILY_STOP | A loss rule blocks new entries | Stop opening trades and read access.tradingAvailability |
| 409 PRICE_REFERENCE_MISMATCH | The market moved past your tolerance | Re-read the quote and decide again with a fresh price |
| 409 WORKING_ORDER_LIMIT | 50 orders already rest on the account | Cancel orders before placing more |
| 422 INSUFFICIENT_MARGIN | Not enough free margin for the order | Reduce size or close exposure |
| 429 TRADING_RATE_LIMITED | The request budget is used up | Wait the Retry-After seconds, then back off |
| 503 STALE_MARKET_DATA | The quote was too old to fill against | Retry the same command after a moment |
| Timeout or lost response | The outcome is unknown | GET /trading/commands/{id} with the same key and ID |
Step 6: Follow fills with the event stream
GET /trading/events/stream is a server-sent events stream of order, position and execution events. Save the last event ID only after handling the event, reconnect with Last-Event-ID, and keep handlers idempotent, since a crash can redeliver an event. A 2xx response proves acceptance, not a full fill.
Prop firm API rules: the risk checks still apply
API orders are not a side door. Admission checks the maximum loss, the daily loss limit, your personal daily stop, margin, the price range and quote age exactly as for a manual order. On a QATS Crypto evaluation the rules are a 10% profit target, a 4% daily loss limit, a 6% end-of-day trailing maximum loss that stops trailing at the starting balance, a 40% consistency rule and 2 minimum trading days, with up to 20× on BTC and ETH, 10× on large caps and 5× on other coins.
Opposite positions on the same market across your own accounts are refused with CROSS_ACCOUNT_HEDGE, and each key belongs to one account. Your own limits (a daily stop, a maximum margin, a maximum number of trades per day) are extra restrictions on top of the plan rules, never a replacement, and the server enforces them.
Moving from the sandbox to an evaluation
Evaluations and initial funded accounts are simulated; payouts are real payments, subject to the plan’s eligibility rules and identity verification.
- Create a purchased-account key (qats_trade_…) for the exact evaluation and switch BASE to https://prop.qats.us/api/v1.
- Read the account again and check its ID, rules and execution mode.
- Start small and compare each fill with the trade journal.
- Keep the sandbox for testing changes; its trades never qualify a challenge or earn payouts.
Frequently asked questions
01Does QATS have a Python SDK?
The API reference shows Python examples with the requests library and publishes an OpenAPI description and a Postman collection. The downloadable helper client is JavaScript; in Python, plain HTTP requests are enough.
02Can I use a Binance or Bybit library with the QATS API?
Not directly. QATS REST v1 uses Bearer tokens and its own paths, not exchange API-key headers or signed requests, so exchange SDKs are not drop-in compatible.
03Do API orders follow the same rules as manual trades?
Yes. API orders go through the terminal’s execution engine and the same server-side checks, including daily loss, maximum loss, margin and price checks.
04Can I test a bot without buying a challenge?
Yes. Any registered trader with a verified email can create a free sandbox with virtual funds and sandbox keys. Sandbox results do not qualify a challenge or earn payouts.
Educational content, not investment advice. QATS evaluations and initial funded accounts are simulated; payouts depend on eligibility, verification and the applicable agreement.