Register a webhook endpoint
Register a URL to receive real-time event notifications via HTTP POST.
How it works:
- You provide an HTTPS URL and choose which events to subscribe to
- Hyperprop returns a signing secret (shown once — save it)
- When events occur, we POST a JSON payload to your URL with an HMAC-SHA256 signature
- Verify the signature using the secret to ensure the payload is authentic
Available events:
| Event | Description |
|---|---|
account.created | A new trading account was provisioned |
account.status_changed | Account status changed (not_started → in_progress → passed/failed/expired) |
account.updated | Account fields updated (balance, metadata, HWM) |
account.imported | A trader redeemed an import key inside the Hyperprop app — the account now has its owner. Payload carries an import block: { importKey, importedUserId, importedEmail, importedAt } so you can verify who redeemed which key and when. Billing + market data start at this moment. Store import.importedEmail on your customer’s profile — that login is now soulbound to the account’s customerId and all the customer’s future imports must use it (see GET /trader-bindings). |
account.unlinked | You unlinked a not-yet-started account via POST /trading-accounts//unlink. Payload carries an import block: { voidedImportKey, newImportKey, unlinkedUserId, unlinkedEmail } — the fresh key is ready to hand out. Note: the customer’s soulbound binding survives unlink — if the customer needs a new Hyperprop login, rebind them via POST /trader-bindings//rebind. |
account.passed | Account passed — profit target reached or admin update (convenience — also fires account.status_changed) |
account.failed | Account failed — live rule violation (max loss, daily loss, drawdown…) or admin update (convenience — also fires account.status_changed) |
account.lockout_created | Trading lockout applied to an account |
account.lockout_removed | Trading lockout removed from an account |
account.lockout_updated | Lockout details changed (reason, end time) |
account.payout_processed | A payout (balance withdrawal) was executed on an account |
account.max_payouts_reached | The account reached its rule’s maxPayouts with this payout — your “move trader to live” trigger |
purchase.created | A new purchase was recorded for the organization |
purchase.status_changed | Purchase status changed (pending → paid, paid → refunded/cancelled) |
trader.created | A new trader was associated with the organization |
snapshot.ready | End-of-day snapshots for a trading day are computed and queryable — fire your EOD import off this instead of guessing a time |
fill.created | An order executed quantity — fires once when the order reaches a terminal state: fully filled, or cancelled/rejected/expired after a partial execution. Near-real-time intraday fill stream (explicit subscription only, see below) |
| account.balance_changed | The account’s balance moved. One event per change with previousBalance, currentBalance, change, reason (fill · payout · revert · adjustment), orderId / payoutId where applicable and occurredAt. Same id and values on the WebSocket frame and the ?since= replay, so you can dedupe across channels on id. Explicit subscription only (see below) |
Use ["*"] to subscribe to all events (including future event types).
Exception: fill.created and account.balance_changed are high-volume and are never delivered through a ["*"] wildcard — an endpoint must list them explicitly in its events array.
account.balance_changed is the one event to follow if you keep your own copy of balances: every fill, payout, adjustment and trading-day revert produces exactly one. Payload:
{
"id": "evt_9f1c...",
"type": "account.balance_changed",
"createdAt": "2026-09-09T20:14:08.412Z",
"organizationId": "a6fcc0ce-...",
"data": {
"previousBalance": 25434.67,
"currentBalance": 25806.63,
"change": 371.96,
"reason": "fill",
"orderId": "efd59f41-09c5-4bf4-9378-78f70f115a16",
"payoutId": null,
"occurredAt": "2026-09-09T20:14:08.107Z",
"account": { "id": "ddf46c59-...", "accountNumber": "MFFUEV...", "customerId": "1e38d177-...", "status": "in_progress", "currentBalance": 25806.63 },
"trader": { "email": "trader@example.com", "name": "…", "externalUserId": null, "metadata": {} }
},
"previousAttributes": { "currentBalance": 25434.67 }
}
Like fills, balance webhooks are only recorded while an active endpoint lists account.balance_changed; the WebSocket stream delivers and replays them regardless. Reconcile against GET /platform/v1/organization/balances on your own schedule.
Fill webhooks are not recorded until you subscribe. Because a wildcard
cannot opt you into fills, fill webhook deliveries are only written for
organizations that have an active endpoint explicitly listing fill.created.
Adding it starts webhook delivery within seconds, but fills that occurred
before you subscribed are not re-delivered — webhooks are
forward-looking. This gate applies to the webhook channel only: the WebSocket
event stream delivers and replays fills regardless of webhook configuration.
For history, read fills from
GET /platform/v1/organization/reconciliation for any past day.
snapshot.ready: fires once per organization per trading day, right after the trade engine writes the EOD batch (shortly after CME close, 16:00 CT). Payload:
{
"id": "evt_2c1d...",
"type": "snapshot.ready",
"organizationId": "a6fcc0ce-...",
"data": {
"tradingDay": "2026-07-22",
"accountsSnapshotted": 142,
"snapshotsUrl": "/platform/v1/organization/snapshots/accounts?tradingDay=2026-07-22",
"fillsUrl": "/platform/v1/organization/snapshots/fills?tradingDay=2026-07-22"
}
}
On receipt, call GET /snapshots/accounts?tradingDay=... — the data is guaranteed to be there. If you prefer polling, the snapshots endpoint also returns latestCompletedTradingDay.
fill.created: fires within seconds of an order reaching a terminal state with executed quantity — a complete fill (status: "filled"), or an order that partially executed and was then cancelled, rejected, or expired. Exactly one event per executed order, carrying the final totals: filledQuantity is the cumulative executed quantity (may be less than quantity for a final partial), filledPrice is the volume-weighted average across all executions of the order, and fee/commission/realizedPnl are the accumulated totals. Check status to distinguish a final partial from a complete fill; dedupe on orderId. (In the rare case an order recorded as cancelled is later corrected to filled by the engine, a second event with the same orderId and updated totals is emitted — treat the latest as authoritative.)
Requires explicit webhook subscription (never delivered via ["*"]; webhook event records only start once an active endpoint lists it). The same events always stream over the WebSocket at /organization/events/stream regardless of webhook configuration — see that endpoint for its delivery guarantees. Expect one event per executed order per account, so size your endpoint for your real fill rate. Payload:
{
"id": "evt_9f3a...",
"type": "fill.created",
"organizationId": "a6fcc0ce-...",
"data": {
"orderId": "0d1e...",
"contract": "MNQZ6",
"product": "MNQ",
"symbol": "MNQ",
"side": "buy",
"orderType": "market",
"status": "filled",
"quantity": 2,
"filledQuantity": 2,
"filledPrice": 21150.25,
"fee": 0.74,
"commission": 0.5,
"realizedPnl": null,
"filledAt": "2026-07-22T14:03:11.482Z",
"account": { "id": "...", "accountNumber": "MFFU-1042", "customerId": "your-customer-id", "status": "in_progress", "currentBalance": 104250 },
"trader": { "email": "trader@example.com", "externalUserId": "your-user-id" }
}
}
A partial-then-cancelled order looks like "status": "cancelled", "quantity": 3, "filledQuantity": 1 — one contract really executed (balance, P&L, and fees reflect it), so record it as a fill of 1.
Payout events: account.payout_processed fires on every executed payout with the amounts and options applied in a payout block:
{
"id": "evt_7b2c...",
"type": "account.payout_processed",
"data": { "id": "...", "status": "in_progress", "...": "..." },
"payout": {
"payout_id": "e5f6a7b8-c9d0-1234-ef01-234567890abc",
"amount": 1600.0,
"balance_before": 52500.0,
"balance_after": 50900.0,
"payout_count": 1,
"mll_locked_to": 50100.0,
"consistency_reset": true
}
}
account.max_payouts_reached fires once, alongside the payout that hits the rule’s maxPayouts limit, carrying payout.payout_id and payout.payout_count.
Limits: Maximum 5 webhooks per organization.
Payload format: Each event includes the full trading account object (data) plus previousAttributes showing what changed. Where available, data includes event-time account context such as trader, trading plan, trading rule, purchase, and active lockout details. See the webhook documentation for full payload schemas.
Rule violation alerts: Subscribe to account.failed to be alerted the moment the trading engine fails an account. Violation events carry structured context:
| Field | Description | Example |
|---|---|---|
trigger | What caused the event | rule_violation, profit_target, admin_bulk |
ruleType | Which rule fired (violations only) | max_loss, daily_loss, max_drawdown, daily_drawdown |
data.violationReason | Human-readable explanation | "Max Loss exceeded: $2,514.00 loss >= $2500 limit" |
{
"id": "evt_5f8a...",
"type": "account.failed",
"trigger": "rule_violation",
"ruleType": "daily_loss",
"data": { "id": "...", "status": "failed", "violationReason": "Daily Loss exceeded: $1,012.50 loss >= $1000 limit", "...": "..." },
"previousAttributes": null
}
Signature verification:
const crypto = require('crypto');
function verify(rawBody, signature, secret, timestamp) {
if (Math.abs(Date.now() / 1000 - timestamp) > 300) return false; // replay protection
const expected = 'sha256=' + crypto.createHmac('sha256', secret)
.update(timestamp + '.' + rawBody).digest('hex');
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}
Authentication: Accepts either Authorization: Bearer <jwt> (dashboard) or X-API-Key: hp_live_... (programmatic).
Authorizations
JWT Bearer token for user session auth. Format: "Bearer {token}". Used by User and Organization endpoints.
Body
Webhook endpoint URL. Must be HTTPS.
"https://your-server.com/webhooks/hyperprop"
Event types to subscribe to. Use ["*"] for all current and future events.
1*, account.created, account.status_changed, account.updated, account.imported, account.unlinked, account.passed, account.failed, account.lockout_created, account.lockout_removed, account.lockout_updated, account.payout_processed, account.max_payouts_reached, purchase.created, purchase.status_changed, trader.created, snapshot.ready, fill.created, account.balance_changed Optional label (e.g. "Production", "Staging CRM")
255"Production webhook"