Skip to content

Endpoint Reference

All endpoints are under https://api.polysimulator.com/v1/partner and authenticate with your tenant X-API-Key (see Authentication).

MethodPathPermissionStatus
POST/v1/partner/usersusers:writeAvailable now
GET/v1/partner/users/{user_id}accounts:readAvailable now
POST/v1/partner/users/{user_id}/login-linkusers:writeAvailable now
POST/v1/partner/accountsaccounts:writeAvailable now
GET/v1/partner/accounts/{account_id}accounts:readAvailable now
GET/v1/partner/accounts/{account_id}/tradesaccounts:readAvailable now
PATCH/v1/partner/accounts/{account_id}accounts:writeAvailable now
POST/v1/partner/accounts/{account_id}/closeaccounts:writeAvailable now
POST/v1/partner/accounts/{account_id}/resetaccounts:writeAvailable now

The whole /v1/partner surface returns 404 until your tenant is enabled — see the release status. Cross-tenant reads also return 404, never 403: you only ever see your own resources, and there is no enumeration signal.


Register a trader, or idempotently resolve an existing one.

FieldTypeRequiredDescription
external_idstring (1–255)YesYour stable ID for this trader. Idempotency key; unique within your tenant.
emailstring (3–320)YesTrader’s email. Lowercased + trimmed server-side.
metadataobjectNoFree-form JSON you control. Stored and echoed back.
{
"external_id": "pc_trader_123",
"email": "trader@example.com",
"metadata": { "country": "ES" }
}

Response — 201 Created (new) or 200 OK (idempotent replay)

Section titled “Response — 201 Created (new) or 200 OK (idempotent replay)”
{
"user_id": 12345,
"tenant_user_id": "6a2f0c4e-1b7d-4e2a-9f3c-1a2b3c4d5e6f",
"external_id": "pc_trader_123",
"email": "trader@example.com",
"created": true
}
FieldTypeDescription
user_idintThe trader’s ID. Use it on all account calls.
tenant_user_idstring (UUID)Your mapping row for this trader.
external_idstringEchoes your external_id.
emailstringThe resolved (normalized) email.
createdbooltrue on a new mapping (201), false on idempotent replay (200).
  • Same external_id, same email → 200, existing mapping (created: false).
  • Same external_id, different email → 409 EXTERNAL_ID_CONFLICT.
  • If the email already belongs to an existing trader, registration links to that trader and returns a normal success — there is no signal about prior existence, so you never branch on it. You always get back a user_id.
StatuserrorWhen
201New trader created
200Idempotent replay (existing mapping, matching email)
400INVALID_EMAILEmail failed the shape check (one @, a domain with a dot)
403INSUFFICIENT_PERMISSIONKey lacks users:write
409EXTERNAL_ID_CONFLICTexternal_id already mapped to a different email
409USER_ALREADY_MAPPEDThis trader is already mapped under a different external_id
502AUTH_PROVISION_FAILEDUpstream identity provisioning failed (retry)

Fetch a tenant-scoped trader profile plus only your tenant’s accounts for that trader. Personal balances, billing, and leaderboard data are never exposed.

{
"user_id": 12345,
"tenant_user_id": "6a2f0c4e-1b7d-4e2a-9f3c-1a2b3c4d5e6f",
"external_id": "pc_trader_123",
"email": "trader@example.com",
"accounts": [
{
"account_id": "0b1d8e2a-3c4f-5a6b-7c8d-9e0f1a2b3c4d",
"external_id": "pc_challenge_001",
"stage": "challenge",
"status": "active",
"starting_balance": "10000.00",
"wallet_id": 9911,
"created_at": "2026-05-31T12:00:00Z"
}
]
}

Each accounts entry is the summary shape (no balance/equity — call GET /v1/partner/accounts/{account_id} for live state). accounts is [] if the trader has no accounts under your tenant.

StatuserrorWhen
200Profile returned
403INSUFFICIENT_PERMISSIONKey lacks accounts:read
404USER_NOT_FOUNDThe user isn’t mapped to your tenant (also returned for unknown IDs)

POST /v1/partner/users/{user_id}/login-link

