Create a trading stream token
Returns a short-lived token for the real-time trading stream of all your organization’s trading accounts: every order, execution, position change, balance/equity update and rule violation, as it happens. New accounts are included automatically.
The stream is read-only. It accepts no trading commands.
Connect to the returned url with the token in the Authorization header:
TOKEN=$(curl -s -X POST "https://api.hyperprop.com/platform/v1/organization/trading-accounts/stream-token" \
-H "X-API-Key: hp_live_your_key_here" | jq -r .data.token)
websocat -H "Authorization: Bearer $TOKEN" wss://api.hyperprop.com/trade/org/stream
The first message is {"type": "Welcome", "organization_id": ..., "expires_at": ...}. Then each event arrives as one JSON message with a type: OrderSubmitted, OrderFilled (one per execution, with the trade), OrderCancelled, PositionUpdate, AccountUpdate (balance, equity, P&L, drawdown, status), RuleViolation and account Notification. Every event carries account_id. A Heartbeat arrives every 15 seconds.
Options (optional query parameters, combinable): snapshot=1 sends the current state of your accounts (AccountSnapshot messages, then SnapshotComplete) right after Welcome; seq=1 numbers every message; floors=1 adds RiskLimitsUpdate messages; v=2 selects message format 2, where every message carries schema and ts_ms and every position carries position_id and state_epoch. Without them every message is exactly as described above.
Filter (optional): add query parameters to the stream URL to receive only some of your accounts. Other accounts’ events are dropped on the server and never reach your connection.
account_types: comma-separatedevaluation,sim_funded,competition, for examplewss://api.hyperprop.com/trade/org/stream?account_types=sim_funded.account_ids: comma-separated account IDs, at most 100 per connection.
With both, an account must match both. Every event type is filtered the same way, including position closes from liquidation, administrative closes and recoveries, and RiskLimitsUpdate. An invalid filter is refused with HTTP 400 (INVALID_FILTER). Without a filter you receive every account, as before. The filter is fixed for the connection; to change it, reconnect.
On a filtered connection, Welcome includes the active filter, snapshot=1 sends only the accounts in the filter (flat ones included), and you are told when an account’s membership changes:
{"type": "AccountJoined", "account_id": ..., "account_type": "sim_funded", "reason": "new_account" | "account_type_changed", "as_of": ..., "positions_status": "ok", ...}when an account enters the filter (for example a newly created sim-funded account). It carries the account’s current state with the same fields as one account inAccountSnapshot:positions_versionandpositions(an empty list means flat), withrisk_limitswhen you usefloors=1andstate_epochandposition_idwhen you usev=2. If the state cannot be read at that moment,positions_statusis"unavailable"andpositionsisnull: read the account’s positions before relying on its position state.{"type": "AccountLeft", "account_id": ..., "account_type": ..., "reason": "account_type_changed", "as_of": ...}when an account’s type no longer matches. No further events are sent for it.
AccountJoined and AccountLeft are ordinary messages: with seq=1 they are numbered with the others, and with v=2 they carry schema and ts_ms. An account that fails or passes keeps its type, so its closing events still arrive. Filtered connections count toward the same connection limit.
Token lifetime: 15 minutes. Before it expires, request a new token and send {"action": "refresh", "token": "<new token>"} on the open connection (reply TokenRefreshed). Otherwise the connection closes with code 4001 and you reconnect with a new token. A revoked API key can’t get new tokens, so its connections end within one token lifetime.
Missed events: the stream does not replay events. If your connection falls behind, you get {"type": "StreamGap", "dropped_events": n}. After a StreamGap or a reconnect, reconnect with snapshot=1 or read current state with GET /organization/trading-accounts/positions; with perAccountResults=true it returns each account’s position book with stateEpoch and stateVersion to reconcile with PositionUpdate (v=2). A client that stops reading is disconnected (code 4008).
Limits: 5 open connections per organization per server; more are refused with HTTP 429. One connection carries all your accounts, so one or two are enough. Send {"action": "ping"} for a Pong.
Requires view permission on traders.
Access: read-only and full-access API keys. Dashboard members need view access to Traders & accounts.
curl --request POST \
--url https://api.hyperprop.com/platform/v1/organization/trading-accounts/stream-token \
--header 'X-API-Key: <api-key>'import requests
url = "https://api.hyperprop.com/platform/v1/organization/trading-accounts/stream-token"
headers = {"X-API-Key": "<api-key>"}
response = requests.post(url, headers=headers)
print(response.text)const options = {method: 'POST', headers: {'X-API-Key': '<api-key>'}};
fetch('https://api.hyperprop.com/platform/v1/organization/trading-accounts/stream-token', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.hyperprop.com/platform/v1/organization/trading-accounts/stream-token",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_HTTPHEADER => [
"X-API-Key: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://api.hyperprop.com/platform/v1/organization/trading-accounts/stream-token"
req, _ := http.NewRequest("POST", url, nil)
req.Header.Add("X-API-Key", "<api-key>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.hyperprop.com/platform/v1/organization/trading-accounts/stream-token")
.header("X-API-Key", "<api-key>")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.hyperprop.com/platform/v1/organization/trading-accounts/stream-token")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["X-API-Key"] = '<api-key>'
response = http.request(request)
puts response.read_body{
"success": true,
"data": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzY29wZSI6Im9yZ19zdHJlYW0ifQ.c2lnbmF0dXJl",
"expiresIn": 900,
"expiresAt": "2026-10-25T16:15:00.000Z",
"url": "wss://api.hyperprop.com/trade/org/stream"
}
}{
"success": false,
"statusCode": 401,
"error": "Unauthorized",
"message": "Authentication required",
"code": "UNAUTHORIZED"
}{
"success": false,
"statusCode": 403,
"error": "Forbidden",
"message": "This API key does not have admin permissions",
"code": "INSUFFICIENT_PERMISSIONS"
}{
"success": false,
"statusCode": 409,
"error": "Conflict",
"message": "This Idempotency-Key was already used with a different request body. Use a new key for a different request.",
"code": "IDEMPOTENCY_KEY_REUSED"
}{
"success": false,
"statusCode": 503,
"error": "Error",
"message": "The trading stream is not available right now",
"code": "STREAM_UNAVAILABLE"
}Authorizations
Organization API key (hp_live_...), sent in the X-API-Key header. Create, rotate and restrict keys in the dashboard or with the API keys endpoints; an organization can have two active keys, so you can rotate without downtime. A full-access key can call every endpoint in this reference. A read-only key can call GET endpoints (and create a trading stream token); any other request returns 403 READ_ONLY_KEY. A key with an IP allowlist returns 403 IP_NOT_ALLOWED from any other address. Each endpoint lists which keys can call it under Access.
Headers
Optional. A value that identifies this operation on your side, at most 255 characters. A retry with the same key, method, path and body within 24 hours returns the original response (with Idempotency-Replayed: true) instead of repeating the change. The same key with a different body returns 409 IDEMPOTENCY_KEY_REUSED; a retry while the first request is still running returns 409 IDEMPOTENCY_KEY_IN_PROGRESS. Server errors (5xx) are not saved, so a retry runs again.
255curl --request POST \
--url https://api.hyperprop.com/platform/v1/organization/trading-accounts/stream-token \
--header 'X-API-Key: <api-key>'import requests
url = "https://api.hyperprop.com/platform/v1/organization/trading-accounts/stream-token"
headers = {"X-API-Key": "<api-key>"}
response = requests.post(url, headers=headers)
print(response.text)const options = {method: 'POST', headers: {'X-API-Key': '<api-key>'}};
fetch('https://api.hyperprop.com/platform/v1/organization/trading-accounts/stream-token', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.hyperprop.com/platform/v1/organization/trading-accounts/stream-token",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_HTTPHEADER => [
"X-API-Key: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://api.hyperprop.com/platform/v1/organization/trading-accounts/stream-token"
req, _ := http.NewRequest("POST", url, nil)
req.Header.Add("X-API-Key", "<api-key>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.hyperprop.com/platform/v1/organization/trading-accounts/stream-token")
.header("X-API-Key", "<api-key>")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.hyperprop.com/platform/v1/organization/trading-accounts/stream-token")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["X-API-Key"] = '<api-key>'
response = http.request(request)
puts response.read_body{
"success": true,
"data": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzY29wZSI6Im9yZ19zdHJlYW0ifQ.c2lnbmF0dXJl",
"expiresIn": 900,
"expiresAt": "2026-10-25T16:15:00.000Z",
"url": "wss://api.hyperprop.com/trade/org/stream"
}
}{
"success": false,
"statusCode": 401,
"error": "Unauthorized",
"message": "Authentication required",
"code": "UNAUTHORIZED"
}{
"success": false,
"statusCode": 403,
"error": "Forbidden",
"message": "This API key does not have admin permissions",
"code": "INSUFFICIENT_PERMISSIONS"
}{
"success": false,
"statusCode": 409,
"error": "Conflict",
"message": "This Idempotency-Key was already used with a different request body. Use a new key for a different request.",
"code": "IDEMPOTENCY_KEY_REUSED"
}{
"success": false,
"statusCode": 503,
"error": "Error",
"message": "The trading stream is not available right now",
"code": "STREAM_UNAVAILABLE"
}