Run your strategy on your own data and infrastructure. Send Candella one signed signal per trade and it becomes a real order across your followers' connected brokerage accounts, with manual approval and the risk caps they set. Equities and options today, with crypto on the way. It is the fast alternative to wiring your strategy into a brokerage API like Interactive Brokers.
cc.start_session() # arms live execution
webull.place_order("SPY", "buy", 10) # your execution, your account
cc.signal("SPY", "buy", 10, price=512.30) # the copy signal, one lineThe signal API is a plain HMAC-signed HTTP request, so you can integrate from any language right now. The Python SDK is available today. Native TypeScript, Go, Java, C++, and Rust SDKs are on the way.
Send your algo's decisions to Candella with one signed request per trade. Install the Python SDK, or call the signed HTTP API below from any language.
pip install candellafrom candella import Copytrade
cc = Copytrade(api_key="...", secret="...", strategy_id="my-algo")
cc.start_session() # arms live execution at market open
cc.signal("SPY", "buy", 10, price=512.30) # copies to your followers
cc.signal_option("SPY", "BTO", 5, price=3.10,
expiry="2026-07-18", strike=520, right="C")
cc.end_session() # when your algo sleepsGenerate an API key and secret on your Algo keys page. Three values identify and sign every request.
| Value | Purpose |
|---|---|
| api_key | Public key. Sent as the X-Candella-Key header. Identifies your account. |
| secret | Signs every request with HMAC-SHA256. Shown once at creation; store it safely, it cannot be retrieved again. |
| strategy_id | Names this strategy. Sent as strategyId in the body. |
Signals only reach real accounts while you have an active session. Open one with start_session() when your algo wakes and close it with end_session() when it sleeps. A signal sent with no active session is authenticated, validated, and recorded, then dropped before any order is placed. That is how you integration-test safely: send signals without opening a session and nothing hits a real account.
A single endpoint accepts every signal.
POST https://candella.dev/api/ingest/algo-signal| Field | Type | Required | Notes |
|---|---|---|---|
| strategyId | string | yes | Your strategy id. |
| signalId | string | yes | Stable, deterministic id (your order id or a content hash), never random. Powers idempotency. |
| symbol | string | yes | Ticker for a stock, or the underlying for an option. |
| assetType | "stock" | "option" | yes | Crypto is on the way. |
| side | "BUY" | "SELL" | "BTO" | "STC" | yes | Open or close, equity or option. |
| quantity | number > 0 | yes | Shares for a stock, contracts for an option. |
| price | number > 0 | yes | Used for position sizing and the slippage guard. |
| emittedAt | string | yes | ISO-8601 timestamp of when your algo fired. |
| optionLeg | object | for options | Required when assetType is "option": { underlying, expiry "YYYY-MM-DD", strike, right "C" | "P" }. |
| bookSizeUSD | number > 0 | no | Virtual book size for a signal-only strategy with no funded account to size against. |
| Header | Value |
|---|---|
| Content-Type | application/json |
| X-Candella-Key | Your api_key. |
| X-Candella-Timestamp | Current epoch milliseconds, as a string. |
| X-Candella-Signature | Lowercase hex HMAC-SHA256. See Signing. |
| Status | Meaning |
|---|---|
| 202 accepted | Accepted. Fans out to your followers if a session is active; with no session it is recorded and dropped at the consent gate, no order placed. |
| 202 duplicate | Idempotent re-send (same strategyId and signalId already seen). No second order. |
| 400 invalid_signal | The body failed validation. The detail field lists the offending fields. |
| 400 invalid_json | The body was not valid JSON. |
| 401 unauthorized | Signature or timestamp check failed. detail is one of: missing_fields, bad_timestamp, stale, bad_signature. |
| 401 missing_key / unknown_key | No API key, or a key that did not resolve to an account. |
| 422 guard_blocked | Blocked by a notional or contract-size limit. detail carries the reason. |
| 429 rate_limited | Too many signals in a short window. Retry after the seconds in detail. |
Every request is signed. The Python SDK does this for you; here is the exact recipe for any other language.
Requests more than five minutes from server time in either direction are rejected as stale, so sign and send promptly rather than pre-computing signatures.
SECRET='your-secret'
KEY='your-api-key'
# Compact JSON, keys sorted. The identical bytes are signed and sent.
BODY='{"assetType":"stock","emittedAt":"2026-06-26T14:30:00+00:00","price":512.30,"quantity":10,"side":"BUY","signalId":"order-12345","strategyId":"my-algo","symbol":"SPY"}'
TS=$(python3 -c 'import time; print(int(time.time()*1000))')
SIG=$(printf '%s.%s' "$TS" "$BODY" \
| openssl dgst -sha256 -hmac "$SECRET" -hex \
| sed 's/^.*= //')
curl -sS https://candella.dev/api/ingest/algo-signal \
-H "Content-Type: application/json" \
-H "X-Candella-Key: $KEY" \
-H "X-Candella-Timestamp: $TS" \
-H "X-Candella-Signature: $SIG" \
--data "$BODY"Re-sending a signal must never double-fire a real order. Pass a stable, deterministic signalId (your algo's own order id, or a content hash of the signal), never a random one. Candella keys the order off your strategyId and signalId, so an identical re-send collapses to a no-op instead of a second order. The Python SDK derives one for you if you omit it; a raw HTTP integrator must supply it.