Section titled “POST /v1/partner/users/{user_id}/login-link”

Send the trader a one-time magic-link email to sign in to the native PolySimulator UI. The link is never returned in the response body.

The body is a JSON object ({} minimum is required — an absent body is rejected 422).

FieldTypeRequiredDescription
redirect_urlstringNoWhere the trader lands after auth. Must be in your tenant’s redirect allowlist. Defaults to https://polysimulator.com.
{ "redirect_url": "https://polysimulator.com/markets" }
{ "status": "accepted" }

202 means the email was dispatched, not that the trader has clicked it.

StatuserrorWhen
202Magic-link email dispatched
400REDIRECT_NOT_ALLOWEDredirect_url not in the tenant allowlist
403INSUFFICIENT_PERMISSIONKey lacks users:write
404USER_NOT_FOUNDThe user isn’t mapped to your tenant
502MAGIC_LINK_FAILEDUpstream email dispatch failed (retry)

Create a tenant-governed account, backed by an isolated wallet funded to starting_balance.

FieldTypeRequiredDescription
user_idintYesThe user_id from POST /v1/partner/users. Must be mapped to your tenant.
external_idstring (1–255)YesYour stable ID for this account. Idempotency key; unique within your tenant.
stagestringYesOne of demo, challenge, funded. A label for your evaluation flow.
starting_balancestringYesOpening balance. Max 2 decimal places, within tenant bounds (default $100$1,000,000).
metadataobjectNoFree-form JSON you control.
rulesobjectNoFree-form JSON. Stored verbatim and echoed back; PolySimulator does not interpret or enforce it.
{
"user_id": 12345,
"external_id": "pc_challenge_001",
"stage": "challenge",
"starting_balance": "10000.00",
"metadata": { "plan": "10k", "attempt": 1 },
"rules": { "profit_target_pct": 20, "max_drawdown_pct": 10 }
}

Response — 201 Created (new) or 200 OK (idempotent replay)

Section titled “Response — 201 Created (new) or 200 OK (idempotent replay)”
{
"account_id": "0b1d8e2a-3c4f-5a6b-7c8d-9e0f1a2b3c4d",
"wallet_id": 9911,
"user_id": 12345,
"external_id": "pc_challenge_001",
"stage": "challenge",
"status": "active",
"starting_balance": "10000.00",
"rules": { "profit_target_pct": 20, "max_drawdown_pct": 10 },
"metadata": { "plan": "10k", "attempt": 1 }
}
FieldTypeDescription
account_idstring (UUID)The account’s ID. Use it on state/trades/close/reset/PATCH calls.
wallet_idintThe wallet backing this account.
statusstringactive on creation; closed after a close; back to active after a reset.
rulesobjectEcho of the rules you sent ({} if none).
metadataobjectEcho of the metadata you sent ({} if none).

external_id is the idempotency key, unique within your tenant:

  • Same external_id with matching user_id + stage + starting_balance200 with the existing account (nothing is created).
  • Same external_id with any of those three differing409 ACCOUNT_EXTERNAL_ID_CONFLICT.

A safely-retried create never double-provisions a wallet. Treat both 201 and 200 as success.

StatuserrorWhen
201New account created
200Idempotent replay (existing account, matching immutable fields)
400INVALID_STAGEstage not in demo/challenge/funded
400INVALID_BALANCEstarting_balance not a number, or > 2 decimal places
400BALANCE_OUT_OF_BOUNDSstarting_balance outside the tenant min/max
403INSUFFICIENT_PERMISSIONKey lacks accounts:write
404USER_NOT_FOUNDuser_id not mapped to your tenant
409ACCOUNT_EXTERNAL_ID_CONFLICTexternal_id reused with different immutable fields
422VALIDATION_FAILEDA field failed schema validation (wrong type, missing required field)

Return the live account-state envelope: balance, equity, unrealized PnL, and open positions marked to market against current Polymarket prices.

