How an account is configured
Three objects shape what an account may trade and under what limits. They look overlapping from the outside, so this page states exactly what each one owns and which of them you write.
Short version: you write a trading group and, for prop, a challenge template. A risk profile is something the server keeps for you unless you deliberately want to share one rule set across several templates.
The provisioning order
Section titled “The provisioning order”trading group ──► challenge template ──► trader ──► account (required) (prop only) (required) ▲ risk profile (optional)- Trading group — the bare minimum. A template cannot be created without
one (
400 trading_group_required), and every account resolves to one. - Challenge template — for a prop program. Carries the rules, and points at the group from step 1.
- Risk profile — optional, and only when you want to share one rule set across templates, or to put limits on a non-prop account. Otherwise the server keeps a managed one for you.
- Trader —
POST /api/v1/users. An account needs an owner, and itsidis theownerUserIdthe account calls want. - Account —
POST /api/v1/accounts/create-prop(prop) orPOST /api/v1/accounts(plain).
ownerUserId is the trader
Section titled “ownerUserId is the trader”The name trips people up, so plainly: it is the person who trades the
account — a users.id. It is not your tenant and not your prop firm.
| Field | Is | Where it comes from |
|---|---|---|
ownerUserId |
The trader | POST /api/v1/users |
tenantId |
Your platform tenant | Forced from your credential — never sent |
propTenantId |
One prop firm you operate | GET /api/v1/firms |
Naming a trader from another tenant returns 404 owner_user_not_found rather
than confirming they exist.
const { user } = await sdk.users.create({ email: 'ana@example.com', externalId: 'crm-4471' })// user.id → pass as ownerUserIdPass your own externalId and the create becomes idempotent: a retried
provisioning run returns the same trader (idempotent: true) instead of
splitting one person across two identities. Creating a trader does not
authenticate them — they still sign in with a wallet or social auth; this only
establishes the identity accounts hang off.
What each one owns
Section titled “What each one owns”| Owns | Answers | |
|---|---|---|
| Trading group | Venue and symbol allowlists, fee schedule and rebate share, leverage caps, spread skew and price bands, per-symbol and per-venue overrides | Which markets, at what price |
| Challenge template | The prop product: account size, entry fee, payout split, duration, tier ladder, next-step chain, reset and activation fees — plus the rule set the run is judged against | What the trader buys, and how they pass or fail |
| Risk profile | A named, reusable rule set (trader, challenge or funded) |
Which limits apply |
The layers are a most-restrictive cascade, not a hierarchy. A group’s venue allowlist is enforced at compose time; the prop rules are enforced after the price is resolved. Both run on every order — neither overrides the other, and each can reject on its own.
The rules live in one place
Section titled “The rules live in one place”A template’s rules and its risk profile are the same rules. You write them once, on the template:
await sdk.propTemplates.create({ id: 'acme-25k-1step', name: 'Acme 25K Evaluation', accountSizeUsd: '25000.00', feeUsd: '199.00', payoutSplitPct: '80', tradingGroupId: group.id, propTenantId: firm.id, rules: [ { type: 'max_drawdown_usd', value: 1250 }, { type: 'profit_target_usd', value: 2000 }, ],})The server materializes a managed risk profile holding those rules and binds
it to the template. You never name it, create it, or bind it. It shows up in
GET /api/v1/risk-profiles with managedByTemplateId set, and it is read-only
there — PATCH and archive return 409 managed_by_template. To change the
rules, patch the template.
That is the whole model for most integrations. The rest of this page is for the two cases where you touch a profile directly.
Case 1 — sharing one rule set across templates
Section titled “Case 1 — sharing one rule set across templates”Six templates (three sizes × two steps) often share two rule sets. Author the profile once and reference it, instead of repeating the rules six times and watching them drift on the next edit:
const { profile } = await sdk.riskProfiles.create({ name: 'Acme 1-step', kind: 'challenge', rules: [{ type: 'max_drawdown_usd', value: 1250 }],})
for (const size of ['25000.00', '50000.00', '100000.00']) { await sdk.propTemplates.create({ /* … */ accountSizeUsd: size, riskProfileId: profile.id })}rules and riskProfileId are mutually exclusive. Sending both is
400 rules_and_profile_conflict — a template has one source of truth for its
rules, so neither one silently wins. When you send riskProfileId, the profile’s
rules are copied down into the template so the snapshot the engine freezes onto
each account matches the profile exactly.
While a template reads a shared profile, patching its rules returns
400 rules_locked_by_profile. Either edit the profile (every template reading it
follows), or send riskProfileId: null to move the rules back onto the template,
where they become managed again.
Case 2 — limits on a non-prop account
Section titled “Case 2 — limits on a non-prop account”A plain live or paper account has no template. To give one limits, author a
trader profile and bind it:
const { profile } = await sdk.riskProfiles.create({ name: 'Retail default', kind: 'trader', enforcement: 'hard', rules: [{ type: 'max_leverage', value: 20 }],})await sdk.riskProfiles.bind(profile.id, accountUuid)What lands on a new account
Section titled “What lands on a new account”POST /api/v1/accounts accepts tradingGroupId and riskProfileId directly, so
an account starts on the right scope instead of being created on the tenant
default and re-bound a moment later. Both are optional and both are validated
against your tenant: an unknown group is 400 trading_group_not_found, and a
managed profile is 400 profile_is_managed — it belongs to its template, so
create the account from that template instead. Omit them and the account
inherits the tenant’s default group with no profile bound, exactly as before.
POST /api/v1/accounts/create-prop takes a templateId and an ownerUserId,
and the template carries the rest: its tradingGroupId and its risk profile are
both propagated onto the new account, and the rule set is frozen onto the prop
account at creation. So for a prop integration you configure one object — the
template — and the group and profile ride along.
A later edit to the template does not re-judge accounts already in flight; they keep the rules they were sold under. New accounts pick up the new rules.
Which rules are enforced today
Section titled “Which rules are enforced today”| Bound to | Enforced by | Status |
|---|---|---|
| A prop account (from a template) | The prop engine, against the frozen snapshot | Hard, always on. A breaching order is rejected with 409 prop_breach_would_occur before the venue sees it |
A non-prop account (trader profile) |
The order-time risk-profile gate | Advisory today — ENFORCE_RISK_PROFILES ships off, so a bound trader profile annotates rather than blocks |
| Any account, via its trading group | The compose-time group gate | Hard, always on for venue and symbol scope |
A managed profile is deliberately not evaluated on the risk plane: the prop engine already enforces the identical rules from the frozen snapshot, so evaluating them twice would report every rule a second time as an advisory warning alongside the real verdict.
Next: Create a challenge template · Create a trading group · Create a risk profile · Prop account model