{
"account_id": "0b1d8e2a-3c4f-5a6b-7c8d-9e0f1a2b3c4d",
"external_id": "pc_challenge_001",
"stage": "challenge",
"status": "active",
"balance": "9875.12",
"starting_balance": "10000.00",
"equity": "10142.42",
"unrealized_pnl": "267.30",
"realized_pnl": null,
"open_positions": [
{
"market_id": "0xabc123...",
"outcome": "Yes",
"quantity": "100.0000",
"avg_entry_price": "0.4100",
"current_price": "0.4350",
"market_value": "43.50",
"unrealized_pnl": "2.50"
}
],
"rules": { "profit_target_pct": 20, "max_drawdown_pct": 10 },
"metadata": { "plan": "10k", "attempt": 1 },
"updated_at": "2026-05-31T12:00:00Z"
}
FieldTypeDescription
balancestringCash balance of the account’s wallet.
starting_balancestringThe opening balance the account was provisioned with.
equitystringbalance + Σ(open-position market value). Mark-to-market total.
unrealized_pnlstringequity − starting_balance. Mark-to-market P&L vs the baseline.
realized_pnlnullAlways null in v1 — see caution below.
open_positions[]arrayPer-market open positions, each marked to market.
rulesobjectEcho of the stored rules ({} if none). Updatable via PATCH. Stored verbatim; not interpreted.
metadataobjectEcho of the stored metadata ({} if none).
updated_atstringStamped once at creation and equal to created_at in v1 — it does not advance on PATCH/close/reset, so don’t use it as a change signal. Always present (never null).
StatuserrorWhen
200State returned
403INSUFFICIENT_PERMISSIONKey lacks accounts:read
404ACCOUNT_NOT_FOUNDAccount doesn’t exist or belongs to another tenant
422INVALID_ACCOUNT_IDaccount_id in the path is not a valid UUID

GET /v1/partner/accounts/{account_id}/trades

Section titled “GET /v1/partner/accounts/{account_id}/trades”

The primary data feed for your evaluation engine: filled trades for the account’s wallet, newest first, with keyset (cursor) pagination.

ParamTypeDefaultDescription
limitint100Page size, 1500.
cursorstringOpaque cursor from a previous page’s next_cursor. Omit for the first page.
sidestringFilter: BUY or SELL (case-insensitive). Any other value → 422 VALIDATION_FAILED (not silently treated as “no match”).
market_idstringFilter to a single Polymarket condition_id.
{
"data": [
{
"trade_id": 8881,
"order_id": 8881,
"market_id": "0xabc123...",
"side": "BUY",
"outcome": "Yes",
"price": "0.4100",
"quantity": "10.0000",
"notional": "4.10",
"fee": "0.00",
"filled_at": "2026-05-31T12:30:00Z",
"client_order_id": "trader-idempotency-key",
"realized_pnl": null
}
],
"next_cursor": "eyJmaWxsZWRfYXQiOiIyMDI2LTA1LTMxVDEyOjMwOjAwKzAwOjAwIiwib3JkZXJfaWQiOjg4ODF9"
}
FieldTypeDescription
data[]arrayFilled trades only, ordered filled_at DESC, order_id DESC.
data[].notionalstringprice × quantity in USD.
data[].feestringFee charged on the fill ("0.00" for paper trading today).
data[].client_order_idstring | nullIdempotency key supplied when the trade was placed, if any.
data[].realized_pnlnullAlways null in v1 (compute from fills).
next_cursorstring | nullOpaque cursor for the next page. null when there are no more rows.

There is no offset and no total count. The cursor encodes (filled_at, order_id); follow next_cursor until it is null. (Why keyset — see Rate Limits & Errors.)

def all_trades(account_id, key, base="https://api.polysimulator.com"):
cursor, out = None, []
while True:
params = {"limit": 500}
if cursor:
params["cursor"] = cursor
page = requests.get(
f"{base}/v1/partner/accounts/{account_id}/trades",
headers={"X-API-Key": key}, params=params,
).json()
out.extend(page["data"])
cursor = page["next_cursor"]
if not cursor: # null → last page
return out
StatuserrorWhen
200Page returned
400INVALID_CURSORcursor is malformed (not a cursor we issued)
403INSUFFICIENT_PERMISSIONKey lacks accounts:read
404ACCOUNT_NOT_FOUNDAccount doesn’t exist or belongs to another tenant
422INVALID_ACCOUNT_IDaccount_id in the path is not a valid UUID
422VALIDATION_FAILEDside is not BUY/SELL, or limit is out of 1500

Update an account’s stored rules and/or metadata. Each field you send replaces the stored object wholesale; an omitted (or null) field is left untouched. Returns the full account-state envelope with the updated config echoed back.

Only rules and metadata are patchable — stage, starting_balance, user_id, and external_id are immutable. To advance a trader to a new stage, provision a new account with the next stage label (there is no promote-stage operation; see the v1 boundaries).

FieldTypeRequiredDescription
rulesobjectNoNew rules object. Replaces the stored value wholesale. Omit to leave unchanged.
metadataobjectNoNew metadata object. Replaces the stored value wholesale. Omit to leave unchanged.

The body is validated with extra='forbid': an unknown key (e.g. {"rulez": {...}}) returns 422 VALIDATION_FAILED rather than silently doing nothing. An empty body ({}) is a valid no-op that echoes current state back.

{
"rules": { "profit_target_pct": 8, "max_drawdown_pct": 6, "max_daily_loss_pct": 4 },
"metadata": { "plan": "8k", "attempt": 1, "cohort": "2026Q3" }
}

The full account-state envelope (identical shape to GET), with rules/metadata reflecting the update.

StatuserrorWhen
200Config updated (or no-op on an empty body)
403INSUFFICIENT_PERMISSIONKey lacks accounts:write
404ACCOUNT_NOT_FOUNDAccount doesn’t exist or belongs to another tenant
422INVALID_ACCOUNT_IDaccount_id in the path is not a valid UUID
422VALIDATION_FAILEDUnknown field in the body, or rules/metadata not a JSON object

POST /v1/partner/accounts/{account_id}/close

Section titled “POST /v1/partner/accounts/{account_id}/close”

Archive an account — never destructive. Sets status to closed and stops any future trade from running against it. Balance, open positions, trade history, and the rest of the envelope are preserved — every read endpoint keeps working on a closed account.

Before closing, any open pending limit orders on the wallet are cancelled and their reserved funds released (BUY notional refunded to the balance, SELL reservations returned). In v1 trading is via the native UI, so the wallet usually has none — but close handles them for when the execution path lands.

Takes no request body. close is idempotent: closing an already-closed account is a 200 no-op.

The full account-state envelope (same shape as GET), now with "status": "closed".

StatuserrorWhen
200Account closed (or idempotent replay on an already-closed account)
403INSUFFICIENT_PERMISSIONKey lacks accounts:write
404ACCOUNT_NOT_FOUNDAccount doesn’t exist or belongs to another tenant
409ORDERS_NOT_CANCELLEDOpen orders couldn’t all be cancelled (over the bulk-cancel limit or a transient lock); retry to drain the remainder
422INVALID_ACCOUNT_IDaccount_id in the path is not a valid UUID

POST /v1/partner/accounts/{account_id}/reset

Section titled “POST /v1/partner/accounts/{account_id}/reset”

Restart a challenge on the same account, preserving the audit trail. Unlike a new account, reset keeps the same account_id, external_id, and backing wallet, and:

  • resets the balance back to starting_balance;
  • cancels any open pending orders and releases their reservations (the refund is then absorbed by the balance reset, so there’s no inflation);
  • archives any open positions so post-reset equity equals the reset balance;
  • re-activates the account (a previously closed account is reopened by a reset).

The prior journey — opening deposit, every fill, earlier resets — is preserved; nothing is deleted. Takes no request body.

The full account-state envelope (same shape as GET): balance and equity are back to starting_balance, status is active, and open_positions is [].

StatuserrorWhen
200Account reset
403INSUFFICIENT_PERMISSIONKey lacks accounts:write
404ACCOUNT_NOT_FOUNDAccount doesn’t exist or belongs to another tenant
409ORDERS_NOT_CANCELLEDOpen orders couldn’t all be cancelled (over the bulk-cancel limit or a transient lock); retry to drain the remainder
422INVALID_ACCOUNT_IDaccount_id in the path is not a valid UUID