# Noctant Operator API Private-Beta Partner Changelog

Status: CURRENT — Owner: Operator API — Trigger: a change a partner can see
lands — Successor: none.

What changed for a partner. The upgrade notes come first: what an integration
written against an older call must change, kept true as the API changes. The
dated entries follow, the newest day first: one entry per change, its body
kept as written. This file is served at
`https://docs-staging.noctant.com/operator-api/changelog`. The guide it
belongs to is
[`private-beta-partner-onboarding-guide.md`](private-beta-partner-onboarding-guide.md).

## Upgrade notes

What an integration written against an older call must change to work with
the API as it is now, one note per change. Each note says what changed, since
when (the dated entry below that announced it) and what to do now. A note
states only what has landed: a change that breaks an older integration adds
or rewrites its note in the same change that adds its entry.

`/v1` changes only additively once the first external partner is live; until
then a `/v1` change is made in place and recorded in this changelog. A breaking
change after that point is `/v2`, served beside `/v1` until every consumer has
moved. Until then each breaking `/v1` change has a note here; a `/v2` gets its
own notes for moving to it.

An additive change needs no note. Generate your client from the current
OpenAPI document, accept a field you do not know, and treat an unknown value
of a closed set (an event type, a status, a reason, an error code) as one you
do not handle.

### Account history

- Regenerate your SDK: `program_account_attempt_id` can now be absent on fills,
  balance changes, orders and trades for accounts without a program attempt.
  Imported account metadata has its own required identity fields and no
  sponsorship metadata. Existing Sponsored account metadata stays unchanged.
  Since [2026-11-07](#2026-11-07-account-history-can-lack-a-program-attempt-breaking).

### Webhooks

- **Account, order, position and trade bodies are Sponsored or Imported.**
  Regenerate your client: the bodies of `account.opened`, `account.closed`,
  `account.trading_suspended`, `account.balance_changed`, every `order.*` and
  `position.*` event, `trade.closed` and `trade.excursion` are now unions;
  `account.created` stays Sponsored only. Check
  `data.import_grant_id` before reading `program_account_id`,
  `external_account_id`, `user_id` or `external_user_id`. An operator with no
  claimed grant still receives only Sponsored bodies, byte for byte as before.
  `operator-webhook-receiver` 0.7.0 returns an Imported body as
  `Received::Unhandled` (`unreadable_data`): answer `2xx` and read it from the
  OpenAPI `Imported…` schema. Since
  [2026-11-07](#2026-11-07-a-trader-can-share-their-own-accounts-with-an-app).

- **Backtest trades require protection and excursion.** Historical trade
  reads and newly recorded `backtest.trade.closed` include both at the close.
  Excursion is final, with no `state`; its prices are long bid or short ask
  before execution costs, and bar times identify the bar opening. Earlier
  sessions and their history are removed; start a new session. Stored events
  keep their recorded bodies, and replay sends nothing for removed sessions.
  Receiver 0.6.0 drops the new fields; 0.7.0 exposes them and returns old close
  bodies as `Received::Unhandled`, reason `unreadable_data`. Answer `2xx`;
  upgrade receiver and SDK together, then replay the retained window. Replay
  cannot add analytics to an old body. Since
  [2026-11-07](#2026-11-07-backtest-trades-state-protection-and-excursion-breaking).

- **The signed body is `id`, `type`, `schema_version`, `created`, `data`.**
  It has no `event_id` or `event_type` field: those are the headers
  `X-Noctant-Event-Id` and `X-Noctant-Event-Type`, which repeat the body's
  `id` and `type`. Verify `X-Noctant-Signature` over the raw body first, then
  check that `data.org_id` is your organization. Answer `2xx` to a signed,
  in-org, well-formed event you do not handle (a type, a value or a field
  your build does not know), and store or ignore it. Noctant treats every
  `3xx` and every `4xx` except `429` as final and never redelivers that event,
  so a refusal loses it: once your receiver is fixed, replay the window it
  refused. `operator-webhook-receiver` 0.4.0 returns such an event as
  `Received::Unhandled` (the note on 0.4.0 below). Since: the body has not
  changed; the guides told a
  receiver to refuse an unknown type until
  [2026-10-30](#2026-10-30-this-changelog-is-served-and-opens-with-upgrade-notes). See also [2026-10-12](#2026-10-12-webhook-event-types-and-challenge-statuses-are-closed-sets).

- **Deduplicate by event id alone.** A redelivered or replayed event keeps its
  `id`, but its body can still differ from the first delivery's when that
  delivery came before Noctant began sending the `data` it recorded
  (2026-10-30), and every send carries a new signature. Keep the first body
  you stored for an `id` and answer `2xx` to every repeat, whatever its bytes;
  a `4xx` other than `429` to a repeat is final for that delivery. Since:
  the guides said to refuse a repeated id whose body differs until
  [2026-10-30](#2026-10-30-this-changelog-is-served-and-opens-with-upgrade-notes).

- **An event's `data` is what Noctant recorded.** Every delivery, retry,
  redelivery and replay of an event sends the `data` recorded when it
  happened. An event recorded before its type gained a field lacks it (such a
  field is optional in the OpenAPI document, unless an upgrade note below
  names what an event recorded before carries instead), and one recorded
  before its type lost a field still carries it. Accept a field you do not
  know and a missing optional field. Replay a window to receive an event that
  was not sent before this change (2026-10-30). Since
  [2026-10-30](#2026-10-30-a-webhooks-data-is-what-noctant-recorded-on-every-delivery-and-replay).

- **A backtest trade arrives when it closes.** `backtest.trade.closed` is sent
  while its session is open, and a session the trader never finishes still
  sends its trades. `backtest.session.completed` is recorded after the
  session's last trade; a trade the close itself makes comes with it.
  Deliveries can arrive out of order. A trade's `created` is when the close
  was recorded, no longer the run's completion. Store each trade as it
  arrives, by `trade_id`. Treat a run as complete only on
  `backtest.session.completed`, then read
  `GET /v1/historical-sessions/{backtest_run_id}/trades` for all `trade_count`
  of its trades. Since
  [2026-11-01](#2026-11-01-a-backtest-trade-is-sent-when-it-closes-breaking).

- **Noctant waits at most 48 hours for a `Retry-After`.** A longer value on
  `429` or `503` is retried after 48 hours and spends an attempt if your
  receiver answers the same way. To stop deliveries for longer, answer once
  you are ready and replay the window you missed (30 days). Since
  [2026-10-31](#2026-10-31-one-receivers-failures-slowness-or-pause-no-longer-delay-anothers-webhooks-retry-after-is-honoured-up-to-48-hours).

- **`operator-webhook-receiver` 0.3.0 and `operator-webhook-receiver-postgres`
  0.2.0 take a repeat as a duplicate.** A repeated `(id, type)` is a duplicate
  whatever its body: a processed event answers `200` again and is not
  processed again. Earlier versions answered `400` to a repeat whose body
  differed, which loses the event. An id the store holds under another event
  type answers `503`. Before you upgrade the helper, run once:
  `ALTER TABLE noctant_webhook_idempotency DROP COLUMN envelope_sha256, DROP COLUMN conflict_count;`
  Its `install_schema` refuses a table that still has them.
  `WebhookIdempotencyRecord::from_parts` takes the event id and type, and
  `classify_webhook_idempotency` returns a `Result`. Since
  [2026-10-30](#2026-10-30-the-rust-receiver-crates-take-a-repeated-event-as-a-duplicate-whatever-its-body).

- **`GET /v1/me` states the subscription as `webhook.subscription`.**
  `operator.webhook.event_types` (a list, or `null` for every type) is gone.
  `operator.webhook.subscription` is always present: its `mode` is `default`
  (every event type, the ones added later included) or `explicit` (a fixed
  list), and its `event_types` are the types delivered today. Check the
  events you need against `webhook.subscription.event_types`. A `default`
  subscription delivered your preset's event types until the
  [2026-10-30](#2026-10-30-noctant-ticks-the-features-your-company-may-open-work-in-presets-are-named-by-what-they-hold)
  change; one on a narrower preset then became that preset's list, as
  `explicit`.
  No event is delivered to an operator without the scope `webhooks:receive`.
  Since [2026-10-06](#2026-10-06-get-v1me-states-the-webhook-subscription-as-its-mode-and-its-event-types).

- **`GET /v1/webhooks/deliveries` has no account filter.** Its filters are
  `event_id`, `event_type` and `status`. It pages with `limit` and the three
  fields of `next_cursor` (`before_finished_at`, `before_attempt_number`,
  `before_delivery_row_id`), all three or none. A query field it does not
  declare is ignored, so `external_account_id` there returns every delivery of
  your operator. Filter by `event_id`, or page the whole list. Since
  [2026-10-01](#2026-10-01-account-operations-use-program_account_id-as-their-account-handle).

- **A webhook replay names its account by `program_account_id`, and pages.**
  `POST /v1/webhooks/replays` takes an optional `program_account_id`.
  `external_account_id` in its body is ignored, so such a replay covers every
  account of your operator for the types and window asked. A page enqueues at
  most `limit` events (1 to 500, 100 when omitted); while its answer says
  `has_more`, call `POST /v1/webhooks/replays/{replay_request_id}/continuation`
  with `{"idempotency_key": …, "limit": …}`. Each page states
  `source_event_ids`. Build the replay's `idempotency_key` from
  `program_account_id`. Since [2026-10-01](#2026-10-01-account-operations-use-program_account_id-as-their-account-handle); paging since
  2026-09-27, before the dated entries began.

- **An event type you name must be one the API lists.** A delivery filter's
  `event_type`, a replay's `event_types` and a subscription's `event_types`
  must be values of `WebhookEventType`; any other is refused `400` (it
  returned an empty page). Since [2026-10-12](#2026-10-12-webhook-event-types-and-challenge-statuses-are-closed-sets).

- **`operator-webhook-receiver` and `operator-api-client` 0.2.** From 0.2.0,
  `WebhookVerifier::new`, `new_with_tolerance` and `from_signing_secret`, and
  `OperatorAppCredentials::webhook_verifier`, take a `ReceiverScope`, and
  another organization's event is refused before its id is recorded. 0.2.1
  and earlier refuse `account.closed` with the `reason` `platform`; 0.2.2 and
  earlier refuse the `reason` `owner_erased` and a challenge event whose
  `status` is `withdrawn`. Use `operator-webhook-receiver` 0.6.0 (the notes on
  repeats above, on 0.4.0 below, on `account.trading_suspended` and on
  `order.rejected`). Since
  [2026-10-08](#2026-10-08-operator-webhook-receiver-020-operator-api-client-020-breaking), [2026-10-12](#2026-10-12-accountclosed-can-name-the-reason-platform) and
  [2026-10-18](#2026-10-18-an-erased-traders-challenge-is-withdrawn-and-their-account-closes-as-owner_erased).

- **`operator-webhook-receiver` 0.4.0, `operator-webhook-receiver-postgres`
  0.3.0 and `operator-api-client` 0.3.0 answer `2xx` to an event your build
  cannot read.** `WebhookVerifier::verify` is the one verify call. For every
  signed, in-org, well-formed event it returns `Received::Handled(event)` or
  `Received::Unhandled { envelope, reason }`, and its error is only a refusal
  (`signature`, `scope` or `malformed`). The reason is
  `unknown_schema_version`, `unknown_type` or `unreadable_data`, with the path
  in `data` where reading stopped. `verify_and_process` returns `Unhandled`
  without running your processor or recording the event: answer `200` and log
  or alert on the reason. Earlier versions refused such an event, and a `4xx`
  loses it. Replace `verify_envelope`, `verify_envelope_at`, `verify_as`,
  `verify_as_at`, `verify_at`, `VerifiedWebhook`,
  `WebhookIdempotencyRecord::from_verified` and `verify_envelope_and_process`
  with `verify` or `verify_and_process`; to verify at a fixed time, build the
  verifier with a fixed `Clock`. The helper's table is unchanged. After you
  upgrade, see
  [After You Upgrade Your Receiver](private-beta-partner-onboarding-guide.md#after-you-upgrade-your-receiver).
  Since [2026-10-30](#2026-10-30-the-rust-receiver-crates-answer-2xx-to-an-event-your-build-cannot-read).

- **`BalanceSourcePosition` is `JournalSourcePosition`.** In
  `operator-api-contracts`, the type of `source_position` on
  `account.balance_changed` is named `JournalSourcePosition`. Rust code that
  names the type renames it; its fields and JSON are unchanged. Since
  [2026-10-31](#2026-10-31-balancesourceposition-is-journalsourceposition-breaking).

- **The header-name constants are in `operator-api-contracts`.**
  `operator-webhook-receiver`'s `EVENT_ID_HEADER`, `EVENT_TYPE_HEADER` and
  `SIGNATURE_HEADER` are `WEBHOOK_EVENT_ID_HEADER`,
  `WEBHOOK_EVENT_TYPE_HEADER` and `WEBHOOK_SIGNATURE_HEADER` in
  `operator-api-contracts`. Rust code that names them imports them from there;
  the header names are unchanged. Since
  [2026-10-31](#2026-10-31-the-header-name-constants-are-in-operator-api-contracts-breaking).

- **`account.trading_suspended` has no `description`.** It repeated free
  text: the note sent with a status change, or the description of the rule
  that stopped trading. Its `reason` reads `administrative` on every
  suspension. `suspended_by` names who placed a hold, and
  `challenge.status_changed` tells an attempt's stop. The `reason` you send to
  disable an account is still required, and stays with your request: it is the
  hold's note, in your audit trail (`GET /v1/audit-events`, `detail.reason`).
  An event recorded before still carries `description` (the note on recorded
  `data` above). `operator-webhook-receiver` 0.5.0 reads the event without it;
  0.4.0 returns it as `Received::Unhandled` (`unreadable_data`), and 0.3.0 and
  earlier refuse it. Since
  [2026-11-01](#2026-11-01-accounttrading_suspended-no-longer-carries-description-breaking).

- **`order.rejected` and `order.cancelled` say `rejection_code`; `order.modified`
  says `cause`.** The engine's code and sentence are `rejection_code` and
  `rejection_reason`, as on the orders read, no longer `code` and `reason`.
  `order.modified` states `cause` on every change (`trader`,
  `target_position_reduced`, `oca_reduced`, `child_woken` or `trail_moved`) in
  place of `reason_code`. An event recorded before still carries the old fields
  (the note on recorded `data` above). In `operator-api-contracts` the pair is
  `OrderRejection` (`code()`, `reason()`) in the field `rejection`, and
  `reason_code` is `cause: OrderChangeCause`. `operator-webhook-receiver` 0.6.0
  reads the new fields and returns an event recorded before as
  `Received::Unhandled` (`unreadable_data`); 0.5.0 returns a new
  `order.rejected` or `order.cancelled` so, and reads a new `order.modified` as
  a trader's change. Since [2026-11-02](#2026-11-02-orderrejected-and-ordercancelled-name-their-code-rejection_code-and-ordermodified-states-its-cause-breaking).

### Accounts and launches

- **Account calls take `program_account_id`.** The operations under
  `/v1/accounts/by-external-id/` (`status`, `close-all`,
  `commands/{command_id}`, `summary`, `positions`, `orders`, `trades`) are
  gone: such a path answers `400` `validation_failed` (`403` `forbidden`
  first when the key lacks the scope of the operation it lands on), because
  `by-external-id` is read as a `program_account_id`. Look the account up once
  with `GET /v1/accounts/by-external-id?external_account_id=…` (scope
  `accounts:read`), keep `account.program_account_id`, and call
  `/v1/accounts/{program_account_id}/…`. Challenges, simulated payout
  requests, account corrections, launch sessions and webhook replays name the
  account the same way, and every account webhook carries it. Orders and
  trades answer the account's current attempt unless you send
  `execution_scope` (`attempt` with `attempt_id`, or `all_attempts`). Since
  [2026-10-01](#2026-10-01-account-operations-use-program_account_id-as-their-account-handle).

- **A known account launches with `POST /v1/launch-sessions`.** A claimable
  launch carries the account's creation terms (`initial_balance`, `currency`,
  `account_name`, `execution_profile_key`, `product_id`, `feed_source`,
  `provider_type`, `rules_owner`, the policies, `claim_allowed` and
  `marketing_allowed`) and its person. Once an account exists under its
  `external_account_id`, a claimable launch whose terms or person differ is
  refused `409` `conflict` when the trader's browser connects. After the
  first connect, launch the person into the account with
  `POST /v1/launch-sessions` (`external_user_id` or `global_user_id`,
  `program_account_id`, `requested_route`, `requested_product_mode`,
  `return_url`). If you send a claimable launch for that account again, send
  the terms you first sent, for the same person. Since: the API has always
  answered so; the guides say it from [2026-10-30](#2026-10-30-this-changelog-is-served-and-opens-with-upgrade-notes).

- **The launch link carries its token after `#`.** `launch_url` is
  `<page>#nlt_<64 hex>`; it was `<page>/nlt_…`. Redirect the browser to it
  whole: a redirect that drops or encodes the `#` is refused. To log one, keep
  its origin and path and redact the fragment (`…/launch#<redacted>`). Since
  [2026-10-10](#2026-10-10-the-launch-link-carries-its-token-after-).

- **An account's open profit is not stated.**
  `GET /v1/accounts/{program_account_id}/summary`, `GET /trader/v1/accounts`
  and `GET /trader/v1/accounts/{account_id}` omit `equity` and
  `free_margin` while a position is open, and
  `GET /v1/accounts/{program_account_id}/positions` carries no
  `unrealized_pnl`. Show them as unavailable, never as zero. Since
  [2026-10-31](#2026-10-31-an-account-summary-states-equity-only-while-the-account-is-flat-a-position-carries-no-open-pl).

- **A backtest launch names its account.**
  `POST /v1/historical-launch-sessions` and
  `POST /v1/claimable-historical-launch-sessions` with
  `requested_product_mode` `backtest` take `account`:
  `{"kind": "account", "execution_profile_key", "initial_balance", "currency"}`
  or `{"kind": "challenge", "template_id"}`; one without it is refused `400`
  `validation_failed`. Since
  [2026-11-01](#2026-11-01-a-backtest-launch-hands-its-trader-an-account-or-a-challenge).

- **A historical launch names its chart.**
  `POST /v1/historical-launch-sessions` and
  `POST /v1/claimable-historical-launch-sessions` require `chart` (`symbol`,
  `timeframe`) and answer `400` `validation_failed` without it. Send a symbol
  `GET /v1/feeds` lists for the launch's `feed_id`. Handle `404`
  `unknown_symbol` (no such instrument, or no sealed history yet) and `400`
  `no_data_for_period` (the window is outside its history); neither succeeds
  unchanged. Historical launches made before then, and the backtest runs they
  started, are gone from the launch and historical-session reads and from
  webhook replays. Since [2026-11-01](#2026-11-01-a-historical-launch-names-the-chart-it-opens-on-breaking).

- **Find a relaunch's backtest by `external_ref`.** A `backtest` launch whose
  `external_ref`, `feed_id`, window and `account` (for a challenge, the same
  published revision) match the trader's unfinished backtest from an earlier
  launch (not a challenge whose step has passed, failed or expired) reopens
  that session when the submitted symbols and start time are also the ones the
  trader first started it with (for a challenge, its first step's): its results
  carry the earlier launch's `launch_session_id`, and no result names the new
  launch's. Key backtest results by `external_ref`, or by the
  `launch_session_id` the results state, never by the id of the latest launch
  you made. Since
  [2026-11-01](#2026-11-01-a-relaunch-with-the-same-external_ref-reopens-the-traders-open-backtest).

- **A claimable historical request has no direct form.** In
  `operator-api-contracts`, `CreateClaimableHistoricalLaunchSessionRequest` no
  longer has `historical_request_for_external_user`; its `validate()` makes
  every check. Since
  [2026-11-01](#2026-11-01-a-refused-body-names-the-same-field-every-time-breaking).

- **The history reads share one query and one cursor.** In
  `operator-api-contracts`, `ListPositionProtectionQuery` is
  `ListJournalHistoryQuery`, and the schema `PositionProtectionCursor` is
  `JournalHistoryCursor` in the OpenAPI document and the TypeScript SDK:
  `GET /v1/accounts/{program_account_id}/positions/{position_id}/protection`
  and `GET /v1/accounts/{program_account_id}/orders/{order_id}/history` both
  take and answer them. Code that names either renames it; the query
  parameters and JSON are unchanged. Since
  [2026-11-01](#2026-11-01-an-order-states-every-change-made-to-its-terms).

- **A closed trade has one shape.** `GET /v1/accounts/{program_account_id}/trades`
  and `trade.closed` state the same required closed-trade fields. Stop
  reading `swap` and `correlation_id` from the trades read and `swap` and
  `pnl_pips` from `trade.closed`; read the formerly optional trade fields as
  always present. A `trade.closed` recorded before this change for a position
  built from several fills states its `entry_price` unrounded; compare
  amounts as decimals. Rust code reads the trade's fields through `trade`.
  Since
  [2026-11-01](#2026-11-01-a-closed-trade-has-one-shape-in-the-trades-read-and-tradeclosed-breaking).

- **A trade, an order, a fill and a balance change belong to the attempt the
  engine recorded them in.** `GET /v1/accounts/{program_account_id}/trades`,
  `/orders`, `/fills` and `/balance-changes` label each row's
  `program_account_attempt_id` by that attempt, replacing the time window. A
  reset's or a new challenge's `adjustment` is the first change of the attempt
  it opens. Re-read `all_attempts` to correct labels you stored. Since
  [2026-11-02](#2026-11-02-a-trade-is-listed-under-the-attempt-the-engine-recorded-it-in)
  and
  [2026-11-03](#2026-11-03-orders-fills-and-balance-changes-are-listed-under-the-attempt-the-engine-recorded-them-in).

### Errors and retries

- **Every error has one body: switch on `error`.** An error answers
  `{"error": <code>, "detail": <sentence>, …}` (`FirmError` in the OpenAPI
  document). Switch on `error`; `detail` is for a human, never parse it.
  `command_receipt_timeout` (`504`) adds `command_id`, `account_id`, `status`
  and `retry_after_ms`; `rate_limit_exceeded` (`429`) adds
  `retry_after_seconds`. Keep the status and the `Retry-After` header.
  Retry:
  - `429` and `503` after the `Retry-After` seconds when it is sent (`60` in a
    deployment window, `1` for a Trader API request the platform cannot take
    yet), otherwise after a backoff;
  - a `409` only when `error` is `journal_catching_up`,
    `account_command_applying`, `guards_not_acknowledged`,
    `attempt_end_applying`, `account_birth_applying`, `judgment_catching_up`
    or `browser_identity_retry_required`: the same request with the same key,
    after a short backoff;
  - `504` `command_receipt_timeout`: the same request with the same key.

  Never resend a `409` `conflict` unchanged. A `409` `payout_held` waits on
  what its `detail` names: send the same request (with its key, for a
  creation) once that clears. Show a code you do not know as a failure; do
  not retry it. Since
  [2026-10-05](#2026-10-05-every-error-answers-one-body-and-a-refusal-that-decided-nothing-says-so-by-its-code), [2026-10-15](#2026-10-15-a-challenge-door-waiting-on-the-accounts-last-command-says-account_command_applying),
  [2026-10-15](#2026-10-15-every-refusal-names-its-code), [2026-10-17](#2026-10-17-an-approval-whose-post-trade-checks-are-behind-is-retryable),
  [2026-10-24](#2026-10-24-a-deployment-window-answers-in-the-error-body), [2026-10-26](#2026-10-26-a-trader-api-command-is-refused-503-before-it-is-sent-while-the-engine-does-not-decide) and
  [2026-10-28](#2026-10-28-a-session-request-the-platform-cannot-authenticate-yet-answers-503).

- **Some refusals changed status or code.**
  - A payout refused for the trader's KYC answers `409`
    `payout_kyc_required`; it answered `403`
    ([2026-10-14](#2026-10-14-payout_kyc_required-answers-409-and-the-document-lists-every-status)).
  - `410` `person_erased` answers a call that names a person who deleted
    their Noctant account: `POST /v1/users/{global_user_id}/kyc-status`, a
    repeated `POST /v1/accounts`, a claimable launch's connect
    ([2026-10-15](#2026-10-15-a-person-who-deleted-their-account-is-refused-on-every-door-that-names-them)), and `POST /v1/challenges`,
    `POST /v1/challenges/{instance_id}/reset` and
    `POST /v1/challenges/{instance_id}/unfail`
    ([2026-10-18](#2026-10-18-no-challenge-starts-for-an-erased-trader)). It is final; provision the person
    under a new external id if they come back.
  - On a closed account, a new challenge attachment answers `409` `conflict`
    and a new reset `410` `account_closed`; both answered `404`. The same
    request with its key returns the recorded challenge
    ([2026-10-30](#2026-10-30-a-challenge-attachment-or-reset-replays-its-key-after-the-account-closes)).
  - A historical launch the operator holds no grant for answers `403`
    `replay_grant_required`, and `detail` names the product, the feed and the
    window; it answered `403` `forbidden` with no `detail`. Handle the new
    code on both historical creates
    ([2026-11-01](#2026-11-01-a-historical-launch-without-its-grant-names-the-grant-it-needs-breaking)).
  - A body too long answers `413` `payload_too_large`, an unknown path `404`
    `not_found`, and a method the path does not take `405`
    `method_not_allowed`, each in the error body; they answered plain text or
    nothing ([2026-10-15](#2026-10-15-every-refusal-names-its-code)).
  - `GET /v1/feeds/{feed_id}/availability` answers a symbol the feed does
    not list, or one with no sealed history, `404` `unknown_symbol`; it
    answered `404` `not_found` ([2026-11-01](#2026-11-01-a-historical-launch-names-the-chart-it-opens-on-breaking)).

- **`rate_limit_unavailable` is retired.** No Noctant server sends it, and the
  OpenAPI document, the TypeScript SDK and `operator-api-contracts` no longer
  list it. Delete any branch that names it: in TypeScript or Rust it no longer
  compiles. Since
  [2026-11-07](#2026-11-07-the-unused-rate_limit_unavailable-error-code-is-retired-breaking).

- **An amount its field cannot hold exactly answers `400`
  `validation_failed`.** `amount` on
  `POST /v1/users/{global_user_id}/account-corrections` and `initial_balance`
  on `POST /v1/accounts` and `POST /v1/claimable-launch-sessions` allow at
  most 10 whole digits and 8 decimal places. Trailing zeros do not count.
  `detail` names the field. Send computed values already at that precision:
  the API rounded them before and now refuses them. Since
  [2026-10-31](#2026-10-31-amounts-a-field-cannot-hold-exactly-are-refused-never-rounded-breaking).

- **An operation that opens new work of a feature your company does not
  hold answers `403` `feature_not_enabled`.** Noctant ticks, per company, the
  features it may open new work in; `GET /v1/me` lists yours as `features`.
  The refusal's `detail` names the feature. Static openings answer before the
  scope check, for keys and staff sessions alike; openings that depend on the
  body answer after the scope check and after the body is read. The same
  request answers the same until Noctant ticks the feature: do not retry it.
  The operations, by feature:
  - `accounts`: `POST /v1/users`, `POST /v1/accounts` and
    `POST /v1/claimable-launch-sessions`;
  - `challenges`: `POST /v1/templates`, `PATCH /v1/templates/{template_id}`,
    `POST /v1/templates/{template_id}/publish`, `POST /v1/products`,
    `POST /v1/challenges`, and a historical launch whose `account` is a
    challenge;
  - `resets`: `POST /v1/challenges/{instance_id}/reset`;
  - `payouts`: `POST /v1/users/{global_user_id}/payout-requests`;
  - `kyc_reports`: `POST /v1/products` whose `kyc_gate` is not `none`
    ([2026-11-07](#2026-11-07-creating-a-product-that-requires-kyc-needs-the-companys-kyc-reports-feature));
  - `historical_replay`: `POST /v1/historical-launch-sessions`,
    `POST /v1/claimable-historical-launch-sessions` and
    `PATCH /v1/operators/{operator_id}/replay-product-access`;
  - `simulation_credits`: `POST /v1/simulation-credits/settings` and
    `POST /v1/users/{global_user_id}/simulation-credits/grants`.

  A claimable launch is checked again when the trader's browser connects.
  Reads, and the operations that settle, decide, correct or close work
  already open, are never refused for a feature, and neither is a KYC
  report. In a company without `webhooks`, no new event is delivered; a
  delivery already owed is finished. Since
  [2026-10-30](#2026-10-30-noctant-ticks-the-features-your-company-may-open-work-in-presets-are-named-by-what-they-hold).

### Challenges

- **A challenge goes live after it is created.** `POST /v1/challenges`
  answers `reset_pending` for a first attachment; it answered `active`
  ([2026-10-25](#2026-10-25-a-first-attachment-starts-on-your-organizations-rules-and-goes-live-once-the-engine-installed-them)). `challenge.started` states the status the
  challenge opens in (`reset_pending`, `suspended`, `activation_pending` or
  `awaiting_slot`), not always `active` ([2026-10-19](#2026-10-19-a-next-step-on-the-same-account-starts-when-its-rules-are-in-force),
  [2026-10-20](#2026-10-20-challengestarted-says-the-status-a-challenge-opens-in)). A challenge is live when
  `challenge.status_changed` says `active`. A challenge ended while it waits
  for a slot is `cancelled`, not `failed` ([2026-10-10](#2026-10-10-a-challenge-cancelled-while-it-waits-is-cancelled-not-failed)),
  and an erased trader's challenge is `withdrawn`
  ([2026-10-18](#2026-10-18-an-erased-traders-challenge-is-withdrawn-and-their-account-closes-as-owner_erased)); both are final.

- **One `idempotency_key` names one attachment.** Sending a key again to
  `POST /v1/challenges` with another `user_id`, `program_account_id` or
  `template_id` answers `409` `conflict`; it started a second challenge.
  Since [2026-10-26](#2026-10-26-a-challenge-idempotency_key-names-one-request-across-your-accounts).

- **A product's KYC start gate is enforced.** With a `kyc_gate` of
  `challenge_start` or `both`, `POST /v1/challenges` and
  `POST /v1/challenges/{instance_id}/reset` answer `409`
  `challenge_start_kyc_required` until you report the trader verified with
  `POST /v1/users/{global_user_id}/kyc-status`; then send the same request
  and key. Since [2026-10-25](#2026-10-25-a-product-whose-kyc-gate-covers-the-challenge-start-starts-no-challenge-before-you-report-the-trader-verified).

- **Approving a challenge answers `202`.**
  `POST /v1/challenges/{instance_id}/approve` answers `202` with the
  challenge; it answered `200` with the decision. The approval is decided
  moments later, at its place in the account's order: read
  `newest_request.answer` on `GET /v1/challenges/{instance_id}`, and the
  webhooks. Since [2026-10-24](#2026-10-24-an-approval-is-decided-at-its-place-in-the-accounts-order).

### Risk overrides

- **A risk override is a request; read what is in force.**
  `PATCH /v1/accounts/{account_id}/risk-overrides`, a staff-session operation
  whose path names the execution `account_id`, answers `202` (it answered
  `200`) with the account's state: `in_force`, `pending` and `last_refusal`.
  `GET` answers the same state; it answered the last set sent. A value with
  more decimal places than the account keeps is refused `400`. A set does not
  carry into a challenge run (an attachment, a reset, an unfail or a
  re-attachment), and `PATCH` answers `409` while a passed challenge's rules
  still judge the account. Since [2026-10-24](#2026-10-24-an-accounts-risk-overrides-are-in-force-once-the-engine-applies-them),
  [2026-10-24](#2026-10-24-a-reset-an-unfail-or-a-re-attachment-clears-the-accounts-risk-overrides) and [2026-10-25](#2026-10-25-a-passed-challenge-whose-rules-still-judge-its-account-keeps-the-account-from-risk-overrides).

### Simulated payouts

- **`manual_review` is gone; a `take_at_request` payout leaves the account
  at the request.** Stop sending `manual_review`; steps that had it read
  `automatic`, so set their mode and publish again. `take_at_request` needs
  `payout_peak_policy` `none` or `reduce_by_amount`. Regenerate your client
  and handle the `account_effect_status` values `taking`, `taken`,
  `returning`, `return_awaiting_release` and `returned`, and the
  `transaction_type` values `payout_take` and `payout_return`. A `taken`
  payout is out of the account but not paid: decide each take, even on a
  failed attempt (`POST …/approve` keeps it, `POST …/reject` returns
  it). After a return, a `failed` attempt can come back to `active` through
  `challenge.status_changed` with `reason` `payout_return`. Until a take is
  settled, resetting its attempt answers `409` `conflict`, and closing its
  account answers `409` `conflict` until you decide the take, then
  `account_command_applying` while its keep or return completes. Since
  [2026-11-07](#2026-11-07-a-simulated-payout-is-taken-at-request-breaking).

- **One way to approve, and a flat account first.**
  `POST …/payout-requests/{payout_request_id}/approve-and-execute`, the
  approval mode `operator_approved_debit` and the `account_effect_status`
  `reserved` are gone. Approve with
  `POST /v1/users/{global_user_id}/payout-requests/{payout_request_id}/approve`,
  which needs both `payout_requests:approve` and `payout_settlements:write`;
  the mode is `pause_until_decided`. A request waits for a flat account (no
  open position, no working order): until then it is held with the
  prerequisite `flat_account`. Since [2026-10-25](#2026-10-25-a-payout-can-pause-its-account-until-the-firm-decides).

- **A payout statement's `policy_input` moved its per-profit facts.** They
  sit under `policy_resolution.earned` and `policy_resolution.raw`, beside
  `policy_resolution.raw_best_day_profit`, and left the top level of
  `policy_input` and `policy_resolution`. `priced_at`, at the top level of
  `policy_input`, names which of the two priced the request (`earned` or
  `raw`, `null` where it could not be priced). Requests recorded before
  keep the old keys, so read both shapes. In a
  `requested_amount_exceeds_available` refusal, `available_profit` is always a
  decimal. Since [2026-10-28](#2026-10-28-a-simulated-payout-and-a-challenges-adjudication-count-only-the-profit-the-attempt-earned).

- **A payout statement keeps its create reference.** Approving, rejecting or
  updating the external settlement of a request no longer replaces its
  `operator_reference`. Read a review's reference from
  `policy_decision.operator_review.operator_reference`; a settlement update's
  `operator_reference` is kept in its audit event, which a staff session reads.
  Track the payment with `external_settlement_reference`. Since
  [2026-10-31](#2026-10-31-a-simulated-payout-request-keeps-its-create-reference-breaking).

### Templates

- **`GET /v1/templates` pages by keyset.** `offset` is refused `400`, as is
  any parameter the list does not read; `total_count` is gone; `limit` is 1
  to 500 (100 when omitted). Send a page's `next_cursor` as
  `before_created_at` and `before_template_id`. The list is newest first.
  Since [2026-10-18](#2026-10-18-get-v1templates-pages-by-keyset-breaking).
- **Template decimals must fit their published field precision.** Send
  computed values without fractional tails beyond the field's precision or
  whole digits beyond its bound. The API now refuses them as `400`
  `validation_failed`, naming the field in `detail`; trailing zeros remain
  valid. Since [2026-10-31](#2026-10-31-template-authoring-refuses-decimal-values-persistence-would-change-breaking);
  for a step's payout terms since [2026-11-07](#2026-11-07-a-payout-policys-money-and-percentages-fit-their-published-precision-breaking),
  and for a scaling rung's `trader_split_pct` since
  [2026-11-07](#2026-11-07-scaling-split-fits-its-published-precision-breaking).
- **A step's daily-loss rules share one `day_start_reference`.** A step whose
  `max_daily_loss` rules name two is refused `400` `validation_failed` on
  `POST /v1/templates` and `PATCH /v1/templates/{template_id}`, naming both in
  `detail`. Give them the same one. Since [2026-11-01](#2026-11-01-a-steps-daily-loss-rules-share-one-day-start-reference-breaking).
- **Publish at `org` to offer a template to your traders.** `private`, the
  default, is your firm's alone: no trader of yours finds it in the trading
  app. A revision keeps the visibility it was published at, so publishing
  unchanged content at another one is refused `400` `validation_failed`:
  change the template first. Since [2026-11-03](#2026-11-03-a-private-template-is-your-firms-alone-breaking).

### Products

- **A product carries no listing, no limit and no price.**
  `POST /v1/products` refuses `public_listing`, `display_order`,
  `max_concurrent_per_user`, `cooldown_hours_after_breach`, `price` and
  `currency` `400` `validation_failed`, as it refuses any field it does not
  declare, and a product no longer answers them. Stop sending them; keep what
  you need of them in your own system or in `metadata`. `GET /v1/products`
  lists newest first. A create first sent before this change, sent again
  with its key and without the removed fields, answers `409` `conflict`: read
  the product instead. Since
  [2026-11-03](#2026-11-03-a-product-carries-no-listing-no-limit-and-no-price-of-its-own-breaking).
- **A product is created on sale; its `status` is `active` or `archived`.**
  `POST /v1/products` refuses `status` `400` `validation_failed`, as it
  refuses any field it does not declare. Stop sending it: create a product
  when it should sell, and archive it to stop. A product no longer answers
  `activated_at`; read `created_at`. A create first sent before this change,
  sent again with its key and without `status`, answers `409` `conflict`:
  read the product instead. Since
  [2026-11-03](#2026-11-03-a-product-is-on-sale-from-its-creation-until-you-archive-it-breaking).

### Operator setup

- **All listed presets are available to apply.** `GET /v1/operator-api-presets`
  no longer returns `availability` or `can_apply`. Regenerate your client,
  remove checks of these fields and stop importing
  `OperatorApiPresetAvailability`. Since
  [2026-11-07](#2026-11-07-preset-views-no-longer-carry-availability-flags-breaking).

- **The Operator API presets are named by what they hold.** The staff-session
  operations `GET /v1/operator-api-presets` and
  `PATCH /v1/operators/{operator_id}/operator-api-preset` list and take
  the current catalog, version 5; an old key is refused `400`
  `validation_failed`. The new key of each old one: `bare_trading_tenant` is `trading_only`,
  `historical_replay_backtest` is `historical_replay`,
  `economic_calendar_importer` is `calendar_importer` and
  `read_only_journal_import` is `read_only_import`, and `prop_firm` is `full`,
  which also holds `historical_sessions:read`. `prop_simulator`, `journal_account_creator`
  and `education_partner` have none: set the scopes with
  `PATCH /v1/operators/{operator_id}/operator-api-scopes`. An operator keeps
  the scopes it holds, and nothing reads which preset it was given. Applying
  a preset sets the operator's scopes to the preset's and its webhook
  subscription to `default`, every event type, so an `explicit` list, one
  your staff set or one the 2026-10-30 change made, widens to every type.
  A preset's view no longer lists `webhook_event_types`, and the
  readiness report (`GET /v1/operators/{operator_id}/operator-api-readiness`)
  no longer carries `preset`, `scope_drift` or `webhook_event_type_drift`.
  Since [2026-10-30](#2026-10-30-noctant-ticks-the-features-your-company-may-open-work-in-presets-are-named-by-what-they-hold).

- **`read_only_import` is available; it and `full` hold `imports:revoke`.**
  Catalog version 5 changes no existing operator's stored scopes. Add the
  scope with `PATCH /v1/operators/{operator_id}/operator-api-scopes`, sending
  the entire list, or apply the preset again, which sets the operator's scopes
  to exactly the preset's and its webhook subscription to `default`. An app
  that checks exact preset scopes must update its expected set and its
  operator together. Existing grants survive
  key rotation; replacing the operator requires fresh consent. Since
  [2026-11-07](#2026-11-07-a-trader-can-share-their-own-accounts-with-an-app).

- **`trading_only` holds `users:disconnect`,** as do `full` and
  `historical_replay`. An operator keeps the scopes it
  holds, so one set up before this change does not have it, and its key is
  refused on `POST /v1/users/{global_user_id}/disconnect`. A readiness check
  that requires exactly the preset's scopes (`expect_exact_scopes` in
  `operator-api-client`, as an account-creating journal app runs over
  `trading_only` at startup) fails with
  `operator_api_scopes missing users:disconnect`, and the app does not start.
  Add the scope with
  `PATCH /v1/operators/{operator_id}/operator-api-scopes`, which replaces the
  whole list, so send every scope the operator holds, then start the app.
  Applying the preset again also adds it, and sets the webhook subscription to
  `default`. An app built before this change fails the same check on an operator
  given the preset after it, with
  `operator_api_scopes contains unexpected users:disconnect`: rebuild it. Since
  [2026-11-01](#2026-11-01-a-trader-or-a-partner-can-end-a-connection).

### Audit trail

- **Audit entries carry ids, not names.** `operator_api.user.create` no
  longer carries `email` or `display_name` in `detail`
  ([2026-10-26](#2026-10-26-a-persons-provisioning-audit-entry-no-longer-carries-their-address-or-name)); `trader_api.key.issued` and
  `trader_api.key.revoked` no longer carry `name`
  ([2026-10-28](#2026-10-28-a-trader-api-keys-audit-entries-no-longer-carry-its-name)); `operator_api.product.create` no
  longer carries `price`, `currency`
  ([2026-11-03](#2026-11-03-a-product-carries-no-listing-no-limit-and-no-price-of-its-own-breaking)) or `status`
  ([2026-11-03](#2026-11-03-a-product-is-on-sale-from-its-creation-until-you-archive-it-breaking)). Tell entries apart by `global_user_id`
  and `external_id`, or by `target_id` and `prefix`.

### Staff

- **The Nocta backtest allowance is named in minutes.** In the staff-session
  `GET /v1/entitlements` response, read `usage.nocta_max_backtest_minutes`
  instead of `usage.nocta_max_backtest_bars`; its value, in elapsed minutes,
  is unchanged. Newly recorded `admin.operator.nocta_entitlements_updated`
  audit events name it the same way in their `detail`; earlier events keep
  `nocta_max_backtest_bars`. A generated client treats `usage` as an opaque
  object, so update handwritten field lookups. Since
  [2026-11-07](#2026-11-07-backtest-allowance-names-minutes-breaking).

- **Your firm's logo is set and read at `/v1/brand-logo`.**
  `PATCH /v1/operators/{operator_id}/brand-logo` answers `404`: send the same
  body to `PATCH /v1/brand-logo`. Read the logo with `GET /v1/brand-logo`, not
  from `GET /v1/operators/{operator_id}`, which no longer carries
  `brand_logo`. One logo serves every operator of your firm. Since
  [2026-11-01](#2026-11-01-your-firm-wears-one-logo-set-once-for-all-your-operators).

### Health

- **Monitor `GET /health/live`.** A path that ends in `/health/ready` answers
  `404` on every host. `GET /health/live` answers `200` with an empty body.
  Since [2026-10-10](#2026-10-10-readiness-routes-are-not-answered-from-outside).

### Trader API

- **A trader session and a trader API key reach only accounts and challenges owned
  by that person, including when the person is an organization administrator or
  owner.** Use the Firm console or the Firm API for firm account administration.
  Since [2026-11-07](#2026-11-07-trader-credentials-reach-only-their-owners-accounts-breaking).

- **An account names the attempt that owns it.** `challenge_instance_id` on a
  Trader API account replaces `active_challenge_instance_id`: the newest attempt
  that held the account, which stays after that attempt ends, and absent on an
  account no attempt ever held. Since [2026-11-03](#2026-11-03-a-trader-api-account-names-the-attempt-that-owns-it-breaking).

- **A refused order, change or close is a 422.** Read the engine's refusal
  from the `422` `command_rejected`'s `reason_code` at the request, not from a
  later order read or the stream. A `202` now means the engine did what was
  asked. Since [2026-11-02](#2026-11-02-a-refused-order-order-change-or-close-is-answered-422-at-its-request-breaking).

- **An order carries no free text.** `POST /trader/v1/orders` refuses
  `comment` and `client_order_id` with `400` `invalid_request`. Your
  reference for an order is its `Idempotency-Key`, returned as `command_id`.
  Since [2026-10-13](#2026-10-13-an-order-carries-no-free-text-comment-and-client_order_id-leave-the-trader-api).

- **The Trader Stream.** `margin_updated` carries only `margin_used` and `at`:
  read `free_margin` and `margin_level` from
  `GET /trader/v1/accounts/{account_id}`
  ([2026-10-29](#2026-10-29-the-trader-streams-margin_updated-states-only-the-margin-the-engine-booked)). A socket opened with a session's ticket
  ends with that login (`refused`, `unauthorized`) unless you send
  `{"op": "authorize", "ticket": …}` with a fresh ticket
  ([2026-10-28](#2026-10-28-a-sessions-trader-stream-ends-with-its-login-and-authorize-keeps-it-open)). A new event, `account_status`, arrives on
  every change of the account's status ([2026-10-28](#2026-10-28-the-trader-stream-states-an-accounts-status-as-account_status)).

### Billing

- **A billing plan is named and priced by lines.** `GET /v1/billing/plans`
  names each plan by `plan_id` and `name` instead of `tier`, and states
  `lines` (one per `meter`, each with its `allowance`) instead of
  `included_evaluations`. `GET /v1/billing/statements` no longer answers
  `quota`: read a statement's `lines` and `total`. Since
  [2026-10-30](#2026-10-30-a-billing-plan-is-a-named-plan-priced-by-lines-breaking).

### Simulation credits

- **A credit counts one kind of use.** `GET` and
  `POST /v1/simulation-credits/settings` no longer carry
  `free_credits_per_month`: a level's monthly allowance is its `terms`, answered
  beside `settings`. A trader's credits
  (`GET /v1/users/{global_user_id}/simulation-credits`, and the answers of a
  grant and a refund) answer `by_use` instead of `balance`, `free_this_month`
  and `next_free_at`: read the `metered` row of the `use` you need. A spend
  names what it paid for in `on` instead of `instance_id`. Since
  [2026-10-30](#2026-10-30-a-credit-counts-one-kind-of-use-and-a-traders-level-says-what-each-use-costs-breaking).

### Values no longer written

Records written before keep these values, so read both.

- `close_reason` `administrative`: now `operator_requested`,
  `step_passed_lock`, `account_held` or `account_suspended`
  ([2026-10-08](#2026-10-08-four-new-close_reason-values)).
- `rejection_code` or `engine_command_outcome_reason_code` `PRICE_STALE`
  (`code` on an `order.rejected` or `order.cancelled` recorded before
  2026-11-02): now `MARKET_CLOSED`, `FEED_BEHIND`, `PRICE_UNAVAILABLE`,
  `NO_QUOTE_SINCE_OPEN` or `NO_QUOTE_SINCE_FEED_RESUMED`
  ([2026-10-09](#2026-10-09-a-held-price-is-refused-by-its-cause)).
- `DATA_ONLY_SYMBOL`: no longer answered ([2026-10-20](#2026-10-20-an-order-is-admitted-on-what-the-accounts-feed-offers-before-its-first-tick)).
- `CURRENCY_RATE_UNAVAILABLE` for an amount too large for the ledger: now
  `AMOUNT_OUT_OF_RANGE` ([2026-10-26](#2026-10-26-an-amount-beyond-the-ledger-is-refused-as-amount_out_of_range)).
- `PendingReadinessBlocker` `no_predecessor_on_program_account`: gone
  ([2026-10-10](#2026-10-10-an-account-whose-challenge-waits-for-a-slot-is-yours-to-reach)).
- `admin.operator.brand_logo_updated`: now `admin.organization.brand_logo_updated`,
  on your organization ([2026-11-01](#2026-11-01-your-firm-wears-one-logo-set-once-for-all-your-operators)).

## Dated entries

Every new webhook event type is an entry here the day it lands: the event
type enum in the OpenAPI document (a delivery's `event_type`, a subscription)
grows with it, and a strict client generated from the document may refuse a
value it does not know. A receiver verifies an envelope of an unknown type and
may ignore it.

### 2026-11-07: a level can count replay sessions

A correction to
[2026-10-30](#2026-10-30-a-credit-counts-one-kind-of-use-and-a-traders-level-says-what-each-use-costs-breaking):
its ‘Only a live run may be counted today’ no longer holds. A level can count
`replay_session` on an org that holds credits. Your company's terms remain
`unmetered`, so your traders still spend no credit.

### 2026-11-07: a new reset your key or staff make shows no simulated fee

Correction to the
[2026-10-31 entry on simulated spend](#2026-10-31-read-what-your-traders-challenges-spent-in-simulated-money):
for a new reset your key or your staff make,
`ChallengeView.purchase_price_snapshot` is `0` on that attempt and on the
steps that follow it: no simulated reset fee is recorded. The template's
price stays in
`rules_snapshot.template.challenge_price`. Earlier attempts keep their
frozen values. Read
`GET /v1/users/{global_user_id}/simulated-cash` for recorded spend and
refunds. Every amount is simulated.

### 2026-11-07: a payout policy's money and percentages fit their published precision (breaking)

- `POST /v1/templates` and `PATCH /v1/templates/{template_id}` now answer
  `400` `validation_failed` when a step's simulated payout terms state a
  decimal that cannot be stored unchanged, as the template's other decimals
  already do. `detail` names the field, including its step and tier position.
- Money allows at most 16 whole digits and 2 decimal places:
  `payout_availability.required_remaining_buffer`, and in each tier of
  `payout_tiers` a `cumulative_qualifying_days` key's `min_net_profit`, a
  `cumulative_profit_from` key's `value`, a `profit_hinge` split's `hinge`,
  `max_gross_amount` and `required_remaining_buffer`.
- Percentages allow at most 3 whole digits and 2 decimal places:
  `payout_availability.max_request_pct_of_available`, `payout_cycle.max_pct`,
  and in each tier `max_request_pct_of_available`, a `flat` split's
  `trader_pct` and a `profit_hinge` split's `below_pct` and `above_pct`.
- Trailing zeros do not count, and the API does not round. The OpenAPI
  document states each field's precision.

### 2026-11-07: a simulated payout is taken at request (breaking)

- The payout approval mode `manual_review` is gone: a template that names it
  is refused `400` `validation_failed`. A step that had it now reads
  `automatic`, and the revisions published with it are removed: set the mode
  you want and publish again. A payout your firm decides is
  `pause_until_decided` or the new `take_at_request`, on the program's last
  step only. Every payout inside Noctant is simulated.
- On a `take_at_request` step, a request (on a flat account, as every
  request) takes the payout from the account at once: it is `pending_review`
  with `account_effect_status` `taking`, then `taken`. The account keeps
  trading. Balance changes and `account.balance_changed` show the take as
  `transaction_type` `payout_take`. If the engine refuses it, the request
  reads `rejected` and `refused`, and nothing is taken.
- `POST …/approve` on a `taken` request keeps the money, with no second
  debit: `approved` and `taken`, then `executed`, a paid payout. On a
  `taking` request it answers `409` `account_command_applying`: retry.
  `POST …/reject` gives back exactly what was taken: `rejected` and `returning`
  (`return_awaiting_release` while a hold delays it), then `returned`, shown
  as `transaction_type` `payout_return`. The attempt's failure does not
  withdraw a take: decide it either way.
- `take_at_request` needs `payout_peak_policy` `none` or `reduce_by_amount`;
  any other is refused `400` `validation_failed`. The policy applies at the
  take. A return adds the amount back to the balance, to the four tracked
  peaks, and to the start of a daily-loss day that began while the money was
  out. It never leaves more room than if nothing had been taken; under `none`
  it can leave up to that amount less trailing-drawdown room.
- If a `max_daily_loss`, `max_total_drawdown` or `max_trailing_drawdown` rule
  failed the attempt while the money was out, and the returned amount puts
  the account back within it, the attempt can come back:
  `challenge.status_changed` from `failed` to `suspended` with `reason`
  `payout_return`, then to `active`. Any other failure stays, `composite` and
  `ratio` rules included. While its take is unsettled, such a failed attempt
  still counts toward its template's `live_limit`.
- A take is settled once kept, or once returned and, if its attempt failed,
  that attempt has come back or stays failed. Until then the account's next
  payout request is held with the prerequisite `payout_under_review`, a failed
  attempt's account stays open, `POST /v1/challenges/{instance_id}/reset` on
  its attempt answers `409` `conflict`, and
  `PATCH /v1/accounts/{program_account_id}/status` to `closed` answers `409`
  `conflict` until you decide the take, then `409` `account_command_applying`
  (retry) while its keep or return completes. A trader's erasure, or the end
  of their connection to your firm, returns a take still under review; their
  attempt does not come back. Replay has no firm: there, a request on such a
  step is refused `firm_decides` and nothing moves.

### 2026-11-07: A trader can share their own accounts with an app

A person can approve your named operator to read their present and future
persistent accounts across companies, closed accounts and recorded history
included. They need no company selection or new membership. Accounts your
operator already sponsors, including ended sponsorships, are excluded.

Bind a fresh reference to your journal user, browser session and exact return
URL. Verify that pending state before claiming the one-time approval code with
`POST /v1/import-grants/claim`. Backfill every page under
`/v1/import-grants/{grant_id}/accounts`, then reconcile signed Imported events
through REST and bounded replay without `program_account_id`.
Imported `data.org_id` is the recipient organization; `source_org_id` is the
source company, and the grant and recipient operator identify the consent.

Catalog version 5 makes `read_only_import` available and adds `imports:revoke`
to it and `full`; existing operators need that scope assigned explicitly.
Either the person or your operator can revoke. Revocation leaves trading and
received copies unchanged; detect it through import-route `404`s or grant
status polling. A sponsored disconnect does not end an import grant.
Key rotation preserves grants; operator replacement needs fresh consent.
Reads provide no atomic snapshot across accounts.

The [journal guide](2026-06-29-external-journal-app-integration-guide.md#reading-a-persons-own-accounts)
describes consent, lost approval returns, paging, replay and both revoke paths.

### 2026-11-07: a write sent during a platform release answers 503, not 500

- While a platform release is under way, part of the platform can run the new version while the rest still runs the
  old one. In that moment a write whose body the release changes answered `500` `internal_error`. It now answers `503`
  `service_unavailable`: nothing was decided. Send the same request again, with the same `Idempotency-Key` where the
  operation takes one, after a backoff. This covers creating and resetting challenges; approving or rejecting reviews;
  manually failing, unfailing and resuming challenges; retrying pending re-attachments; resolving rule flags; proposing
  accommodations; loading calendar input; publishing templates; changing challenge and simulation-credit settings;
  granting and refunding simulation credits; and creating and recalling restriction layers.
- A template publish naming a visibility that the release adds answered `400` `validation_failed` in that moment; it
  answers `503` too.
- No request, response field or webhook changes.

### 2026-11-07: Account history can lack a program attempt (breaking)

- Regenerate your SDK: `program_account_attempt_id` can now be absent on fills, balance changes, orders and trades for accounts without a program attempt. Imported account metadata has its own required identity fields and no sponsorship metadata. Existing Sponsored account metadata stays unchanged.
- Account reads are published under `/v1/import-grants/{grant_id}/accounts` for the exact app that claimed the trader's grant. Account imports must be turned on for the receiving company; this change turns them on for no company.
- These routes read present, future and closed accounts and recorded history across source companies while the trader stays the account's active owner and an active member of its company. Accounts this app sponsors or once sponsored are excluded. The grant gives no trading or account-control authority.

### 2026-11-07: Account import consent and management

- Account import routes are published. Every company refuses consent previews, new approvals and first claims until account imports are turned on for it. This change turns them on for no company.
- A signed-in person can use `/trader/v1/import-grants` without selecting a company. When account imports are turned on, the person can approve an app to read their own accounts; the approving browser receives a one-time code with its exact client reference.
- The receiving app claims through `POST /v1/import-grants/claim` using the code, reference and exact original return URL. Claimed status is available under `users:read`; revocation requires the explicitly granted `imports:revoke` scope. This change assigns that scope to no preset.
- A signed-in person or the receiving app can revoke existing access even while account imports are turned off. Revocation preserves trading. Data the app already received cannot be recalled.

### 2026-11-07: backtest allowance names minutes (breaking)

The staff-session `GET /v1/entitlements` response names the Nocta backtest
allowance `usage.nocta_max_backtest_minutes` instead of
`usage.nocta_max_backtest_bars`. Its value and meaning are unchanged: elapsed
minutes, not chart candles. Newly recorded
`admin.operator.nocta_entitlements_updated` events of `GET /v1/audit-events`
name it the same way in their `detail`; earlier events keep
`nocta_max_backtest_bars`.

Read the new key; the old one has no alias. A generated client treats `usage`
as an opaque object, so update handwritten field lookups.

### 2026-11-07: backtest trades state protection and excursion (breaking)

Historical trade reads and newly recorded `backtest.trade.closed` require
`protection` and `excursion` as each trade closes. Protection states the first
SL/TP with market times and the exit levels; an absent level means none.
Excursion states `favorable_price`, `favorable_at`, `adverse_price` and
`adverse_at`: the best and worst long bid or short ask over the replayed
position lifetime, before markup and slippage, including opening and closing
observations. Ties keep the first time. Partial closes freeze the lifetime so
far; a scale-in keeps that lifetime.

Excursion is final and has no `state`. Bar times identify the bar opening,
not an observed intrabar instant. `created` remains recording time.

Earlier sessions and their history are removed; start a new session. Stored
events retain their recorded bodies. Replay sends nothing for removed
sessions. Receiver 0.6.0 reads new bodies but drops both analytics fields;
0.7.0 exposes them and returns old close bodies as `Received::Unhandled`,
reason `unreadable_data`. Answer `2xx`; upgrade the receiver and SDK together,
then replay the retained window. Replay cannot add analytics to an old body.

### 2026-11-07: Challenge refusals name the reason

A refused challenge operation's `detail` now gives its reason as a sentence. Its error code and HTTP status are unchanged. Your audit trail now records a new refusal's reason as a sentence too, instead of a JSON body.

### 2026-11-07: Creating a product that requires KYC needs the company's KYC reports feature

Creating a product with `kyc_gate` other than `none` now requires your company's `kyc_reports` feature; otherwise it answers `403` `feature_not_enabled`, naming `kyc_reports`. This includes an exact creation retry after the feature is turned off. Existing products and KYC reporting continue. The requirement covers starts and simulated payouts.

### 2026-11-07: Preset views no longer carry availability flags (breaking)

`GET /v1/operator-api-presets` no longer returns `availability` or `can_apply`
on each preset. All listed presets are available to apply.
`OperatorApiPresetAvailability` and its `controlled_available` and
`future_surface` values are removed from the OpenAPI document.

Regenerate your client and stop reading these fields or importing
`OperatorApiPresetAvailability`.

### 2026-11-07: scaling split fits its published precision (breaking)

- `POST /v1/templates` and `PATCH /v1/templates/{template_id}` now refuse a
  scaling rung's `trader_split_pct` with `400` when it exceeds 2 decimal
  places. The simulated payout split allows at most 3 whole digits and
  2 decimal places, within 0 through 100 inclusive.
- Send `33.33`, for example; `33.333` is refused. Trailing zeros do not
  count, and the API does not round. The OpenAPI document states the
  field's precision.

### 2026-11-07: the unused `rate_limit_unavailable` error code is retired (breaking)

- `rate_limit_unavailable` is no longer an error code, and no Noctant server
  sends it. The OpenAPI document's `FirmErrorCode`, the TypeScript SDK and
  `operator-api-contracts` no longer list it, and the document lists `503`
  with `service_unavailable` alone.
- TypeScript or Rust code that names it no longer compiles: delete that
  branch. Requests, responses and the `429` `rate_limit_exceeded` answer are
  unchanged.

### 2026-11-07: Trader credentials reach only their owner's accounts (breaking)

A trader session and a trader API key reach only accounts and challenges owned by that person, including when the person is an organization administrator or owner. A key's selected accounts narrow that set. Use the Firm console or Firm API for firm account administration.

A key's selected accounts now apply before `limit` on `GET /trader/v1/accounts`.

### 2026-11-06: The Trader Stream's heartbeat waits for a frame still being written

- A correction to the [2026-11-03 heartbeat entry](#2026-11-03-the-trader-streams-heartbeat-comes-every-5-seconds):
  a Trader Stream socket's `heartbeat` frame is due every 5 seconds and goes ahead of queued events, but it waits for
  a frame the socket is still writing. On a slow connection it can come later than 5 seconds.
- So a silent socket may be slow rather than dead. What to do is unchanged: if a socket stays silent for a few of those
  periods, open a new one with a new ticket and subscribe with the cursor you hold.

### 2026-11-03: a challenge-service failure answers 500 on publish, and an answer cut short answers 503

- `POST /v1/templates/{template_id}/publish` answers `500 internal_error` when the challenge service failed (it
  answered 500, or with something the Firm API cannot read), like every other challenge operation. It answered
  `503 service_unavailable`, which told you to retry a request that answers the same again.
- Any challenge operation whose answer from the challenge service is cut off mid-answer now answers
  `503 service_unavailable`: send the same request again, with the same key. It answered `500 internal_error`.
- No request, response field or error code changes.

### 2026-11-03: a challenge session left after its verdict reads the verdict

- `GET /v1/historical-sessions`, `GET /v1/historical-sessions/{backtest_run_id}` and the `session` of its `/trades`:
  a launched challenge's session that reached its verdict (a breach, a pass or an expiry) and was left by its trader
  read `challenge.status` `active` until the trader finished it. It now reads the verdict from that moment. Its
  `state` stays `created`.
- `backtest.session.completed` is unchanged: it is sent when the trader finishes the session, resets it or moves to
  the next step, never at the verdict.
- No request, response field or webhook changes.

### 2026-11-03: a launch's tab stays your launch page after a reload

- After a reload, the tab a launch opened is still your launch page for that
  session: "Back to …" with your name stays, an account launch's list still
  follows the launched account's `visibility_policy`, and a historical launch
  reopens its window. The entry of 2026-11-01 and the guide said a reload of
  Noctant drops "Back to …".
- A launch whose account or org is no longer the trader's ends at the reload.
- A new tab the trader opens is their own Noctant and shows no "Back to …".
- No request or response field changes.

### 2026-11-03: A `private` template is your firm's alone (breaking)

- `POST /v1/templates/{template_id}/publish` with `visibility` `private`, the
  default, publishes a revision only your firm reads, through the Operator
  API. Your traders no longer find it in the trading app: they cannot see,
  save, start, invite to or practise it there. Before, a `private` revision
  reached every trader of your firm, as `org` does.
- `visibility` `org` publishes a revision every trader of your firm finds in
  the trading app. A trader who copies it or practises it reads that revision,
  never an edit you have not published.
- Publishing unchanged content again at another `visibility` is refused `400`
  `validation_failed`, its `detail` "This content is already published as
  private. Change the template to publish it as org." Before, it answered
  `200` with the revision the content already had, at its earlier visibility.
  Change the template, then publish it.
- Your products, launch links and the challenges you start or attach for your
  traders read your templates as your firm, whatever their visibility, and are
  unchanged. A challenge already started keeps its revision.

### 2026-11-03: A product carries no listing, no limit and no price of its own (breaking)

- `POST /v1/products` takes `idempotency_key`, `template_id`,
  `display_name`, `slug`, `description` (optional), `status`, `kyc_gate` and
  `metadata`, and nothing else. A field it does not declare is refused `400`
  `validation_failed`, and `detail` names it: "public_listing is not a field
  of this request.".
- `public_listing`, `display_order`, `max_concurrent_per_user`,
  `cooldown_hours_after_breach`, `price` and `currency` are gone from the
  request and from every product the API answers. None decided anything:
  Noctant lists no product (a trader browses published templates, never a
  product); it enforced neither limit (the limits it enforces are a
  template's `live_limit` and your organization's, in
  `GET /v1/challenge-settings`); and a challenge's simulated fee is its
  template's displayed fee.
- `GET /v1/products` lists newest first.
- Keep your own listing choice, order, price or limits in your own system, or
  in `metadata`, which Noctant stores and returns as you sent it.
- A create whose `idempotency_key` was first sent before this change, sent
  again without the removed fields, answers `409` `conflict`: read the
  product with `GET /v1/products`.
- `operator_api.product.create` audit entries (`GET /v1/audit-events`) no
  longer carry `price` or `currency` in `detail`.

### 2026-11-03: A product is on sale from its creation until you archive it (breaking)

- `POST /v1/products` no longer takes `status`. A product is on sale (`active`)
  from the moment you create it, so create it when you want it to sell.
  Sending `status` is refused `400` `validation_failed`, and `detail` names it:
  "status is not a field of this request.".
- A product's `status` is `active` or `archived`. Archiving it
  (`POST /v1/products/{product_id}/archive`, or archiving the template it
  sells) is final: it sells no new account and no new challenge on a finished
  account, and the accounts already sold under it continue.
- `draft` and `hidden` are gone. No call ever put a `draft` product on sale,
  and a `hidden` product sold exactly as an `active` one does. A product
  created `hidden` now reads `active`; one created `draft` reads `archived`.
- A product no longer answers `activated_at`: it is on sale from its
  `created_at`. `archived_at` is when it was archived.
- A new account naming an archived product is refused `400`
  `validation_failed`: "referenced product is archived: only an active product
  is sold".
- A create whose `idempotency_key` was first sent before this change, sent
  again without `status`, answers `409` `conflict`: read the product with
  `GET /v1/products`.
- `operator_api.product.create` audit entries (`GET /v1/audit-events`) no
  longer carry `status` in `detail`.

### 2026-11-03: a relaunch never reopens a challenge whose step has ended

- A `backtest` launch with the same `external_ref`, `feed_id`, window and `account` as a launched challenge whose
  step has passed, failed or expired no longer reopens it. A start from a launch that has not started a session
  starts a new challenge from its first step, and its results carry that launch's `launch_session_id`. A start from
  the launch that began the ended challenge answers `409`.
- This narrows the reopen of
  [2026-11-01](#2026-11-01-a-relaunch-with-the-same-external_ref-reopens-the-traders-open-backtest): a challenge that
  reached its verdict is no longer reopened.
- No request or response field changes.

### 2026-11-03: a Trader API account names the attempt that owns it (breaking)

- `challenge_instance_id` on `GET /trader/v1/accounts` and `GET /trader/v1/accounts/{account_id}` replaces
  `active_challenge_instance_id`, which was absent once an attempt stopped being live.
- It names the attempt that owns the account: the newest that held it, whatever its status, so it stays after that
  attempt ends. An attempt cancelled while it waited, or whose start ended before it went live, never owned the
  account. It is absent on an account no attempt ever held.
- Pass it as `challenge_instance_id` on the Trader API order list to read that attempt's orders.

### 2026-11-03: a Trader API order list reads one attempt with `challenge_instance_id`

- `GET /trader/v1/accounts/{account_id}/orders` gains an optional query parameter, `challenge_instance_id`: only the
  orders the engine recorded under that attempt. An attempt with no order on this account reads an empty list. Orders
  the engine recorded under no attempt, and orders from before it recorded one, are listed only without the parameter.
- `since` is unchanged: a filter on the date an order was created. It says nothing about the attempt an order belongs
  to, so it can include another attempt's order on the same account.

### 2026-11-03: a trader asks for a new confirmation link under the email-alert refusal

- Under the refusal "Confirm your email address before sending alerts to it.", the alert editor offers "Send me
  a new link": Noctant mails a new confirmation link to the address the trader signed up with, at most one every 5
  minutes. A trader whose link expired or never arrived no longer signs out to ask for one. The journal-app guide's
  account launch section says so.
- No request, response or webhook changes.

### 2026-11-03: a trader's email alerts wait until their address is confirmed

- An alert a trader sets to send email is refused with "Confirm your email address before sending alerts to it."
  until they confirm their address from the link Noctant mails them. In-app and push alerts are not affected. A
  trader who signs up during a launch starts unconfirmed; the journal-app guide's account launch section says what
  to tell a trader who asks.
- No request, response or webhook changes.

### 2026-11-03: Orders, fills and balance changes are listed under the attempt the engine recorded them in

- Each `/orders`, `/fills` and `/balance-changes` row is labelled with the
  attempt the engine recorded it in, as `/trades` rows are. It is no longer
  the attempt whose window holds its time.
- The order, fill and realized P&L of a close that passed a step, or that the
  passed step recorded after it, stay with the passed attempt.
- A reversal fill is listed under the attempt of the position it closed.
- A reset's or a new challenge's `adjustment` is the first change of the
  attempt it opens and sets the balance that attempt starts on. An attempt's
  result is its own last change's `new_balance`.
- A next step's or a reset's rows, its `adjustment` included, appear once it
  goes live.
- Rows recorded before this change, and rows the engine records while no
  challenge holds the account, are listed under the account's first attempt.
- Re-read `all_attempts` to correct labels you stored.

### 2026-11-03: the console offers a new confirmation link when your address is not confirmed

- A console sign-in with a login whose email address is not confirmed yet is refused with its own code, `unproven`:
  "This Noctant login's email address is not confirmed. Open the link Noctant mailed to it, then sign in again."
  Under it, the console offers "Send me a new link": Noctant mails a new confirmation link to the login's own
  address, at most one every 5 minutes. The self-onboarding guide's step 3 says so.
- No Operator API request, response or webhook changes.

### 2026-11-03: the order of a close made after an attempt ends is listed under that attempt

- When an attempt ends with a position open, the platform's close of that position is the attempt's: its trade was
  already listed under the attempt, and now its order is too, on `/orders` and on the Trader API order list with
  `challenge_instance_id`. This covers the close at the attempt's end, a close made when the market opens, a stop-loss,
  take-profit or stop-out, and the trader's close of the position.
- An order the trader placed after the attempt ended is listed as before: under the account's first attempt on
  `/orders`, and on the Trader API order list only without `challenge_instance_id`.
- Rows recorded before this change keep their label.

### 2026-11-03: The Trader Stream's heartbeat comes every 5 seconds

- A Trader Stream socket now writes its `heartbeat` frame every 5 seconds,
  whatever else it has to say, where it wrote one every 15. The frame itself
  has not changed, and it says nothing but that the socket is there.
- A socket silent past a few of them is dead even with no close. Open a new one
  with a new ticket and subscribe with the cursor you hold: every later event
  arrives once, in order.

### 2026-11-02: a challenge's loss objectives state their floor

- `daily_loss`, `max_drawdown` and `trailing_drawdown` in `ChallengeView.objectives` have `floor`: the account
  equity at which the rule's limit is reached, in the account's currency, rounded half away from zero to cents. It
  is worked out from the figures the engine stored, so it is stated while a position is open too, when `measured`
  is `null`.
- `daily_loss`: the day's start less the limit, for the challenge day `daily_loss_period_start_at` names; `null`
  until the engine has recorded a day's start. `max_drawdown`: the balance the challenge opened on, raised by an
  applied size increase, less the limit. `trailing_drawdown`: the peak the rule trails less the limit, held at the
  rule's lock once it binds, or at the level a simulated payout fixed; `null` until the engine has stated the peak.
- What happens at the floor is the rule's consequence in `rules_snapshot`. A rule that counts the estimated cost of
  closing open positions (`commission_inclusive`) reaches its limit that much above `floor` while a position is
  open. A step with two daily-loss or two drawdown rules lists one objective, whose `floor` is that rule's; the
  other can be reached first.

### 2026-11-02: A deleted Noctant account takes its replay sessions and alerts with it

- When a person deletes their Noctant account, their replay sessions on every
  firm, the backtests your launches started included, are now deleted soon
  after, with everything they hold. `GET /v1/historical-sessions` then lists
  none of them, and `GET /v1/historical-sessions/{backtest_run_id}` and its
  `/trades` answer `404`. No webhook is sent for the deletion. A `backtest.*`
  event of theirs not yet delivered is not sent, and a webhook replay no
  longer reaches their `backtest.*` events.
- Their alerts are deleted too, with their delivery records, their push
  devices and the records of their alert-pause emails.
- Your partner-webhook deliveries and your audit trail stay. A disconnect
  still deletes no historical session.
- No request, response or webhook changes.

### 2026-11-02: a historical launch can carry a trade to compare with

- `POST /v1/historical-launch-sessions` and
  `POST /v1/claimable-historical-launch-sessions` take an optional
  `comparison_trade`: the trader's own trade, which Noctant draws on the
  launch's `chart` so the trader can compare their replay with it. For
  example `{"direction": "long", "entry": {"time": "2026-10-14T08:31:00Z",
  "price": "1.08412"}, "exits": [{"time": "2026-10-14T09:02:00Z", "price":
  "1.08530"}, {"time": "2026-10-14T09:40:00Z", "price": "1.08610"}],
  "stop": "1.08200", "target": "1.08900"}`.
- `direction` is `long` or `short`. `entry` and each of `exits` are a `time`
  (RFC 3339) and a `price` (a decimal string above 0). `exits` are in time
  order, none before the entry, at most 50; with none, the trade is still
  open. `stop` and `target` are optional prices above 0. Every time is at or
  after `market_time_start` and before `market_time_end`. It carries no size
  and no money: any other key, or a value outside these rules, answers `400`
  `validation_failed`, and no launch is made.
- A launch's `historical_launch` states its `comparison_trade`.
- Chart Replay (`playback`) and the backtest session's chart draw the entry,
  each exit, and the stop and target as lines, beside the trader's own
  replay trades. Each appears only once the replay reaches it. They are
  drawn in the browser tab the launch opened.
- The consent step tells the trader that you can send Noctant a trade of
  theirs to draw on the chart.
- When the trader's Noctant account is erased, Noctant removes the trades you sent with their launches.
- A historical launch, direct or claimable, for a trader whose Noctant
  account is erased answers `410` `person_erased`, and no launch is made.
- A launch made without `comparison_trade` answers as before: the field is
  absent.

### 2026-11-02: A payout you approved stays yours to settle after its account closes

- `PATCH /v1/users/{global_user_id}/payout-requests/{payout_request_id}/external-settlement`
  now records your settlement of a payout you approved after its account
  closed, a disconnect included. It answered `404`. Every payout inside
  Noctant is simulated.
- Approving or rejecting a payout, and requesting a new one, still answer `404`
  once the account closes.

### 2026-11-02: A refused order, order change or close is answered 422 at its request (breaking)

- `POST /trader/v1/orders`: an order the engine refuses (its size, margin or
  price, the market's hours or its feed, the account's hold, or a challenge
  rule) is answered `422` `command_rejected` with the engine's `reason_code`.
  Before, it was answered `202` and the order then read `rejected` or
  `cancelled`. The refused order is still recorded and listed with its code,
  and a refusal by a challenge rule still counts as an occurrence of that rule.
- `POST /trader/v1/orders/{order_id}/modify`: a change the engine refuses for
  its order is answered `422` `command_rejected`. The Trader Stream's
  `order_modify_rejected` still arrives and now names the request's
  `command_id`.
- `POST /trader/v1/positions/{position_id}/close`: a close the engine refuses
  for its position is answered `422` `command_rejected`.
- `GET /trader/v1/commands/{command_id}` states `rejected` with the code for
  each. The firm's `GET /v1/accounts/{program_account_id}/commands/{command_id}`
  reports `engine_command_outcome` `rejected` with its reason and
  `reason_code`.
- A request decided before this change keeps its recorded answer under its
  key.

### 2026-11-02: a step with two loss rules lists an objective for each

- `ChallengeView.objectives` lists one `daily_loss`, `max_drawdown` or `trailing_drawdown` objective for each such
  rule the step authors, each with its own figures and `floor`. A step with two such rules used to list one of them.
  This corrects today's earlier entry: no rule goes unlisted, and any of them can be reached first. The daily-loss
  objectives come first, then the drawdown objectives, each group in ascending order of its rule's `id` in
  `rules_snapshot.rules`, whatever order that list gives them in.
- `profit_target` states the larger target where a step sets both a percentage and an amount, as the pass reads it.
- `profit_target` reads the realized result while a position is open, also on a step whose `pass_measure` counts open
  positions. The pass is decided on that measure.

### 2026-11-02: A trade is listed under the attempt the engine recorded it in

- Each `/trades` row is labelled with the attempt the engine recorded the
  trade in, no longer the attempt whose window holds its close.
- A close that passed a step, or that the passed step recorded after it,
  stays with the passed attempt.
- A next step's trades, and their excursion reads, appear once the step
  goes live, or under its own attempt once it ends without going live.
- Orders, fills and balance changes are still labelled by window.
- Re-read `all_attempts` to correct labels you stored.

### 2026-11-02: a Trader API account states its `commission_per_lot`

- `Account` (`GET /trader/v1/accounts`, `GET /trader/v1/accounts/{account_id}`) gains an optional field,
  `commission_per_lot`: the commission per lot the account pays on a fill, over any its instruments state. It is
  absent where the account sets none. Fills are unchanged: the field states what the engine already charged.

### 2026-11-02: a trailing drawdown past its lock is measured from the locked peak

- `current`, `target`, `remaining`, `progress` and `tone` in the `measured` of a
  `trailing_drawdown` objective in `ChallengeView.objectives` now read from the
  peak the rule's lock holds, as the rule is enforced. Before, they read from
  the raw peak, so once the lock had bound, an account with all its room left
  could read its limit reached. The field's shape does not change.

### 2026-11-02: an account launch names the chart it opens on

- `POST /v1/launch-sessions` and `POST /v1/claimable-launch-sessions` accept an optional `chart` (`symbol`,
  `timeframe`), and the launch's answers state it.
- The terminal opens on it; without it, on the trader's last chart.
- A symbol the account's feed does not offer your company to trade answers `404 unknown_symbol` when the launch is
  created. There is no window or history check.

### 2026-11-02: disconnect, retention, objective and equity descriptions are corrected

- `POST /v1/users/{global_user_id}/disconnect` clears your copies of the
  person's email and name at the answer. The names you gave their accounts
  become the account numbers as each account closes. The description said
  account names were cleared at the answer too.
- A disconnect deletes no record: accounts, orders, positions, trades, balance
  changes, challenges, simulated payouts, KYC reports, historical sessions and
  your audit trail are kept with no time limit. When the person deletes their
  Noctant account, the accounts' records stay too, naming the person only by
  id. The guide said their trading records stayed until they asked Noctant to
  erase their account.
- Webhook events are kept, and a replay reaches events created in the last 30
  days. The guide called this a 30-day replay store.
- A challenge's `objectives` are measured as the trader's challenge pages
  measure them while the account holds no open position. While it holds one, a
  loss objective's `measured` is `null`, and the trader's terminal shows its
  own live estimate. The description said they were measured exactly as the
  trader's pages measure them.
- The account summary's `equity` is absent while a position is open, and the
  Firm API serves no quotes to value one. The description, and the entry of
  2026-10-31, said to value open positions from live quotes.
- No request, response or webhook changes.

### 2026-11-02: `order.rejected` and `order.cancelled` name their code `rejection_code`, and `order.modified` states its `cause` (breaking)

- `order.rejected` and `order.cancelled` state the engine's code as
  `rejection_code` and its sentence as `rejection_reason`, the names the orders
  read (`GET /v1/accounts/{program_account_id}/orders`) and an order's history
  (`modify_refused`) use for the same pair. They no longer carry `code` or
  `reason`. The values are unchanged: preserve a code you do not know, and show
  the sentence without parsing it. `order.expired` keeps its `reason`.
- `order.modified` states `cause` on every change, in place of `reason_code`:
  `trader` (the trader changed the order), `target_position_reduced`,
  `oca_reduced`, `child_woken` or `trail_moved`, the `cause` an order's history
  states for the same change. A trader's change no longer arrives without one.
  Treat a `cause` you do not know as a change you do not tell apart.
- An event recorded before this change keeps its old fields on every
  redelivery and replay: `code` and `reason`, or `reason_code` (absent on a
  trader's change). `cause`, `rejection_code` and `rejection_reason` are
  required in the OpenAPI document; an event recorded before carries the old
  fields instead.
- The REST reads are unchanged on the wire. The OpenAPI document now states the
  orders read's `rejection_code` and `rejection_reason` as strings that are
  never `null`, and the TypeScript SDK types them so.
- In `operator-api-contracts` the pair is one type, `OrderRejection` (read it
  with `code()` and `reason()`), in the field `rejection` of
  `OrderRejectedWebhookEvent`, `OrderCancelledWebhookEvent`,
  `OrderHistoryEntryView::ModifyRefused` and `AccountOrderView` (an `Option`
  there). `OrderModifiedWebhookEvent.reason_code` is `cause: OrderChangeCause`;
  `operator_api_contracts::OrderChangeCause` still names that type. Rust code
  that names the old fields no longer compiles.
- `operator-webhook-receiver` 0.6.0 reads the new fields, and returns an event
  recorded before this change as `Received::Unhandled` with the reason
  `unreadable_data`. 0.5.0 returns a new `order.rejected` or `order.cancelled`
  that way, and reads a new `order.modified`, which has no `reason_code`, as a
  trader's change. Answer `2xx`, upgrade, then replay the window.

### 2026-11-02: the trader's Connections button ends every integration at once

- The trader's Connections button now ends their connection to every operator
  of your organization at once. One revocation, one `connection.revoked` and one
  audit entry are still recorded per operator, as before.
- Your key's disconnect still ends only its own connection.
- In the session-only Trader API, the connections list
  (`GET /trader/v1/connections`) now answers one optional `connection`. The
  disconnect is `POST /trader/v1/connections/disconnect` and still answers
  `200` with one `Connection`, always present. No key can call these
  operations, so no integration changes are needed.

### 2026-11-02: the trader's terminal wears the mark your account's `branding_policy` names

- On an account whose `branding_policy` is `noctant`, the trader's terminal
  wears Noctant's mark. Your name still names the account in the trader's
  account list and in the data-processing line, and your name and logo show on
  the launch and consent screens and the Back control.
- On `co_brand`, `partner_managed` and `white_label`, it wears your mark: your
  logo, else your initials. Before, the terminal wore your mark on every
  account you sponsor, whatever its `branding_policy`.
- No request, response or webhook changes.

### 2026-11-02: `visibility_policy` governs only your launch page

- A launch page follows the launched account's `visibility_policy`:
  `same_sponsor_only` lists the person's accounts sponsored by the same
  operator as the launched account, `same_sponsor_plus_noctant` lists those
  plus the person's direct Noctant accounts, and `all_connected` lists every
  account.
- The trader's own signed-in page is never narrowed by it: it lists every
  account of every active membership, the accounts other firms sponsor
  included. Before, a reload or an account switch on that page could narrow
  its list by the policy of the account it opened.
- No request, response or webhook changes.

### 2026-11-02: you hear and read a challenge's daily-loss pause

- Two new webhook event types, `challenge.daily_loss_lockout_started` and
  `challenge.daily_loss_lockout_cleared`. `GET /v1/webhook-event-types` lists them, and a Default subscription
  includes them; an Explicit list receives them once you add them.
- A challenge's daily-loss rule can pause the rest of its challenge day instead of failing the challenge. Until the
  pause ends, an order that would open a position is refused; closing stays allowed. The pause changes no status:
  the challenge and its account read `active` throughout. Before this change you heard nothing of a pause on an
  account with no working order or open position, and nothing of any pause's end.
- `challenge.daily_loss_lockout_started` carries `rule_id`, `period_start_at` (the challenge day that earned the
  pause), `until` (when it is due to end), `measured_loss`, `threshold` and `baseline` (amounts in the account's
  currency; a limit set as a percentage arrives as the amount it came to), `timezone` and `triggered_at`, which is
  also the envelope's `created`.
- `challenge.daily_loss_lockout_cleared` carries `period_start_at`, `until`, `cleared_at` (the envelope's
  `created`) and `cause`: `period_boundary` when the next challenge day began, `attempt_ended` when another
  challenge took the account. It names the pause by `challenge_id` and `period_start_at`, as its start did.
- `ChallengeView` has `daily_loss_locked_until` and `daily_loss_lock_period_start_at`, and `ChallengeListItem`
  has `daily_loss_locked_until`: present while the pause stands, which can be past `daily_loss_locked_until` until
  the engine has closed the day.
- `AccountTradingStatus`, `SponsoredAccountStatus` and `sponsorship_status` are described as what they are: the
  account's own state. While they read `active`, a challenge's daily-loss pause, a scheduled trading window or a
  simulated payout's pause can still refuse an order that opens a position. Read the first two on the challenge and
  the third on the trader's payout statements (`GET /v1/users/{global_user_id}/payout-statements`).
- All of it is additive: no field, value or event you already receive changes.

### 2026-11-01: a backtest launch hands its trader an account or a challenge

- `POST /v1/historical-launch-sessions` and
  `POST /v1/claimable-historical-launch-sessions` take `account`, required for
  `backtest` and refused for `playback`. It is either
  `{"kind":"account","execution_profile_key","initial_balance","currency"}`,
  checked as `POST /v1/accounts` checks those terms, or
  `{"kind":"challenge","template_id"}`, which pins the revision your company
  is offered when you create the launch (a claimable launch too, not when it
  is claimed).
- The account's currency, and a challenge's, is `USD` for now: any other
  answers `400`.
- A backtest launch without `account` answers `400`. The trader no longer
  picks the profile or the size.
- A launch's `historical_launch` states the `account` it hands.
- A launched challenge's later steps and resets belong to the same launch.
  `GET /v1/historical-sessions` lists each (`launch_session_id` filters the
  whole chain), and each one sends `backtest.trade.closed` and
  `backtest.session.completed` (with its `challenge_status`) to the operator
  that made the launch. A webhook replay re-sends them.
- Disabling that operator's backtest grant on the feed stops a launched
  challenge's next step or reset. The sessions already run stay readable, and
  their webhooks are still sent.
- Every historical session states `account_currency`, and a challenge's
  sessions state `challenge` (`chain_id`, `step_sequence`, `reset_number`,
  `status`).

### 2026-11-01: a backtest trade is sent when it closes (breaking)

- `backtest.trade.closed` is recorded and sent when the trade closes, while
  its session is still open. Before, a session's trades were recorded only
  when the trader finished it, and a session never finished sent none.
- Its `created` is when Noctant recorded the close. Before, it was the run's
  completion, the `completed_at` of its `backtest.session.completed`. Its
  `data` is unchanged.
- `GET /v1/historical-sessions/{backtest_run_id}/trades` lists a session's
  trades as they close, while its `state` is `created`.
- `backtest.session.completed` is recorded after the session's last trade; a
  trade the close itself makes comes with it. Once it is, the trades read
  lists all `trade_count` of them. Deliveries can arrive out of order:
  deduplicate by `id`.
- A session that was open when this change went live sends and lists only the
  trades it closes from then on; its `trade_count` still counts them all.
- Trades recorded before this change keep the `created` they were recorded
  with.

### 2026-11-01: a challenge failed by staff and reinstated is billed the months it lived after its return

- A challenge on a template with `billing_period` `monthly` that staff failed
  while a held check owed an end, then reinstated and failed again, is charged
  the simulated monthly renewals that fell due up to its last end.
- Before, those renewals stopped at the end the first attempt owed, so a month
  the reinstated challenge lived could go uncharged.

### 2026-11-01: a challenge states its objectives as Noctant's pages measure them

- `ChallengeView` has `objectives`: the objectives the challenge's step sets,
  measured exactly as Noctant's own challenge pages measure them for your
  trader, so you can show the same figures. Every answer that carries a
  challenge's view has it: `GET /v1/challenges/{instance_id}`, an attachment,
  a reset, an approval or rejection, and the other lifecycle actions. It is
  `null` until the challenge has opened and its rules are installed.
- Each objective names its `kind`. `profit_target`, `daily_loss`,
  `max_drawdown` and `trailing_drawdown` are simulated money in the account's
  currency. `trading_days`, `profitable_days` and `time_limit` are whole days.
  A step lists only the objectives its rules set, and one drawdown: a step
  with both a total and a trailing drawdown lists `max_drawdown`. Its standing
  constraints (instruments, sizes, hours) are in `rules_snapshot`, as before.
- `measured` holds `current` (the profit made, the loss used, or the days
  counted), `target` (the target, the limit, or the days required),
  `remaining` (what is left before the target or the limit), `progress`
  (from `0` to `1`) and `tone`: `neutral`, `progress`, `warn` (at or past the
  rule's warning level), `danger` (at the limit) or `done`. Money is rounded
  half away from zero to cents, and `progress` to four places.
- A loss objective's `measured` is `null` while Noctant cannot measure it:
  the account holds an open position, the challenge has ended, or the engine
  has not yet stated the figures it is measured from. A profit target and the
  day counts are always measured.

### 2026-11-01: a challenge's objectives are worked out to the exact cent

- `remaining`, `progress` and `tone` in `ChallengeView.objectives` are now
  worked out in exact decimals. Before, a `remaining` on a half cent could read
  one cent low (`4487.93` for `4487.935`), a target or limit met exactly could
  read a `progress` of `1` under the tone before it, and a loss exactly at its
  warning level could read `neutral`. The field's shape does not change.
- `max_drawdown`'s floor does not follow the peak; an applied size increase
  raises it. `warn` is a loss at or past its rule's warning level, or a time
  limit 80% used.

### 2026-11-01: a closed trade has one shape in the trades read and `trade.closed` (breaking)

- A trade in `GET /v1/accounts/{program_account_id}/trades` and the `data` of
  `trade.closed` state the same closed trade, every field required:
  `trade_id`, `position_id`, `symbol`, `side`, `quantity`, `entry_price`,
  `exit_price`, `gross_pnl`, `commission`, `net_pnl`, `entry_order_id`,
  `entry_fill_id`, `exit_order_id`, `exit_fill_id`, `entry_time`,
  `closed_at`, `duration_seconds`, `is_profitable`, `pnl_currency` and
  `close_reason`. Every amount is simulated. The trades read adds
  `account_id`, `org_id`, `program_account_attempt_id` and `protection`; the
  event adds the account's and the trader's identifiers.
- The trades read no longer states `swap` or `correlation_id`. `swap` was
  always `"0"`: a simulated trade pays no financing. `trade.closed` no longer
  defines `swap` or `pnl_pips`; it never stated either.
- `entry_order_id`, `position_id`, `entry_fill_id`, `exit_fill_id`,
  `pnl_currency`, `entry_time` and `duration_seconds` were optional in the
  trades read. They are always stated.
- `entry_price` is the position's average entry price rounded to eight
  decimal places, halves away from zero, in both. On a position built from
  several fills, `entry_order_id`, `entry_fill_id` and `entry_time` name the
  first fill, the one that opened the position. A `trade.closed` recorded
  before this change for such a position states the average unrounded, and a
  replay sends it as recorded.
- `pnl_currency` is one of the currency codes the OpenAPI document lists, as
  `Currency`. `Currency` is described as an account's currency and `currency`
  on `SimulatedCashTotals` as one of the listed codes; they named trading
  runtime config and ISO 4217, and the list also holds crypto codes.
- In `operator-api-contracts`, `AccountTradeView` and
  `TradeClosedWebhookEvent` hold these fields in `trade: ClosedTrade`,
  flattened, so the JSON is unchanged by it. Read `event.trade.entry_price`
  where your Rust code read `event.entry_price`. `pnl_currency` is a
  `Currency`, which the crate exports.

### 2026-11-01: a closed trade states the best and the worst price it reached

- `GET /v1/accounts/{program_account_id}/trades/{trade_id}/excursion` answers, for one closed trade, the best and the
  worst price its position reached while it was open, and the first time each was reached: `favorable_price`,
  `favorable_at`, `adverse_price` and `adverse_at`.
- Prices are the feed's quotes on the side the position closes on: the bid for a long and the ask for a short, before
  your account's spread markup and slippage. These are the prices the trade's stop-loss and take-profit are judged on.
  A long's best is its highest bid; a short's best is its lowest ask.
- The window runs from the quote standing when the position opened to the last quote at or before the trade closed. A
  trade covers its position from the opening, as its `entry_time` says. Each time lies between `entry_time` and
  `closed_at`, to the millisecond. This is the stored market record: a quote counts even when the market was closed
  for trading or the quote did not reach the engine.
- Noctant stores each trade's answer once, after it has stored every quote through the close: usually seconds after
  the close, and within two days when the feed has stopped. `state` is `pending` until then. `final` and `unavailable`
  are stored answers and never change, so keep them. `unavailable` carries its `reason`: `before_history_start`,
  `market_not_served` or `no_quote_before_open`. Backtest trades have no excursion.
- The new webhook event `trade.excursion` carries the same answer, once per trade of a sponsored account, including
  an account that has closed since. Its `created` is when Noctant stored the answer, so it can follow `account.closed`.
  Nothing orders it against `trade.closed`: join them on `trade_id`. Trades already closed are answered and sent too. A
  subscription that is an explicit list receives it once the list names it.
- The read needs the `trades:read` scope. The OpenAPI document and the TypeScript SDK describe the read and the event.

### 2026-11-01: a closed trade's best and worst price can take about two days when the feed has stopped

- Correction to the
  [entry on a closed trade's best and worst price](#2026-11-01-a-closed-trade-states-the-best-and-the-worst-price-it-reached):
  when the feed has stopped, Noctant stores a trade's answer within about two
  days of the close, so `state` can stay `pending` a little past two days. The
  entry said within two days. The journal-app guide now says about two days.
- No request, response or webhook changes.

### 2026-11-01: a finished backtest session states the market period it covered

- A `completed` historical session states `covered_until`: how far into its
  market window the trader replayed. Every bar that closed at or before it was
  shown. It lies between `market_time_start` and `market_time_end`, and equals
  `market_time_end` when the whole window was replayed. An earlier value means
  the session was finished before the end of its window. A launched
  challenge's step that advanced or was reset ends there too, and the next
  session of its chain starts at or after it.
- `covered_until` is on every `completed` session of
  `GET /v1/historical-sessions`, `GET /v1/historical-sessions/{backtest_run_id}`
  and the `session` of `GET /v1/historical-sessions/{backtest_run_id}/trades`.
  It is absent while a session is `created`. Sessions completed before this
  change state it too: Noctant filled it from what each had shown.
- `backtest.session.completed` gains `covered_until` and `created_at`, when the
  trader started the session (the readback's `created_at`). The session ran
  from `created_at` to `completed_at`, time spent parked included. An event
  recorded before this change is sent as recorded, without the two fields.
- The summaries of `GET /v1/historical-sessions` and
  `GET /v1/historical-sessions/{backtest_run_id}` say "backtest sessions": a
  playback launch creates no session.

### 2026-11-01: a historical launch carries your own reference

- `POST /v1/historical-launch-sessions` and
  `POST /v1/claimable-historical-launch-sessions` take an optional
  `external_ref`: your own reference for what the trader replays, such as a
  trade or a setup. It is 1-64 characters from `A-Z`, `a-z`, `0-9`, `_` and
  `-`. Any other value answers `400` `validation_failed`, and no launch is
  made.
- Noctant stores it with the launch and never changes it. It returns it as
  `historical_launch.external_ref` on the launch (its create answer and
  `GET /v1/launch-sessions/{launch_session_id}`, and a claimable launch's
  create answer), as `external_ref` on every historical session of the launch
  and on each of its trades (`GET /v1/historical-sessions`,
  `GET /v1/historical-sessions/{backtest_run_id}` and its `/trades`), and as
  `external_ref` in the `data` of their `backtest.trade.closed` and
  `backtest.session.completed` events. A launched challenge's later steps and
  resets are sessions of the same launch and carry it too.
- On a claimable launch it is the one handle you hold: the `launch_session_id`
  its results carry is made when the trader connects.
- A launch made without `external_ref` answers as before: the field is absent.
- Noctant shows it on no screen and in no email. Send an opaque value, with no
  personal data in it.

### 2026-11-01: a historical launch names the chart it opens on (breaking)

- `POST /v1/historical-launch-sessions` and
  `POST /v1/claimable-historical-launch-sessions` require `chart`, for example
  `{"symbol": "EURUSD", "timeframe": "15m"}`. `symbol` is an instrument of the
  launch's `feed_id`, as `GET /v1/feeds` lists it; letters are read
  upper-case. `timeframe` is one of `1m`, `5m`, `15m`, `30m`, `1h`, `4h`,
  `1d`, `1w` and `1mo`. A launch without `chart` answers `400`
  `validation_failed`.
- The chart is checked when you create the launch (a claimable launch too,
  never when it is claimed), after your grant for the feed and window:
  - `404` `unknown_symbol`: the feed lists no such instrument for your
    company, or none of its history is sealed yet;
  - `400` `no_data_for_period`: its history starts after
    `market_time_start` (`detail` says when), or the window holds no bar of
    it.
  Neither succeeds unchanged. `GET /v1/feeds/{feed_id}/availability` states
  where an instrument's history starts and where it has gaps.
- A `playback` launch opens Chart Replay on the chart, paused at
  `market_time_start`. A `backtest` launch opens the Replay page's start form
  on the chart's symbol, and the session's chart on its timeframe; the
  trader may still pick another symbol. An equity opens on its extended
  session.
- A launch's `historical_launch` states its `chart`.
- The OpenAPI document states the window: a backtest's `market_time_end` is
  the session's end, and its start may move later than `market_time_start`,
  on whole minutes, never earlier.
- `GET /v1/feeds/{feed_id}/availability` answers a symbol the feed does not
  list, or one with no sealed history, `404` `unknown_symbol`; it answered
  `404` `not_found`.
- Historical launches made before this change were removed, with their links
  to the backtest runs they started: those runs are no longer in
  `GET /v1/historical-sessions` or in a webhook replay.

### 2026-11-01: a historical launch without its grant names the grant it needs (breaking)

- `POST /v1/historical-launch-sessions` and
  `POST /v1/claimable-historical-launch-sessions` answer `403`
  `replay_grant_required` when the operator holds no enabled grant for what
  the launch asks; they answered `403` `forbidden` with no `detail`. `detail`
  names the product, the feed and the window. A `backtest` needs a `playback`
  grant and a `backtest` grant.
- Your firm's staff set a grant with
  `PATCH /v1/operators/{operator_id}/replay-product-access`, inside the replay
  access Noctant's platform staff set for your firm. `GET /v1/feeds` states the
  grants the operator holds. The same request answers the same until the grant
  is there.
- The grant is still decided first: a launch refused for it was not checked
  for its chart.

### 2026-11-01: a monthly subscription is no longer billed past an engine breach waiting behind an undecided check

- A challenge on a template with `billing_period` `monthly` whose account the
  engine breached while an earlier check could not be decided yet (a day the
  engine held across its turn, or a close awaiting its valuation) is charged
  only the simulated monthly renewals that fell due up to that breach, whether
  the daily sweep charges them or the challenge is ended first, for example
  failed by staff.
- When the engine breached such an account more than once, the earliest breach
  bounds the renewals, as it does behind a held pass
  ([held pass](#2026-10-31-a-held-pass-no-longer-bills-a-monthly-subscription-past-an-engine-breach-behind-it)).
- Before, every renewal up to the sweep or the end was charged, even after the
  breach had stopped the account.

### 2026-11-01: a paused simulated payout is offered what pays whichever day the firm decides

- On a step whose `payout_approval` mode is `pause_until_decided`, a quote's
  `max_available`, and the same figure in a
  `requested_amount_exceeds_available` refusal, is now what the engine's debit
  takes whichever day the firm approves: the lower of what it takes today and
  what it takes on a later challenge day. A firm's review normally crosses a
  challenge day. A later day starts its daily-loss limit from the balance, and
  an end-of-day trailing peak rises to it, so after a winning day the later
  figure is lower. With a 5% daily-loss limit off a day that started at 10,000,
  1,000 can leave a balance of 11,000 today, and 549.99 from the next challenge
  day on. A paused account cannot trade, so every later day takes the same
  figure. Every payout inside Noctant is simulated.
- That trader used to be offered 1,000 and the account paused on it, and the
  firm's approval on a later day read `approved`, `refused` and
  `would_breach_floor`, with nothing debited. A request for more than the
  figure now reads `rejected` and `not_applicable`, with
  `requested_amount_exceeds_available`, and nothing is paused.
- A request already under review keeps its amount, and its debit is decided on
  the day the firm approves it. `automatic` and `manual_review` steps are
  offered what they were.
- A correction to
  [2026-11-01](#2026-11-01-a-simulated-payout-above-what-the-engine-takes-is-refused-at-the-request):
  on a step that pauses the account, a request made from now on reads
  `would_breach_floor` only if the account moved after the request.

### 2026-11-01: a position tells every stop-loss and take-profit move

- `position.modified` reports each trader move of an open position's stop loss
  or take profit, with previous and new levels, `modified_at` and
  `source_position`; an absent level means no level of the position's own.
  The OpenAPI document publishes its body under `webhooks`, at
  `position.modified`.
- `GET /v1/accounts/{program_account_id}/positions/{position_id}/protection`
  reads the position's opening, moves and trade exits in journal order, with
  `limit` and `next_cursor` for paging.
- Every trade's `protection` gives the first stop loss and take profit, when
  each was set, and the levels in force at the exit, for R and the stop
  distance in price.
- Explicit webhook subscriptions must name `position.modified` to receive it.
- A receiver build without `position.modified` answers `Received::Unhandled`
  with reason `unknown_type`.

### 2026-11-01: a position's events state its average entry price at eight places

- `position.updated` (`previous_average_price`, `new_average_price`) and
  `position.closed` (`entry_price`) state the position's average entry price
  rounded to eight decimal places, halves away from zero, as `trade.closed`
  does. On a position built from several fills they stated it unrounded, up
  to 28 places, so one close carried two entry prices.
- On the Trader API stream, `position_updated` (`average_entry_price`) and
  `position_closed` (`entry_price`) change the same way.
- An event recorded before this change states the average unrounded, and a
  replay sends it as recorded.
- No field is added, removed or retyped. Compare amounts as decimals.

### 2026-11-01: a refused body names the same field every time (breaking)

- A `400` `validation_failed` answer to a body that fails more than one check
  now names the same one in `detail` every time: the first failing field by
  name, and a check on the body as a whole only when no field fails. Before,
  it named one of them at random.
- On `POST /v1/claimable-historical-launch-sessions`, a malformed field's
  `detail` names that field. Before, it answered `__all__ is invalid` about
  half the time.
- In `operator-api-contracts`, `CreateClaimableHistoricalLaunchSessionRequest`
  has no `historical_request_for_external_user`: its own `validate()` makes
  every check. Build a `CreateHistoricalLaunchSessionRequest` yourself where
  your Rust code called it.
- `error` is unchanged: switch on it, never on `detail`.

### 2026-11-01: a relaunch with the same `external_ref` reopens the trader's open backtest

- A `backtest` launch that carries `external_ref` reopens the trader's
  unfinished backtest session from an earlier launch of yours when both
  launches name the same `external_ref`, `feed_id`, `market_time_start`,
  `market_time_end` and `account` (for a challenge, the same published
  revision). When the trader starts from the launch, Noctant opens that
  session where they left it and makes no new session, provided the submitted
  symbols and start time are the ones the trader first started it with (for a
  challenge, its first step's).
- The reopened session keeps the launch that started it: its readback and its
  `backtest.trade.closed` and `backtest.session.completed` events carry that
  launch's `launch_session_id`, with the same `external_ref`. The new launch
  starts no session, so no result names its `launch_session_id`.
- A launched challenge reopens at the step it has reached.
- A finished session is never reopened. Neither is any session when the
  launch has no `external_ref`, names another `feed_id`, window or `account`,
  or comes from another operator, or when the trader starts with other symbols
  or another start time. In each case a launch that has not started a session
  starts a new one, and a used launch answers 409.
- No request or response field changes.

### 2026-11-01: a reset re-sent with its key returns the new challenge's adjudication

- Re-sending `POST /v1/challenges/{instance_id}/reset` with the same request
  and its `idempotency_key` returns the new challenge's `adjudication` exactly
  as `GET /v1/challenges/{instance_id}` states it for that challenge. The
  re-sent reset answered `null` there, as if the challenge bought no post-hoc
  rule, even when the challenge's own read stated its judgment.

### 2026-11-01: a simulated payout above what the engine takes is refused at the request

- A quote's `max_available` is now what the engine would take, on every
  template. With no `required_remaining_buffer` it used to be the available
  profit alone, while the engine also refuses a debit that would reach an
  enforcing daily-loss limit or drawdown floor. A payout counts against the
  day's loss limit, so a trader holding more profit than that limit was offered
  an amount the engine then refused. A template with a
  `required_remaining_buffer` is offered what it was. A step the firm reviews
  by hand is capped the same, though nothing is debited there. Every payout
  inside Noctant is simulated.
- `POST /v1/users/{global_user_id}/payout-requests` for an amount above that
  figure now reads `policy_status` `rejected` and `account_effect_status`
  `not_applicable`, with `requested_amount_exceeds_available` and the
  `max_available` in `policy_decision`, and nothing is debited or paused.
  Before, the request was accepted and its debit refused: it read `refused`
  with `would_breach_floor`, as `approved` on an automatic step and as `rejected`
  on a step that pauses the account.
- `would_breach_floor` stays for a debit decided after the request: an account
  that moved, or a firm's approval on a later challenge day, whose limit may
  take less, on an account that never moved.
- The same request answers `409` `payout_held` while a day's limit cannot be
  measured yet: from the end of the challenge day until the engine's close of
  it is recorded. A flat book closes at the boundary, so that is a moment; only
  a book open at the turn waits for its feed, and an open book is held for
  `flat_account` first. "This simulated payout is on hold
  for a moment while the account's latest activity is processed. Nothing was
  recorded; send the same request again shortly." A template with a
  `required_remaining_buffer` used to record a refusal here, with a
  `max_available` of 0.

### 2026-11-01: a simulated payout's refusal states the amount it offers

- In a `requested_amount_exceeds_available` refusal, `max_available` is now the
  same figure as the quote's `max_available`, on every template. It used to
  leave out a step's cycle limit and a tier's `max_gross_amount`, so with either
  below the available profit the refusal stated more than the quote offered,
  and a request for that figure was refused again, lower. A refusal recorded
  before keeps its figure. Every payout inside Noctant is simulated.
- A correction to
  [2026-11-01](#2026-11-01-a-simulated-payout-above-what-the-engine-takes-is-refused-at-the-request):
  `409` `payout_held` answers while a day's limit cannot be measured unless
  another gate refuses the request. A request short of its trading days, for
  example, reads `rejected` with `insufficient_trading_days` in that window.

### 2026-11-01: a step's daily-loss rules share one day-start reference (breaking)

- A step's `max_daily_loss` rules now share one `day_start_reference`, on every
  org. An account's day starts from one figure, so a step whose rules named two
  references measured all of them from the reference of the rule with the lowest
  id, whatever the others stated. With a position open at the turn, a rule that
  named `equity` could read its limit off the balance.
- `POST /v1/templates` and `PATCH /v1/templates/{template_id}` for a step whose
  `max_daily_loss` rules name two references now answer `400`
  `validation_failed`, with the two references in `detail`. Two daily-loss rules
  that name the same reference are accepted as before.

### 2026-11-01: A trader or a partner can end a connection

A trader or a partner can now end a connection. Use
`POST /v1/users/{global_user_id}/disconnect` (scope `users:disconnect`, in
`full`, `trading_only` and `historical_replay`), or the trader's Connections
button.

- You can no longer act for the person. You keep reading the accounts you
  sponsored, as history, and their webhooks run until the last account closes.
- A change to the person's accounts answers `404`, a reset or next step
  included; provisioning the person, creating an account for them, a new KYC
  report and a direct launch answer `409` `connection_revoked`. A launch you
  minted earlier no longer redeems. A request already accepted under its key
  still answers what it recorded.
- A firm's staff are the org's own governance over accounts in its org, not the
  partner the connection names, and they stay until the accounts close. No
  reset or activation starts for them either: an activation asked before the
  disconnect answers `activation_refused` with reason `connection_revoked`.
- Its open sponsored accounts close at the executable price, with reason
  `connection_revoked`.
- An open challenge on them ends without a pass, a reset or a refund.
- A payout still under review is withdrawn.
- The `account.closed` events are recorded first and `connection.revoked`
  last. After it, nothing about the person is sent or replayed to that
  partner while they are disconnected, but for `trade.excursion`, sent when
  its answer is stored. A backtest you launched stays readable, a session
  still open included. A retried delivery can still arrive after it.
  `connection.revoked` itself is never replayed: if you missed it, read
  `GET /v1/users/{global_user_id}`.
- Your copies of the person's name, email and account names are cleared.
- The person reconnects only through a claimable launch they accept. Until
  their accounts have closed, the launch answers `409` `connection_closing`.
- `GET /v1/users/{global_user_id}` and the disconnect answer `connection`
  (`connected`, `disconnecting` or `disconnected`) and, from the disconnect,
  `disconnected_at`. The same key answers the same disconnect, even after the
  person connected again.
- Each disconnect records one entry in your audit trail
  (`GET /v1/audit-events`, read by a staff session, not a key):
  `operator_api.user.disconnect` by your key,
  `trader_api.connection.disconnected` by the trader. Its `target_id` is the
  person; its `detail` holds `revocation_id` and `operator_id`, and no name or
  email. The same key sent again records none.
- `connection.revoked` is now sent, and an explicit subscription can name it.
- `operator-webhook-receiver` 0.5.0 decodes it; 0.4.0 and earlier return it as
  `Received::Unhandled` (`unknown_type`).
- `operator-api-client` gains `disconnect_user`.
- `full`, `trading_only` and `historical_replay` now hold
  `users:disconnect`. An operator keeps the scopes it holds, so one set up
  earlier does not have it. An app that requires exactly `trading_only`'s scopes
  at startup, as an account-creating journal app does, refuses to start until
  it has: the upgrade note says what to do.

### 2026-11-01: `account.trading_suspended` no longer carries `description` (breaking)

- `account.trading_suspended` has no `description` field. It repeated free
  text: the note sent with a status change, or the description of the rule
  that stopped trading. Its `reason` reads `administrative` on every
  suspension, so it does not say why trading stopped. `suspended_by` names
  who placed a hold (`operator` or `staff`), and `challenge.status_changed`
  tells an attempt's stop. The event's other fields are unchanged. The
  OpenAPI document and the TypeScript SDK no longer list `description`.
- The `reason` you send to disable an account with
  `PATCH /v1/accounts/{program_account_id}/status` is still required, and
  stays with your request: it is the hold's note, in your audit trail
  (`GET /v1/audit-events`, `detail.reason`). Noctant no longer repeats it in
  the account's engine records or in this event.
- An event recorded before this change still carries `description` on every
  redelivery and replay.
- `operator-webhook-receiver` 0.5.0 reads the event without `description`;
  Rust code that names the field no longer compiles. 0.4.0 returns the event
  as `Received::Unhandled` with the reason
  `unreadable_data`: answer `2xx`, upgrade, then replay the window. 0.3.0
  and earlier refuse it.

### 2026-11-01: an account's balance history is one read, change by change

- New `GET /v1/accounts/{program_account_id}/balance-changes` (scope
  `account_summary:read`): every change to the account's simulated balance,
  oldest first. Each change states `transaction_type`, `amount`, `currency`,
  `previous_balance`, `new_balance`, `changed_at`, the attempt whose window
  holds it (`program_account_attempt_id`), and the trade, order and position
  it belongs to where it has one. `execution_scope` and `attempt_id` choose
  the attempts as they do on the trades and orders reads; `all_attempts`
  reads the whole history, the opening balance first.
- `sequence` numbers an account's changes 1, 2, 3… with no gap, and a change
  recorded later always has a higher number. Page with `limit` and the
  `after_sequence` of the `next_cursor` the answer returns. At `all_attempts`
  and at one named `attempt`, reading on from it never skips or repeats a
  change. Without `execution_scope`, each request reads the attempt current
  when it arrives: a reset between two pages moves the read to the new
  attempt, and the ended attempt's changes after the cursor, its reset
  `adjustment` included, are not returned at that scope. A sync that keeps
  every change reads `all_attempts`.
- A reset's `adjustment` is the last change of the attempt it ends, read at
  `all_attempts` or by naming that attempt; that attempt's result is the
  `new_balance` before it.
- A change's `source_position` equals that of the `account.balance_changed`
  event for it. The opening balance is change 1 and has no
  `source_position`. A balance correction's or a simulated payout's
  `execution_event_id` is the `event_id` of the change it made.
- The read states balance only: Noctant does not value open positions.
- The OpenAPI document lists the currency codes as one `Currency` schema,
  which this read's `currency` and the `currency` of each total of
  `GET /v1/users/{global_user_id}/simulated-cash` name.

### 2026-11-01: an account's fills state what each one cost and which positions it moved

`GET /v1/accounts/{program_account_id}/fills` (`listFills`, scope `trades:read`)
reads the account's fills, newest first, paged with `before_filled_at` and
`before_fill_id` and scoped with `execution_scope` and `attempt_id` as the trades
read is. A fill is its order's whole execution: `fill_quantity` at `fill_price`
at `filled_at`. Each fill states its simulated `commission` in
`commission_currency`, the account's currency, and its `position_effect`:
`opened`, `added`, `reduced` or `closed` with its `position_id` and, for a reduce
or a close, the `trade_id` it made; or `reversed`, a netting fill that closed
`closed_position_id` by `closed_quantity`, made `trade_id`, and opened
`opened_position_id` with `opened_quantity`. Store a fill under `fill_id`, which
never changes. A fill recorded later can share the newest instant you hold, so an
incremental read goes from the head until it has passed every fill at that
instant.

### 2026-11-01: an order states every change made to its terms

- New `GET /v1/accounts/{program_account_id}/orders/{order_id}/history`,
  read with `orders:read`: the order as the engine accepted it, then each
  change to its terms in journal order, each with `at` and
  `source_position`. A change names its `cause` (`trader`, or the engine's
  `target_position_reduced`, `oca_reduced`, `child_woken` or `trail_moved`)
  and, for each field it changed, `previous` and `new`. A change the engine
  checked against the order and refused is its own entry, with what was
  asked, `rejection_code` and `rejection_reason`; a change refused before that
  check, such as a request that was too old when the engine reached it, has
  none. A stop-limit order's trigger is an entry with its `trigger_price`.
- `order.modified` and this read are written independently: a read made as
  the webhook arrives may not hold that change yet.
- Page it with `limit`; when `next_cursor` is present, pass its
  `after_journal_sequence` and `after_event_ordinal`.
- An order the engine rejected at placement has no entries;
  `GET /v1/accounts/{program_account_id}/orders` states why it was refused.
- This read and the position protection read share one query and one cursor
  (breaking for code that names them): in `operator-api-contracts`,
  `ListPositionProtectionQuery` is `ListJournalHistoryQuery`, and the schema
  `PositionProtectionCursor` is `JournalHistoryCursor` in the OpenAPI document
  and the TypeScript SDK. Their parameters and JSON are unchanged.

### 2026-11-01: `connection.revoked` joins the webhook event types

- A new webhook event type, `connection.revoked`. `GET /v1/webhook-event-types`
  lists it, and a Default subscription includes it.
- Nothing sends it yet, and an explicit subscription cannot name it yet: a
  list that names it answers `400`.

### 2026-11-01: webhook, challenge status and order side descriptions are corrected

- `order.cancelled` also comes when the engine refuses a close it was asked to
  make. That order was never accepted, so no `order.accepted` comes before its
  `order.cancelled`, and its `cancelled_by` is `system`. The description said
  only that a working order ended unfilled because it was cancelled.
- `cancelled_by` `system` is every cancel the platform makes of its own, such
  as at a breach or stop-out, a daily-loss lock, a rule-set change, a passed
  step's lock, an attempt's suspension, an erased trader's flatten, a close or
  fill it refused, or a sibling, parent or target position that went; `code`
  names which. The description named four of those causes.
- `account.closed`'s `final_balance` is the engine's balance at the close for
  the `reason` `owner_erased` too, as for `operator`, `staff` and
  `program_ended`.
- `order.modified`'s `reason_code` `TARGET_POSITION_REDUCED` means the position
  the order closes shrank: a hedging order's ticket, or a netting reduce-only
  order's net position. It named the hedging ticket alone.
- The challenge status `cancelled` is a waiting challenge ended while it
  waited: by your firm or staff, by its trader's erasure, or by a breach of the
  passed step it waited to follow. `challenge.status_changed`'s `reason` says
  which: `waiting_cancelled`, `owner_erased` or `passed_step_breached`. The
  description named your firm and staff alone.
- A waiting challenge starts by itself when a slot frees, oldest first among
  the challenges that may start: a next step its previous step still holds
  keeps its place, and its program's later challenges wait behind it. The
  descriptions of the challenge status `awaiting_slot` and the live-limit
  choice `allow_waiting` said "oldest first" alone.
- An order's `side`: `buy` opens or adds to a long position, or reduces or
  closes a short one; `sell` opens or adds to a short position, or reduces or
  closes a long one. `buy` read "going long".
- No request, response or webhook changes.

### 2026-11-01: your firm wears one logo, set once for all your operators

- Your logo is now your firm's, not one operator's. Your traders see it beside
  your firm's name in the terminal's top bar and on the launch pages, whichever
  of your operators sponsored the account or minted the launch. This is the change announced on
  [2026-10-30](#2026-10-30-your-traders-see-your-firms-name-never-an-operators).
- Set or clear it with `PATCH /v1/brand-logo`, with the same body as before
  (`{"change": "set", "image_base64": "…"}` or `{"change": "clear"}`), and read
  it with `GET /v1/brand-logo`. Both answer `{"logo": …}` and need a staff
  session.
- `PATCH /v1/operators/{operator_id}/brand-logo` is gone and answers `404`.
  An operator's detail (`GET /v1/operators/{operator_id}`, and the answer of
  `POST /v1/operators`) no longer carries `brand_logo`.
- A logo one of your operators wore carries over to your firm; if several
  wore one, the one set last carries over.
- A change of the logo is audited as `admin.organization.brand_logo_updated` on
  your organization. `admin.operator.brand_logo_updated` is no longer written.
- An operator's `name` in `GET /v1/me` is the same staff label as in
  `GET /v1/operators`. No trader sees it.

### 2026-11-01: your traders can go back to your app after a launch

- After a successful launch, Noctant keeps the launch's `return_url` and shows
  "Back to …" with your name on every page of that session. If your app opened
  Noctant with `window.open` and kept `opener`, the control brings your window
  to the front and asks Noctant's window to close; otherwise `return_url` opens
  in Noctant's window. A launch without `return_url` shows no control, and a
  reload of Noctant drops it.
- No request or response field changes.

### 2026-10-31: a deadline that falls due while a challenge's check is held suspends its account

- A time limit or inactivity deadline that falls due while a
  `flag_for_review` flag is unresolved, or while the judge cannot judge the
  attempt, now suspends the account and closes its positions, as an expiry
  does, unless the attempt would pass first (for example a time limit met
  under `pass_if_met_at_deadline`). Before, the account kept trading until the
  hold was resolved.
- Your firm receives `account.trading_suspended` (reason `administrative`,
  description `challenge_deadline_owed`), then the challenge's status change
  to `suspended`.
- The verdict (`expired`, or `failed` where the step's inactivity fails the
  attempt) follows when the hold is resolved, on the reading frozen at the
  deadline.

### 2026-10-31: a held pass no longer bills a monthly subscription past an engine breach behind it

- A challenge on a template with `billing_period` `monthly` whose pass is owed
  behind a held check, and whose account the engine then breached, is charged
  only the simulated monthly renewals that fell due up to that breach when it
  is ended while the hold stands, for example failed by staff
  ([held renewals](#2026-10-31-a-monthly-subscription-is-billed-through-the-end-a-held-check-comes-to-never-past-it)).
- Before, it was charged every renewal up to that end, even after the breach
  had stopped its account.

### 2026-10-31: a key lists the challenges of the accounts it sponsors

- `GET /v1/challenges` serves an API key with `challenges:read`. It lists
  the challenges of the accounts your integration sponsors, newest first:
  exactly the challenges `GET /v1/challenges/{instance_id}` reaches for
  that key. Before, it served only a firm's staff session and refused a key
  with `403`.
- Filter by `program_account_id` (new), `global_user_id` or `status`, and
  page with `limit` and the `next_cursor` the answer returns. Each listed
  challenge names its `program_account_id`, so a challenge whose id you did
  not keep is found again from its account or its trader.
- `status`, in the filter and in each listed challenge, is the status set
  `ChallengeView.status` uses. A value outside it is refused `400`
  (`validation_failed`).

### 2026-10-31: a monthly subscription is billed through the end a held check comes to, never past it

- A challenge on a template with `billing_period` `monthly` whose check is held
  is no longer charged, while the hold stands, the simulated monthly renewals
  that fall due after the end its held checks come to: its time limit or
  inactivity deadline
  ([held deadlines](#2026-10-31-a-deadline-that-falls-due-while-a-challenges-check-is-held-suspends-its-account)),
  or an engine breach or a pass they reach first. A renewal that falls due at
  that end's own instant is charged; what follows the end is charged as for
  the same challenge with no hold.
- If such a challenge is ended while a pass stays owed behind the hold, for
  example failed by staff, the renewals that fell due up to that end are
  charged, as its account kept trading.
- Before, every renewal until the hold was resolved was charged, even past an
  expiry, failure or pass dated before it.
- A replay of such a challenge charges the same months, however far the
  trader steps or walks it at once.

### 2026-10-31: a simulated payout request keeps its create reference (breaking)

- Approving, rejecting or updating the external settlement of a simulated
  payout request no longer replaces the statement's `operator_reference`: it
  stays the value sent at create, or none. Repeating the create with the same
  key, amount and reference now returns its recorded statement after those
  actions too; a different amount or create reference still answers `409`
  `conflict`.
- A review's reference is in `policy_decision.operator_review.operator_reference`
  and in its audit event; a settlement update's `operator_reference` is in its
  audit event, which a staff session reads. The statement's
  `external_settlement_reference` still tracks the payment.
- The OpenAPI document and the TypeScript SDK describe these fields. A
  reference an action replaced before this change is not restored.

### 2026-10-31: a template's required inputs are required in the document, and its `settlement_mode` is one of two values

- The OpenAPI document and the TypeScript SDK now mark as required every
  input `POST /v1/templates` and `PATCH /v1/templates/{template_id}` already
  refused to go without. A typed client can no longer leave one out:
  - the template's `challenge_price`, `currency`, `leverage`,
    `execution_profile_key`, `reset_policy`, `template_type`, `live_limit`,
    `requires_activation` and `billing_period`;
  - each step's `fresh_account`, `trading_day_mode`, `payout_peak_policy`,
    `payout_availability`, `payout_tiers`, `payout_approval`,
    `pass_decision`, `pass_measure`, `deadline_with_targets_met`,
    `pass_approval`, `after_pass`, `challenge_day_boundary_time` and
    `challenge_day_boundary_timezone`.
- A body without one of them is still refused `400` `validation_failed`,
  now when the body is read. The detail names the field by its place in the
  body (`steps[0].fresh_account is required.`, before
  `step[0].fresh_account is required`). A `null` for one is refused
  `400` `validation_failed` too.
- `settlement_mode` in a template read (`GET /v1/templates`,
  `GET /v1/templates/{template_id}`) and in the template a create or an
  update answers with is now the `SettlementMode` enum: `simulated` or
  `operator_settled`, the only two values it ever held. The SDK types it so;
  the request's `settlement_mode` is unchanged.
- No valid request and no response value changes.

### 2026-10-31: amounts a field cannot hold exactly are refused, never rounded (breaking)

- `price` on `POST /v1/products`, `amount` on
  `POST /v1/users/{global_user_id}/account-corrections`, and
  `initial_balance` on `POST /v1/accounts` and
  `POST /v1/claimable-launch-sessions` now answer `400` `validation_failed`
  when the value cannot be stored unchanged. `detail` names the field.
- `price` allows at most 16 whole digits and 2 decimal places. `amount` and
  `initial_balance` allow at most 10 whole digits and 8 decimal places.
  Trailing zeros do not count: `"99.90"` remains a valid price. A claimable
  launch's `initial_balance` is checked when the launch is created, not first
  when the trader's browser connects.
- Send computed values already at the field's precision; the API does not
  round them. Before, a finer price was stored rounded to the cent, and a
  finer correction moved the balance by its rounded amount, so repeating it
  with the same key answered `409` `conflict`. A value that rounded to zero or
  exceeded the field's range could answer `500`.

### 2026-10-31: an account summary states equity only while the account is flat; a position carries no open P&L

- `GET /v1/accounts/{program_account_id}/summary`: `equity`, `free_margin` and
  `margin_level` are optional. They are present while the account holds no
  open position (equity is then the balance) and absent while a position is
  open. They used to be present always, with every open position's P&L read as
  zero, so equity read as the balance however the open positions stood. A
  strict client generated from the document treats `equity` and `free_margin`
  as optional from now on.
- `GET /v1/accounts/{program_account_id}/positions`: a position no longer
  carries `unrealized_pnl`. It was always `"0"`, never a valuation. Value an
  open position from live quotes.
- `GET /trader/v1/accounts` and `GET /trader/v1/accounts/{account_id}`:
  `equity` and `free_margin` are optional on the same rule: present while the
  account holds no open position, absent while one is open.

### 2026-10-31: an order states why the engine ended it, and an account its execution terms

- `GET /v1/accounts/{program_account_id}/orders`: a `rejected` or `cancelled`
  order states the engine's `rejection_code` (the `code` that
  `order.rejected` and `order.cancelled` carry) and its sentence,
  `rejection_reason` (their `reason`). Switch on the code; show the sentence,
  never parse it. No other order states either.
- Every account read (`POST /v1/accounts`, `GET /v1/accounts`,
  `GET /v1/accounts/{program_account_id}` and
  `GET /v1/accounts/by-external-id`) states the account's execution terms,
  set when the account opens and never changed:
  - `leverage`: `100` is 1:100;
  - `position_mode`: `{"mode": "netting"}`, or `{"mode": "hedging"}` with its
    `hedge_margin` method: `gross`, `largest_leg`, or `offset` with its
    `factor`;
  - `spread_markup_pips` and `slippage_pips`, as decimal strings;
  - `feed_id`: the market-data feed the account trades on.
- The OpenAPI document and the TypeScript SDK describe these fields.

### 2026-10-31: `BalanceSourcePosition` is `JournalSourcePosition` (breaking)

- In `operator-api-contracts`, `BalanceSourcePosition`, the type of
  `source_position` on `account.balance_changed`, is renamed
  `JournalSourcePosition`: it is the journal position of the event that carries
  it. Its fields, its checks and its JSON are unchanged; its two error messages
  read `journal source` where they read `balance source`. Rename the type where
  your Rust code names it. Nothing else changes.

### 2026-10-31: feed discovery and history availability

The new `feeds:read` scope authorizes two reads for Firm API keys:

- `GET /v1/feeds` lists every feed the firm lists an instrument on or this key may replay, its effective playback and
  backtest windows, and each listed instrument's tick size, pip size and contract size as decimal strings, all as of
  `as_of`.
- `GET /v1/feeds/{feed_id}/availability` answers for one symbol, timeframe and mode, with a session for equities:
  where its history starts, its sealed bars inside the effective window, bar gaps and feed-clock gaps. Each gap
  channel is paged independently with `next_before`.

The Full and Historical replay presets include `feeds:read` in catalog version 3. An existing key gains the scope
only when its scopes are set again.

### 2026-10-31: one receiver's failures, slowness or pause no longer delay another's webhooks; Retry-After is honoured up to 48 hours

- Each receiver's webhooks are sent in turn. A receiver that keeps failing,
  answers slowly, or is paused for a signing-secret rotation no longer
  delays delivery to any other receiver. Before, one such receiver could
  hold every partner's deliveries for up to about 79 hours.
- A receiver is sent at most one webhook at a time, in the order Noctant
  recorded them. An event waiting to be retried can still arrive after later
  events, as before: keep ordering by the payload's own positions.
- A `Retry-After` on `429` or `503` is honoured up to 48 hours, the longest
  wait between attempts. A longer value waits 48 hours. The attempt budget
  and the backoff are unchanged.
- While delivery is paused for a signing-secret rotation, events wait. One
  recorded more than 30 days before delivery resumes is not sent, and replay
  cannot reach it. Read it from the API.
- No request, response or webhook body changes.

### 2026-10-31: read what your trader's challenges spent, in simulated money

- `GET /v1/users/{global_user_id}/simulated-cash` (scope `payouts:read`)
  answers the user's simulated cash on the accounts you sponsor: one total per
  currency in `totals`, and one per challenge chain and currency in
  `by_chain` (`chain_id` is `ChallengeView.chain_id`). These are the numbers
  as the trader reads them in the app's History ("Simulated: spent · earned ·
  refunded"), for the accounts this key sponsors; the app's History also
  counts the trader's other accounts.
  - `spent`: the simulated fees recorded. A priced template you attach
    records its price; a reset or an activation the trader asks for records
    its fee; a monthly template records each renewal. A reset or activation
    your key or your staff make records none.
  - `earned`: simulated payouts, the trader's share. `refunded`: simulated
    fee refunds, with the first payout at the template's `fee_refund_pct`,
    and the whole fee of a challenge cancelled before it started. `bonuses`:
    pass bonuses. `net`: `earned + refunded + bonuses - spent`.
  - Money in two currencies is never added: each currency has its own total.
- Every figure is simulated: nothing is charged or paid. A real fee you charge
  your trader is yours, outside Noctant.
- `404` when you sponsor no account of the user. A closed account's history
  stays in the answer.
- `ChallengeView.purchase_price_snapshot` is the template's price when the
  challenge started, not what was recorded: a reset your key makes still
  shows it, and it does not change when a fee is refunded. The recorded spend
  is this read.

### 2026-10-31: template authoring refuses decimal values persistence would change (breaking)

- `POST /v1/templates` and `PATCH /v1/templates/{template_id}` now answer
  `400` `validation_failed` when an authored decimal cannot be stored
  unchanged. `detail` names the field, including its step/rule position.
- Money fields allow at most 16 whole digits and 2 decimal places.
  `profit_split_pct`, `fee_refund_pct` and `payout_best_day_pct_cap` allow at
  most 3 whole digits and 2 decimal places. `profit_target_pct`,
  `consistency_pct`, `pass_bonus_pct` and `warning_threshold_pct` allow at
  most 4 whole digits and 4 decimal places. Existing sign and percentage
  ranges still apply; `warning_threshold_pct` remains a fraction greater than
  zero and below one. Trailing zeros do not count: `"100.000"` remains
  accepted wherever 100 is valid.
- Send computed values already representable at the stated precision; the
  API does not round them. Previously excess fractions were rounded silently,
  and values rounded to zero or beyond the column range could answer `500`.

### 2026-10-31: the header-name constants are in `operator-api-contracts` (breaking)

- `operator-webhook-receiver` no longer exports `EVENT_ID_HEADER`,
  `EVENT_TYPE_HEADER` and `SIGNATURE_HEADER`. The three header names are one
  constant each in `operator-api-contracts`: `WEBHOOK_EVENT_ID_HEADER`,
  `WEBHOOK_EVENT_TYPE_HEADER` and `WEBHOOK_SIGNATURE_HEADER`. Import them from
  there where your Rust code names them. Their values, and the headers Noctant
  sends, are unchanged.

### 2026-10-31: the OpenAPI document publishes every webhook's signed body

- The OpenAPI document has `webhooks`: one entry per event type, keyed by its
  `type` (`account.created`, `order.filled`, and so on). Each states the signed
  body, the headers `X-Noctant-Event-Id`, `X-Noctant-Event-Type` and
  `X-Noctant-Signature`, and how Noctant reads your answer.
- An event type's body is its envelope, named after its data:
  `AccountCreatedWebhookEventEnvelope` has `id`, `type`, `schema_version`,
  `created` and `data`, and its `data` is `AccountCreatedWebhookEvent`.
  Generate your receiver's types from the document instead of writing the
  envelope or a payload by hand.
- Types generated from the document read the event types and fields of the day
  you generated them. Judge a delivery by its envelope alone, and answer `2xx`
  to a signed, in-org, well-formed event whose `data` your types cannot read.
- `WebhookDeliveryView`, a row of `GET /v1/webhooks/deliveries`, is one
  delivery attempt, not a webhook body: its `event_id` and `event_type` are the
  body's `id` and `type`. Its description now says so.
- Nothing Noctant sends changes: the bodies, the headers and the signature are
  as before. An event type added later is in `webhooks` the day it lands.

### 2026-10-31: the OpenAPI document states what a challenge's view holds and which challenges a reset takes

- `POST /v1/challenges/{instance_id}/reset` takes only a failed or expired
  challenge, and only as its template's reset policy allows; any other status
  is refused `409` (`conflict`). Its summary said it reset an eligible active
  challenge. The rule itself has not changed.
- `ChallengeView` no longer says it mirrors an internal record exactly. It is
  the Firm API's view of one challenge attempt, and a fact Noctant keeps about
  an attempt is in it only when the view names it.
- `rules_snapshot` is the step's sold terms read in today's shape: a setting
  added after the attempt was sold reads as the value Noctant states for
  attempts sold before it. Its description said "exactly as published".
- No request, response or webhook changes.

### 2026-10-31: two account field descriptions are corrected

- A hedging `position_mode` is described as "Each opening fill opens its own
  position": a fill that reduces or closes a position opens none. It read
  "Each fill opens its own position".
- `slippage_pips`, on `OperatorAccountView` and on the Trader API's
  `Account`, is no longer described as "Zero on an instant profile.":
  neither API states a slippage model. Correction to the
  [entry on `slippage_pips`](#2026-10-28-a-trader-api-account-states-its-slippage_pips),
  which said the same.
- No request, response or webhook changes.

### 2026-10-31: webhook messages already owed are delivered after webhooks are switched off

- A webhook message is owed when it is created, and only while your company
  holds `webhooks`. A message owed is delivered whatever the ticks say later:
  one held while you rotate your signing secret, the remaining pages of a
  webhook replay, and one still waiting in the queue all finish after
  webhooks are switched off.
- A webhook replay (`POST /v1/webhooks/replays` and its next page) is never
  refused for the `webhooks` tick: it re-sends only messages created while
  webhooks were on.
- While webhooks are off no message is created. Switching them back on does
  not send that period's messages, by delivery or by webhook replay; read that
  data through the Firm API.
- Setting or clearing your webhook address and setting your webhook
  subscription are no longer refused `feature_not_enabled`.
- The readiness report no longer blocks on webhook or replay-feed setup of a
  feature your company does not hold: it lists it as a warning, keeping the
  code.

### 2026-10-30: a billing plan is a named plan priced by lines (breaking)

- `GET /v1/billing/plans`: each plan names itself with `plan_id` and `name`
  instead of `tier`, and `included_evaluations` is replaced by `lines`, one
  per `meter` (an enum; `run_starts` today), each with an `allowance` tagged
  by `kind`: `unmetered` (every use counted, none refused or charged), or
  `included` with `units` a month and, beyond them, `beyond` tagged by `kind`:
  `refuse` (the start is refused, as before) or `bill` with `unit_price` USD
  each. Your history shows your former tier's terms as a plan of that name.
- `GET /v1/billing/statements`: `quota` is removed. A statement's `lines`
  holds the fee of each plan over its days (`plans`) and each line of the
  plan in force on the month's last day (`lines`), with what it included,
  billed and charged; `total` adds both. `started_count` and
  `unclassified_count` are unchanged.
- A simulation credit lot's `source` lists two more values, `allowance` (a
  trader level's monthly allowance) and `company` (granted by the trader's
  company). Nothing writes them yet; a strict client generated from the
  document should accept them before they appear.

### 2026-10-30: a challenge attachment or reset replays its key after the account closes

- Re-sending `POST /v1/challenges` or `POST /v1/challenges/{instance_id}/reset`
  with the same request and its `idempotency_key` returns the recorded
  challenge even after the sponsored account closed. It answered `404` once
  the account closed, without reaching the recorded request.
- A new attachment on a closed account answers `409` `conflict`, and a new
  reset of a closed account's challenge answers `410` `account_closed`, unless
  an earlier refusal answers first: `410` `person_erased` for an erased trader,
  or `409` `conflict` when the template's reset policy refuses the reset. All
  of these answered `404`. Neither starts a run or reopens the account. The
  account must still be one your operator sponsors for that trader, with
  Noctant enforcing its rules.

### 2026-10-30: a claimable launch recognizes an already connected browser

- A claimable launch now connects without the consent click when the browser is
  already signed in as the person your operator user is connected to, as a linked
  launch does.
- A first connection, and a person who signs in on the launch page, still confirm.
- No request or response field changes.

### 2026-10-30: a credit counts one kind of use, and a trader's level says what each use costs (breaking)

- A trader holds a level in your company: its default level, or one Noctant's
  staff give them for a time. What each kind of use costs (`use`: `live_run`
  or `replay_session`) is a term of the level: `metering` is `unmetered`
  (nothing is counted or spent) or `metered` with `monthly` units each
  calendar month (UTC). Only a live run may be counted today, and only on an
  org that holds credits: every term of your company's levels is
  `unmetered`, so your traders spend no credit, and a replay session costs
  nothing.
- `GET` and `POST /v1/simulation-credits/settings`: the answer is the settings
  under `settings`, beside `terms`, one row per level and use (`tier_id`,
  `use`, `metering`). `free_credits_per_month` is removed from both the
  answer and the request body: a level's monthly allowance is its terms,
  which Noctant's staff set.
- A trader's credits (`GET /v1/users/{global_user_id}/simulation-credits`,
  and the answers of a grant and a refund): `balance`, `free_this_month` and
  `next_free_at` are replaced by `by_use`, one row per `use` with `metered`
  (`balance`, `monthly`, `allowance_this_month`, `next_allowance_at`), `null`
  when the trader's level does not count that use. Each lot names its `use`.
  A spend's `instance_id` is replaced by `on`, tagged by `use`: `live_run`
  with `instance_id`, or `replay_session` with `session_id`.

### 2026-10-30: a database that could not answer is 503, and a database failure is 500

- Any Firm API operation answers `500` `internal_error` when its database
  refused or could not read what it asked: a write a stored rule refused that
  the operation has no refusal of its own for, a row it expected that is
  missing, a row it cannot read. It answered `503` `service_unavailable`, which
  told you to send again a request that answers the same again.
- Every challenge operation answers `503` `service_unavailable` when the
  challenge service's database could not answer (it lost its connection, was
  restarting, or ended the request in a deadlock): send the same request again,
  with the same key. It answered `500` `internal_error`.
- The Trader API answers `503` `service_unavailable`, with `Retry-After`, when
  its database could not answer: send the same request again, with the same
  key. It answered `500` `internal`.

### 2026-10-30: a priced template runs on an account you sponsor, and its fee is recorded as simulated

- `POST /v1/challenges`, `POST /v1/products` and `POST /v1/accounts` with a
  `product_id` no longer refuse a template because its price is not zero. A
  template runs on an account you sponsor when it is in the account's currency
  and every step stays on that account; `400` `template_not_sponsorable` names
  only those reasons now.
- Attaching a priced template records its displayed fee as the run's
  simulated `challenge_purchase`, as every run on Noctant shows its fee. Its
  resets record their simulated reset fees, its first payout refunds the share
  of the fee the template names, and a cancel before it starts returns the
  fee, as for a trader's own purchase. The fee is realism: nothing is charged,
  and Noctant takes none of it. A free template records no fee, as before.

### 2026-10-30: a template's payout availability names no profit basis

- `payout_availability` on `POST /v1/templates`, `PATCH
  /v1/templates/{template_id}`, `GET /v1/templates/{template_id}` and `GET
  /v1/templates/{template_id}/authoring` has two fields,
  `max_request_pct_of_available` and `required_remaining_buffer`.
  `profit_basis` and its values `unrestricted` and `balance_above_initial` are
  gone: every payout is capped at the profit the attempt earned whichever value
  a template named, so the field decided nothing.
- A request that still sends `profit_basis` is accepted and the key is
  ignored. A client generated from the earlier document that validates
  responses expects a required `profit_basis`: regenerate it.
- A profit-hinge split, and a `max_request_pct_of_available` without a
  `required_remaining_buffer`, are accepted on every template, and both are
  priced against the earned profit. They were refused unless the template
  named `balance_above_initial`.

### 2026-10-30: a trader's Noctant sign-in lasts 30 days from its last renewal

- A trader's Noctant sign-in now lasts 30 days from its last renewal. Using
  Noctant renews it at most once a day. Before, it lasted one day, renewed
  hourly, so a trader who opened Noctant from your app every few days signed
  in again almost every time.
- Signing out or clearing the browser's stored data for Noctant still ends
  it, so do not promise "never sign in again".
- No request, response or webhook changes.

### 2026-10-30: a webhook's `data` is what Noctant recorded, on every delivery and replay

- Every delivery of an event sends the `data` Noctant recorded when the event
  happened: its first delivery, each retry and redelivery, and a replay.
  Before, `data` was rebuilt at each send from the event type's current
  definition. An event that definition could no longer read was not sent, and
  a replay page that reached one answered `500`.
- So an event recorded before its type gained a field does not carry that
  field, and one recorded before its type lost a field still carries it. From
  this change on, a field added to an event type is optional in the OpenAPI
  document: an event recorded before the field was added lacks it and is sent
  as recorded. Accept a field you do not know, and read a missing optional
  field as not stated.
- `backtest.trade.closed`'s `created` is when Noctant recorded the trade: its
  run's completion, the same instant as that run's
  `backtest.session.completed` `completed_at`. Before, it was the trade's
  market close, which stays in `data` as `closed_market_time`.
- To receive an event that was not sent before this change, replay its window
  (replays reach back 30 days).
- The envelope's fields, the headers and the signature scheme are unchanged.

### 2026-10-30: Noctant ticks the features your company may open work in; presets are named by what they hold

- Noctant ticks, per company, which features it may open new work in:
  `accounts`, `challenges`, `resets`, `payouts`, `kyc_reports`,
  `historical_replay`, `webhooks`, `simulation_credits`,
  `real_money_settlement`, `in_app_starts`, `own_practice_challenges`. Every
  company holds every feature today. `GET /v1/entitlements` and `GET /v1/me`
  list yours as `features`.
- An operation that opens work of a feature your company does not hold
  answers `403` `feature_not_enabled`, naming the feature, for keys and staff
  alike: provisioning a user, creating an account or a claimable launch
  (`accounts`, also checked when the launch is redeemed), templates, products
  and attachments (`challenges`), a reset, a payout request, historical
  launches and replay feed grants (`historical_replay`), webhook URL,
  subscription and replay (`webhooks`), simulation credit settings and
  grants. Work already open is always settled: reading, approving, rejecting,
  settling, correcting, closing and controlling are never refused for a
  feature, and neither is a KYC report, which payouts already open wait on.
- A company without `webhooks` is owed no new delivery; a delivery already
  owed is finished. A Default webhook subscription now delivers every event
  type, no longer your preset's set, and no feature narrows it: every event
  reports work already open. A receiver that held a narrower preset's
  Default keeps exactly that set, now as its fixed list.
- The Operator API presets are named by what they hold, catalog version 2:
  `full` (every partner scope), `trading_only`, `historical_replay`,
  `calendar_importer`, `read_only_import`. The old keys (`prop_firm`,
  `prop_simulator`, `journal_account_creator`, `education_partner`,
  `bare_trading_tenant`, `read_only_journal_import`,
  `historical_replay_backtest`, `economic_calendar_importer`) are refused. A
  preset is a starting scope set: nothing reads which one an operator was
  given, and a preset's view no longer lists webhook event types.
- The readiness report no longer carries `preset`, `scope_drift` or
  `webhook_event_type_drift`, and no longer blocks on a missing or unknown
  preset, on scopes beyond a preset, on payout scopes outside the prop-firm
  preset, or on a subscription beyond a preset's. It asks for replay and
  backtest feed grants when the operator holds `historical_sessions:read`,
  and warns `operator_api_scope_inert` for a scope whose every operation opens
  work of features your company does not hold.

### 2026-10-30: the guides say the TypeScript SDK is not published yet; corrections to three entries

- Correction to the
  [entry on the upgrade notes](#2026-10-30-the-upgrade-notes-state-feature-refusals-the-new-preset-keys-and-the-default-subscription):
  the TypeScript SDK, `@noctant/operator-api-typescript-sdk`, is not
  published yet, and neither are the examples inside it, so the names the
  guides give them are not public. The journal-app, partner and
  self-onboarding guides now say so: generate your client from the OpenAPI
  document, or call the API over HTTPS as it describes.
- The preset upgrade note says what applying a preset does: it sets the
  operator's scopes to the preset's and its webhook subscription to
  `default`, every event type, so an `explicit` list widens to every type.
- Correction to the
  [entry on the launch page](#2026-10-30-the-launch-page-shows-account-facts-and-what-connecting-shares):
  on a claimable launch, sign-in starts with the email you sent; on a launch
  made with `POST /v1/launch-sessions`, with the email Noctant holds for the
  person, the one they sign in with once they have signed in; either stays
  editable.
- Correction to the
  [2026-10-30 entry](#2026-10-30-noctant-ticks-the-features-your-company-may-open-work-in-presets-are-named-by-what-they-hold):
  not every company held every feature then, since Noctant's own company,
  which sells simulations, did not hold `real_money_settlement`.
- On the launch page, the button of the 'different account' refusal reads
  `Use a different account`, as on the consent step; it read
  `Use Different Account`.
- No request, response or webhook changes.

### 2026-10-30: the guides state retry, session and price details

- Correction to the 2026-10-01 entry: a call to a removed
  `/v1/accounts/by-external-id/...` path answers `400` `validation_failed`
  only when the key holds the scope of the operation it lands on; a key
  without that scope gets `403` `forbidden` first.
- The guides now state that `504` `command_receipt_timeout` and the retryable
  `409` codes are sent again with the same request and key, that `payout_held`
  also answers an approval, that an unwatched backtest session may park before
  15 minutes, that a trade's marker may sit off a bid bar by the spread or
  more, and that the Operator API serves no bars.
- No request, response or behaviour changes.

### 2026-10-30: the launch page shows account facts and what connecting shares

- The launch page no longer shows your `external_account_id`; it shows the account's name, starting balance and currency.
- The consent step lists what connecting shares with you.
- Sign-in starts with the email you sent, still editable.
- A 'different account' refusal names who is signed in.
- No request or response field changes.

### 2026-10-30: the operations that still answered a database outage as 500 or 409 answer 503

- `POST /v1/accounts`, `GET` and `PATCH /v1/accounts/{account_id}/risk-overrides`,
  `POST /v1/users/{global_user_id}/payout-requests`,
  `POST /v1/users/{global_user_id}/payout-requests/{payout_request_id}/approve`,
  `POST /v1/templates` and `PATCH /v1/templates/{template_id}` answer `503`
  `service_unavailable` when a database they read could not answer (the engine
  journal, the account's instrument routes, the instrument catalogue): send the
  same request again, with the same key where it has one. They answered `500`
  `internal_error`.
- `POST /v1/challenges/{instance_id}/unfail` answers `503`
  `service_unavailable` when the engine journal could not answer, and `500`
  `internal_error` when it answered something the service cannot read. It
  answered `409` `conflict` in both cases, with the database's own words as the
  detail.
- `POST /v1/challenges`, `POST /v1/challenges/{instance_id}/reset`,
  `POST /v1/challenges/{instance_id}/unfail`,
  `POST /v1/challenges/calendar-inputs` and
  `GET /v1/challenges/calendar-inputs/latest` answer `503`
  `service_unavailable` when the database could not answer the account's
  instrument routes or the instrument catalogue, and `500` `internal_error`
  when it failed the read. They answered `409` `conflict`, with the database's
  own words as the detail. A refusal (an instrument its feed routes to no
  calendar, a catalogue entry that cannot be read) keeps its `409`.
- `POST /v1/challenges`, `POST /v1/challenges/{instance_id}/reset`,
  `POST /v1/challenges/{instance_id}/reject`,
  `POST /v1/challenges/{instance_id}/manual-fail` and
  `POST /v1/challenges/{instance_id}/unfail` answer `503`
  `service_unavailable` when the database could not record a simulated fee,
  refund or renewal the operation books: send the same request again, with the
  same key where it has one. They answered `500` `internal_error`.
- `POST /v1/challenges`, `POST /v1/challenges/{instance_id}/reset` and
  `POST /v1/challenges/{instance_id}/unfail` answer `503`
  `service_unavailable` when the database could not answer whether the
  account's scale still awaits the engine, and `500` `internal_error` when it
  failed that read. They answered `409` `conflict`, as if the scale were
  pending. A scale that is pending keeps its `409`.
- `POST /v1/users/{global_user_id}/payout-requests/{payout_request_id}/approve`
  of a request whose account is paused answers `503` `service_unavailable`
  when the database could not answer whether the account's scale still awaits
  the engine, and `500` `internal_error` when it failed that read. It answered
  `409` `conflict`, with the database's own words as the detail. A scale that
  is pending keeps its `409`.
- `POST /v1/trader-keys/{key_id}/revoke` answers `503` `service_unavailable`
  when the database could not answer the revoke: send the same request again.
  It answered `500` `internal_error`.
- `POST /trader/v1/keys` and `POST /trader/v1/keys/{key_id}/revoke` answer
  `503` `service_unavailable`, with `Retry-After`, when the database could not
  answer: send the same request again. They answered `500` `internal`.

### 2026-10-30: the Rust receiver crates answer `2xx` to an event your build cannot read

- `operator-webhook-receiver` 0.4.0's `WebhookVerifier::verify` returns
  `Received::Handled(event)` for an event your build reads, and
  `Received::Unhandled { envelope, reason }` for a signed, in-org, well-formed
  event it cannot read: a `schema_version` other than 1
  (`unknown_schema_version`), a type it does not know (`unknown_type`), or
  `data` it cannot read (`unreadable_data`, with the path where reading
  stopped and why). Unreadable `data` is a value it does not know, a field
  it requires that is missing, a key a closed object does not take, or a rule
  the contract checks.
- Before, `verify` refused each of these, and a receiver that answered with a
  `4xx` lost the event: Noctant never redelivers after one.
- A refusal is now only `signature`, `scope` or `malformed`, judged on the
  envelope alone. The `unknown_type` refusal is gone.
- `operator-webhook-receiver-postgres` 0.3.0's `verify_and_process` returns
  `Unhandled` to you without running your processor or recording the event,
  and recommends `200`. A replay after you upgrade processes the event once.
- Removed: `verify_at`, `verify_envelope`, `verify_envelope_at`, `verify_as`,
  `verify_as_at`, `VerifiedWebhook`, `WebhookIdempotencyRecord::from_verified`,
  `verify_envelope_and_process`, and the error variants `ContractValidation`,
  `UnsupportedSchemaVersion`, `UnknownEventType` and `UnexpectedEventType`.
- `operator-api-client` 0.3.0 hands out the 0.4.0 verifier. The helper's
  table is unchanged.

### 2026-10-30: the Rust receiver crates take a repeated event as a duplicate, whatever its body

- The same event id can reach you more than once, and a repeat's `data` is
  not guaranteed to be byte-identical to the first delivery's. Deduplicate by
  the event id and type, and never compare bodies.
- `operator-webhook-receiver` 0.3.0 and
  `operator-webhook-receiver-postgres` 0.2.0 take a repeated `(id, type)` as a
  duplicate whatever its body. A processed event answers `200` again without
  processing again; a claim released after a failure is processed with the
  repeat's body. The receiver's record is the event id and type: the body
  hash and `Conflict` outcomes are gone. The breaking signatures are
  `WebhookIdempotencyRecord::from_parts(event_id, event_type)` and
  `classify_webhook_idempotency`, which now returns a `Result`.
- An id the store holds under another event type answers `503`, a fault of
  the receiver's own store that is retried, never a `4xx`.
- The helper's table drops `envelope_sha256` and `conflict_count`. Run once,
  before upgrading:
  `ALTER TABLE noctant_webhook_idempotency DROP COLUMN envelope_sha256, DROP COLUMN conflict_count;`
  `install_schema` refuses a table that still has them, naming both columns.

### 2026-10-30: the upgrade notes state feature refusals, the new preset keys and the Default subscription

- Two new upgrade notes state changes of the
  [2026-10-30 entry](#2026-10-30-noctant-ticks-the-features-your-company-may-open-work-in-presets-are-named-by-what-they-hold):
  an operation that opens new work of a feature your company does not hold
  answers `403` `feature_not_enabled`, and the Operator API presets have new
  keys.
- The `GET /v1/me` note and the partner guide say a Default subscription
  delivers every event type.
- Precisions: a `4xx` other than `429` to a repeated event is final; a
  repeated claimable launch must match every creation term the account was
  made with, `claim_allowed` and `marketing_allowed` included, and the same
  person; the risk-override operation is a staff session's; a held payout
  approval is sent again without a key; the journal-app prompt launches a
  person into an existing account with `POST /v1/launch-sessions`; `symbol`,
  `cancelled_by` and `time_in_force` are absent only on facts recorded before
  2026-10-08; the guides name the OpenAPI document and the SDK examples by
  their public names.
- No request, response or behaviour changes.

### 2026-10-30: this changelog is served, and opens with upgrade notes

- This file is served at
  `https://docs-staging.noctant.com/operator-api/changelog`. Its upgrade notes
  come first: each change an integration written against an older call must
  make, with a link to its entry.
- The partner guide is served too, at
  `https://docs-staging.noctant.com/operator-api/partner-guide`.
- The guides state three rules the API already applied:
  - answer `2xx` to a signed, in-org, well-formed event of a type you do not
    handle. They said to refuse an unknown type, and Noctant treats a refusal
    as final and never redelivers the event;
  - deduplicate by event id alone. They said to refuse a repeated id whose body
    differs, but a redelivered body is serialized again and can differ;
  - once the account exists, launch the person into it with
    `POST /v1/launch-sessions`. A claimable launch whose account terms differ
    from that account's is refused `409` `conflict` when the browser connects.
- The journal-app and self-onboarding guides list this file's URL and the
  partner guide's.

### 2026-10-30: your traders see your firm's name, never an operator's

- Your traders now see your firm's name, the name your firm has on Noctant,
  in the account picker and on the launch pages, including a dead launch
  link's "Back to …". Before, they saw the name of the operator that sponsored
  the account or minted the launch (for example `Production`), which a trader
  can read as real money.
- An operator's `name` (`POST /v1/operators`, `GET /v1/operators`,
  `GET /v1/operators/{operator_id}`) is now only your staff's label for that
  integration, such as `Live` or `Sandbox`. No trader sees it. Nothing you
  send or read changes.
- Next: a later change moves the logo to the firm and renames the launch
  preview's `operator` to the firm's (name and logo) and the gone body's
  `operator_name` to the firm's name, in one change.

### 2026-10-29: a trade's and an order's `program_account_attempt_id` is always present

- Every row of `GET /v1/accounts/{program_account_id}/trades` and
  `GET /v1/accounts/{program_account_id}/orders` carries
  `program_account_attempt_id`: the attempt whose window holds the row. The
  OpenAPI document now marks it required on `AccountTradeView` and
  `AccountOrderView`, never `null`, and the generated TypeScript SDK types it
  `string`.
- The JSON does not change: the field was already on every row.

### 2026-10-29: the Trader Stream's `margin_updated` states only the margin the engine booked

- `margin_updated` loses `free_margin` and `margin_level`. The engine never
  recorded either: every event stated `free_margin` `"0"` and no
  `margin_level`, so a reader that took them showed a free margin of zero.
  `margin_used` and `at` are unchanged. An account's free margin and margin
  level are read from `GET /trader/v1/accounts/{account_id}`.

### 2026-10-28: a session request the platform cannot authenticate yet answers `503`

- A Trader API request made with a session's token (not a trader API key)
  answers `503` `service_unavailable` with `Retry-After: 1` and the message
  "Authentication is temporarily unavailable" while the platform cannot read
  the sign-in service's current keys. Nothing was decided: send the same
  request again after that second. Before, it answered `500` `internal`. A
  token that is invalid or expired still answers `401` `unauthorized`, and a
  request made with a trader API key is unchanged.

### 2026-10-28: a session's Trader Stream ends with its login, and `authorize` keeps it open

- A Trader Stream socket (`/trader/v1/stream`) opened with a session's ticket
  now ends when that session's login expires: it receives `refused` with the
  `code` `unauthorized` ("the principal's authorization ended") and closes.
  To keep it open, mint a fresh ticket with the same person's current
  session (`POST /trader/v1/stream-tickets`) and send
  `{"op":"authorize","ticket":"…"}` on the open socket; it then lives until
  that login expires. A ticket that cannot renew it (another person's, a
  key's, used or expired) ends the socket with `unauthorized`. Before, a
  session's socket lived as long as the person stayed a member of your
  tenant, whatever happened to the login.
- A socket opened with a trader API key is unchanged: it lives on its key,
  and `authorize` on it is refused `invalid_frame`. The Trader API
  document's `ClientFrame` gains `authorize`.

### 2026-10-28: a simulated payout and a challenge's `adjudication` count only the profit the attempt earned

- A challenge's `adjudication` (`GET /v1/challenges/{instance_id}`) states
  its three figures from one reading of the attempt's facts.
  `excluded_profit` counts only the voids of the trades those facts hold: a
  void of a trade realized after them counts once they hold it.
  `eligible_objective_profit` is `raw_realized_profit` less
  `excluded_profit` and less the carried counted profit, the floating profit
  of positions carried from the previous step that its pass already counted.
  Before, `excluded_profit` summed every void recorded, and
  `eligible_objective_profit` was `raw_realized_profit` less
  `excluded_profit`. The fields are the same; their numbers change for an
  attempt that carried positions into its step, or that has a void its facts
  do not hold yet.
- A simulated payout (every payout inside Noctant is simulated) is decided
  at two profits, the earned one (less voids and the carried counted profit)
  and the raw realized one, and the worse decision holds: a gate that refuses
  at either refuses, the maximum is the lower of the two, the trader's amount
  is the lower of the two prices (each by its own tier and split), and the
  request keeps the larger `required_remaining_buffer`. The best-day cap
  measures the raw best day at both. So a voided or carried profit never
  pays, never counts as a payout day (winning days, qualifying days, the
  cycle) and never reaches a better tier, cap or split. Before, a carried
  profit the pass had already counted was counted again toward the payout's
  days and cumulative profit, and the earned profit capped a simulated
  payout only on an attempt whose rules could void profit.
- A simulated payout request recorded from this change on states its facts
  per profit in its statement's `policy_input`
  (`GET /v1/users/{global_user_id}/payout-statements`):
  `policy_resolution.earned` and `policy_resolution.raw` each hold
  `cumulative_profit`, `per_day_profit`, `completed_day_profit`,
  `cycle_profit`, `applied_tier`, `shadowed_keys`, `next_tier_key`, `split`,
  `retained_profit`, `max_request_pct_of_available`, `max_gross_amount`,
  `minimum_trading_days`, `required_remaining_buffer` and
  `max_admissible_payout`, beside `policy_resolution.raw_best_day_profit`;
  `priced_at` (`earned` or `raw`, `null` where the amount could not be
  priced) names the profit that priced it. Those keys left the top level of
  `policy_input` and of `policy_resolution`. In `policy_decision`, the
  `requested_amount_exceeds_available` refusal's `available_profit` is
  always a decimal; it was `null` where no finite ceiling was set. Requests
  recorded before keep their keys, so a reader of `policy_input` reads both
  shapes. The OpenAPI document declares neither object, and it is unchanged.

### 2026-10-28: a simulated payout request waits until the account's latest activity is applied

- `POST /v1/users/{global_user_id}/payout-requests` answers `409`
  `payout_held` while the platform has not yet applied every event the
  engine committed for the account (a trade, a balance move, a valuation):
  "This payout is on hold for a moment while the account's latest activity
  is processed. Nothing was recorded; send the same request again shortly."
  This hold clears within moments: send the same request, with the same
  `idempotency_key`, again shortly. `payout_held` also answers holds that
  wait on the trader, on you or on the post-trade review (an open position
  or working order, a review or rule window still open, another payout
  under review), so `FirmErrorCode::is_retryable()` stays false for it.
  Every payout inside Noctant is simulated. Before, the simulated payout was
  decided on the activity already applied, so a loss closed just before the
  request could be paid out as profit.
- A refusal on the attempt's own state (it is not active, or not funded)
  and every other hold are still answered first. The trader's KYC and the
  payout terms (the amount available, trading days, the caps) are judged
  only once this hold clears, so a request held now can still be refused
  when it is sent again.

### 2026-10-28: a Trader API account states its `slippage_pips`

- `Account` (`GET /trader/v1/accounts`, `GET /trader/v1/accounts/{account_id}`)
  gains a required field, `slippage_pips`: the deterministic slippage, in
  pips, that a market or stop fill pays on top of the markup (a buy fills
  that much higher, a sell that much lower, and an exit pays it as it pays
  the markup). A limit fills at its limit. It is zero on an instant profile.
  Fills are unchanged: the field states what the engine already charged,
  which the account did not state before.

### 2026-10-28: a trader API key's audit entries no longer carry its name

- In your audit trail (`GET /v1/audit-events`), the `trader_api.key.issued`
  and `trader_api.key.revoked` entries (`target_type` `trader_api_key`,
  `target_id` the key) state `detail` as `prefix`, `may_trade` and
  `account_ids`. Before, `detail` also carried the key's `name`, the free
  text its trader typed, which stayed in your trail after the trader deleted
  their Noctant account. Tell keys apart by `target_id` or `prefix`.

### 2026-10-28: `GET /v1/operators` lists every operator of your firm

- `GET /v1/operators` (staff session) answers every operator of your firm,
  newest first by `created_at`, then `operator_id`. Before this change it
  answered the newest 500 only, and nothing said that more existed. The
  answer is not paged and its shape is unchanged: read `operators` whole.

### 2026-10-28: the Trader Stream states an account's status as `account_status`

- The Trader Stream (`/trader/v1/stream`) carries a new event type,
  `account_status` (`status`: `active`, `suspended`, `breached` or `closed`;
  `at`: when it was recorded), each time the account's status changes: a
  suspension, a resume, a breach, a hold placed or released, a close, a
  retirement, and the account's creation. A released hold that leaves a
  rule's suspension in force states `suspended`. Before this change the stream
  carried none of them; a reader that refuses an unknown event type must
  accept it, and reads the account again once, since a cursor taken before
  this change skipped them.

### 2026-10-26: a challenge `idempotency_key` names one request across your accounts

- `POST /v1/challenges` scopes `idempotency_key` to your operator, as the
  document states. A key sent from 2026-10-26 on names one request: sending it
  again with another `user_id`, `program_account_id` or `template_id` answers
  `409` `conflict`, and no challenge starts. It answered `201` and started a
  second challenge when only the account differed. Re-sending the same request
  with its key still returns the original challenge.
- A key first sent before 2026-10-26 is not found under its new scope: sent
  again, it is a new request under the new scope, whatever it names. It never
  starts a second live challenge on that account: while the challenge it
  started stands, it is refused `409` `conflict`, as the account already holds
  a live challenge. Once that challenge has ended, it is a fresh attachment to
  the ended account, your own new sale. Keys already stored are not
  rewritten.

### 2026-10-26: a challenge's objectives, scale and counted days use only the profit the attempt earned

- Every profit objective (a target, a raised target, the consistency
  check's totals) and a scaling rung measure the attempt's earned profit:
  realized profit less what a judgment voided and less the profit the
  previous step's pass already counted of the positions carried into this
  step. A pass carries only the profit it counted: a carried position's
  counted loss stays charged when the position closes. Before, a counted
  loss was given back (it raised the earned profit), and a scaling rung and
  a raised target counted a carried profit again.
- A challenge's `profitable_days`, and its `trading_days` under a minimum
  daily profit, count each day's profit less its voids and less the carried
  profit taken off that day. Before, a day whose only profit the pass had
  already counted was a profitable day, and `trading_days` under a minimum
  read the day's raw profit.
- This moves a pass, a scale or a consistency verdict only for an attempt
  with voided profit or positions carried from a pass.

### 2026-10-26: a launch connects only the person's one Noctant login

- A person holds one Noctant login: the first one connected to them. When
  the trader's browser connects a launch you made for a person
  (`POST /v1/launch-sessions`, `POST /v1/historical-launch-sessions`) while
  signed in with any other login, the connect is refused `409`
  `browser_identity_conflict`; that answer goes to the trader's browser,
  never to a call you make. Before this change, a login no person held yet
  was linked to the person beside their first, and both signed in as them;
  only a login that was another person's was refused.
- The refused connect uses nothing. The link stays valid until it expires
  (`redeemed_at` stays `null`), and your audit trail (`GET /v1/audit-events`)
  records `operator_api.launch_session.fail` with the `reason`
  `browser_identity_conflict`. The trader's page asks them to sign out and
  use the correct Noctant account: they sign in with the login they connected
  first and open the link again.
- A connect that races another change to the same login answers 409
  `browser_identity_retry_required` and records nothing: the trader opens the
  link again. This connect never answered it before.

### 2026-10-26: a person's provisioning audit entry no longer carries their address or name

- In your audit trail (`GET /v1/audit-events`), the `operator_api.user.create`
  entry (`target_type` `user`, `target_id` the person's `global_user_id`),
  written when you provision a person (`POST /v1/users`) or a claimable launch
  connects one, states `detail` as `external_id`, `global_user_id`, `org_id`,
  `membership_role` and `created`, with `source` `claimable_launch_session` for
  a claim. Before, `detail` also carried the `email` and `display_name` you
  sent, which stayed in your trail after the person deleted their Noctant
  account. Tell people apart by `global_user_id` or `external_id`.

### 2026-10-26: a resent position close or modify answers its decision after the position closed

- `POST /trader/v1/positions/{position_id}/close` and
  `POST /trader/v1/positions/{position_id}/modify`, sent again with an
  `Idempotency-Key` that already has a decision, answer that decision
  (`202` with its `command_id`, or the refusal it was given), even when the
  position has closed since. Before, once the position had closed they
  answered `404` `not_found` and the decision was never read. A new key on
  a closed position still answers `404` `not_found`.

### 2026-10-26: a stop-out closes every position, after the price's due entries

- When a price makes an account due for a stop-out, the engine first fills
  the entry orders due at that price that open exposure (a whole reversal
  included), costliest for the trader first, and then closes every position
  on the account, each with the `close_reason` `stop_out`. A position that
  cannot close at that price (a closed market, no price) is owed and closes
  at its next price. Nothing more opens at that price: an entry the freed
  margin would fund stays working and is judged at the next price. Before,
  the stop-out came first and closed the worst-losing position, one at a
  time, only while the margin level stayed at or below the stop-out level,
  so other positions could stay open.
- On one price, every stop-loss fills before every take-profit across the
  account's positions. Among closes of one kind, the one worst for the
  trader goes first: a close whose result cannot be stated, then one that
  breaches or stops out at once, then the least room above an installed
  challenge floor, then the lower equity. Before, a price's closes went
  losses first, then the lower per-unit P&L.

### 2026-10-26: a Trader API command is refused `503` before it is sent while the engine does not decide

- While the matching engine does not decide commands (it is restarting or
  stopping), placing, cancelling or modifying an order and closing or
  modifying a position answer `503` `service_unavailable` with
  `Retry-After: 1` before anything is sent to it: nothing was decided, and
  the same request with the same `Idempotency-Key` may be sent again. The
  platform reads the engine's own statement every 250 ms, so the door
  reopens within a fraction of a second of the engine deciding again.
- Before, for about the first 10 seconds of a restart a command was sent to
  an engine that was not reading and answered `504`
  `command_receipt_timeout`, and the door reopened about 3 seconds after the
  engine was back.

### 2026-10-26: an amount beyond the ledger is refused as `AMOUNT_OUT_OF_RANGE`

- An order whose margin or notional is too large for the ledger to hold is
  rejected at placement, and a position close whose fee or amount is too
  large is cancelled, with the code `AMOUNT_OUT_OF_RANGE`: the order's
  `rejection_code` (`GET /trader/v1/orders/{order_id}`), the `reason_code` of
  the Trader Stream's `order_rejected` or `order_cancelled`, and the `code`
  of your `order.rejected` or `order.cancelled` webhook. Before, it was
  stated as a missing conversion, `CURRENCY_RATE_UNAVAILABLE`, which now
  means only that. Only an extreme per-lot fee or contract size reaches it.

### 2026-10-25: a first attachment starts on your organization's rules and goes live once the engine installed them

- `POST /v1/challenges` answers `reset_pending` for a sponsored challenge that
  starts now (it answered `active`). The challenge starts on your
  organization's risk rules alone: a set from
  `PATCH /v1/accounts/{account_id}/risk-overrides` does not carry into it,
  and `GET /v1/accounts/{account_id}/risk-overrides` shows its clear as
  `pending` `{}`, then `in_force` `{}`.
- Until the engine has installed those rules the account is held: its working
  orders are cancelled, its positions stay and can be closed, and no new
  order is admitted. `challenge.started` arrives in `reset_pending`, then
  `challenge.status_changed` says `active` with reason
  `pending_attachment_ready`. You receive no `account.trading_suspended` for
  that hold.
- A challenge that waited for a slot opens `reset_pending` at its turn (it
  opened `suspended`), then `active` the same way. Its account's hold at the
  attachment no longer sends `account.trading_suspended`.
- `POST /v1/challenges` decides a first attachment once the account's last
  platform command is applied, as it already did for a re-attachment: it
  answers `409` `account_command_applying` when that takes longer than the
  door's short wait, or when another command reaches the account meanwhile.
  Retry shortly; nothing was decided.
- A `reset_pending` challenge that never goes live (the engine refused its
  rules) is named in its readiness, and ends only by `manual-fail`, which
  gives back what its start took, as any `reset_pending` challenge.
- A challenge's `started_at` is when its clocks start: its time limit,
  inactivity and challenge days count from it. For a challenge that starts
  held (a first attachment, a reset, a re-attachment, waiting for a slot or
  not) that is its go-live, the `changed_at` of its
  `challenge.status_changed` to `active`, and until then the instant it was
  asked. Time a reset or a re-attachment waits for its account (an open
  position at a market close) no longer counts against its time limit.

### 2026-10-25: a passed challenge whose rules still judge its account keeps the account from risk overrides

- `PATCH /v1/accounts/{account_id}/risk-overrides` answers `409` while a
  passed challenge's rules still judge the account: a pass that keeps the
  account trading, while its activation offer stands or its next step waits
  to go live. It answered `202`, and the set reached the passed challenge's
  judgment and a next step on the same account. Once the next step is live on
  a new account, or the pass no longer holds its rules on the account, the
  same request is accepted. The refusal's `detail` reads `challenge
  {instance_id} grades this account; its limits are the challenge's` (for a
  challenge that has not ended it said `live challenge …`).

### 2026-10-25: a payout can pause its account until the firm decides

- A step's payout approval mode may be `pause_until_decided` (on the
  program's last step, as `manual_review`). A payout request on such a step is
  `pending_review` with `account_effect_status` `pausing`, then `paused` once
  the account is paused: no order that opens exposure is accepted until the
  request is decided. An account holds one such request at a time; another
  waits with the prerequisite `payout_under_review`.
- `POST /v1/users/{global_user_id}/payout-requests/{payout_request_id}/approve`
  on a `paused` request debits the payout (`approved` / `executing`) and the
  account trades again once the debit applies. On a request still `pausing`
  it answers `409`: retry. It now requires `payout_requests:approve` and
  `payout_settlements:write` in every mode.
- `POST …/reject` on a `pausing` or `paused` request resumes the account's
  trading, with no money moved. A `pausing` or `paused` request whose account
  closes is withdrawn (`policy_decision.withdrawal.cause` = `account_closed`).
- A payout request in any mode now waits for a flat account: no open
  position AND no working order. While the account has either, the request
  is held with the prerequisite `flat_account` and nothing is recorded; the
  trader closes the positions and cancels the orders, then sends it again.
- Removed: `POST …/approve-and-execute`, the mode `operator_approved_debit`
  (its steps and requests are `pause_until_decided`), and the
  `account_effect_status` value `reserved`.

### 2026-10-25: a product whose KYC gate covers the challenge start starts no challenge before you report the trader verified

- A product's `kyc_gate` of `challenge_start` or `both` is now honoured when a
  challenge starts. `POST /v1/challenges` (an attachment, and a re-attachment
  after an ended challenge) and `POST /v1/challenges/{instance_id}/reset`
  answer `409` `challenge_start_kyc_required` while the trader has no current
  verified KYC record with you: none, `pending`, `rejected`, expired at the
  start, or without its report. Nothing is started and nothing is taken.
  Before this change, both answered as if the product had no start gate.
  Report the trader verified with `POST /v1/users/{global_user_id}/kyc-status`,
  then send the same request, with the same key: it is decided afresh.
- `payout_only` and `none` products start challenges as before, and the
  payout gate is unchanged.

### 2026-10-25: attaching a challenge to an account with open positions closes them at the attach

- Attaching a challenge (`POST /v1/challenges`) to an account that has open
  positions closes them at the attach, and its working orders are cancelled:
  a challenge starts on a flat account. This replaces the 2026-10-25 entry's
  "its positions stay and can be closed".
  - A position whose market is closed closes at its next price, and the
    challenge goes live once the account is flat.
- A first attachment that ends before it goes live gives the account back.
  That covers `manual-fail` on its `reset_pending` challenge, or the platform
  stopping it.
  - The attach's hold is released, so the account is active again, unless it
    breached while the platform could not resume it: then it stays breached.
  - It owes no close under the product's failed-account term.
  - It takes a new challenge only while it holds the size the challenge is
    sold at. The attach's close moves it off that size when its positions made
    or lost money; a new attachment then answers `409` naming what the account
    holds and the size the challenge is sold at.
  - Until a challenge starts on it, it admits closes and no new order.

### 2026-10-24: a deployment window answers in the error body

- While a deployment window is closed, every request but health answers `503`
  with the error body: `{"error": "service_unavailable", "detail": "The
  platform is being updated. Send the request again in a minute."}`. Before,
  it answered `503` with `{"error": "Business ingress is closed for the
  deployment window", "code": 503}`, whose `error` is no code of the API's.
- That answer carries `Retry-After: 60`: send the request again after that
  many seconds. A window has no fixed length, so a request sent then may be
  answered the same way.
- The document declares an optional `Retry-After` header (whole seconds) on
  the `503` and `429` refusals. The rate limiter already sent it on `429`.

### 2026-10-24: a reset, an unfail or a re-attachment clears the account's risk overrides

- A challenge run that resumes grading on an account (a reset, an unfail, a
  terminal re-attachment) runs on your organization's risk rules alone. A set
  from `PATCH /v1/accounts/{account_id}/risk-overrides` made while no
  challenge graded the account no longer carries into the run.
  `GET /v1/accounts/{account_id}/risk-overrides` shows the run's clear as
  `pending` `{}` until the engine applies it, then `in_force` `{}`. If the
  engine refuses it, the set it kept stays `in_force`, and the refusal is
  `last_refusal`.
- `POST /v1/challenges/{instance_id}/resume` on an unfailed challenge answers
  `409` while the engine has not decided the unfail's config (retry shortly).
  If the engine refused that config, it answers `409` naming the engine's
  code, and the challenge stays `suspended`, yours to `manual-fail`. A
  challenge unfailed before this change has no recorded clear: its resume
  answers `409` too, and it stays `suspended`, yours to `manual-fail`.
- A reset or re-attachment run goes live only once the engine applied its
  clear. A run started before this change has no recorded clear: it never goes
  live, and
  `GET /v1/challenges/{instance_id}/pending-reattachment-recovery` names the
  new blocker `runtime_config_clear_unrecorded`. End it with
  `POST /v1/challenges/{instance_id}/manual-fail`; its start is given back.

### 2026-10-24: an account's risk overrides are in force once the engine applies them

- `PATCH /v1/accounts/{account_id}/risk-overrides` answers `202` instead of
  `200`: the change is a request. Its body is the account's override state
  below, with the change as `pending`. The request body is unchanged.
- `GET /v1/accounts/{account_id}/risk-overrides` answers the state:
  `in_force` (the set the engine applied last; `{}` when none is),
  `pending` (the set asked for that the engine has not decided yet, or
  `null`) and `last_refusal` (the newest set the engine refused after the one
  in force, with its `reason_code` and `reason`, or `null`). Before, it
  answered the last set sent, whether or not the engine had applied it.
- A refused set (for example `RUNTIME_CONFIG_PENDING_ORDERS`: the account
  holds a working order) is never in force and is not carried into the
  account's later configs.
- A value with more decimal places than the account keeps is refused with
  `400`, naming the field: `margin_requirement` keeps 4, `max_position_size`
  8, `margin_call_level` and `stop_out_level` 2 (and each stays below 10,
  1e10 and 1000). Before, the stored value was rounded while the engine
  received the value as sent.

### 2026-10-24: an approval is decided at its place in the account's order

- `POST /v1/challenges/{id}/approve` now answers `202` with the challenge as
  it stands: its `newest_request` is the approval, with `answer` `null`.
  Within moments the approval is decided at its place in the account's order,
  after every trade before it, so a breach before it wins: the challenge
  fails, and the approval is refused. Before, the approval was decided at the
  call and answered `200`.
- What the call can refuse at once is still `409` with the same codes, and
  records nothing.
- A challenge (`GET /v1/challenges/{id}`, and the approve answer) carries
  `newest_request`: its newest favourable request (`kind` `pass_request`,
  `approval` or `activation`), `requested_at`, and `answer` once decided:
  `granted`, `held`, `not_passed`, `unvalued`, or `approval_refused` /
  `activation_refused` with the `refusal`'s `reason`.
- A partner learns of a refused approval as before: the breach that refuses
  it fails the challenge, and the failure and status webhooks the end sends
  say so. A refusal for another reason (a flag or a hold that arose between
  the call and its place) leaves the challenge `pending_review` with no new
  webhook, and the `newest_request`'s answer gives the reason.

### 2026-10-21: a breach after a pass counts whatever the pass did to the account

- A passed step's rules breached while the passed challenge is still its
  program's current one on the account now count whatever the step's
  after-pass choice. Before, a pass that locked the account (a review the firm
  approved, an activation offer) ignored the breach. Now its next step ends
  before it went live, or its offer is forfeited, and the account closes by
  the template's `failed_account_end`, as for a pass that left the account
  trading.
- A completed challenge whose rules breach the account before they are
  cleared stays `completed`, and its account closes by the template's
  `failed_account_end`. Under `close_now` a re-attachment of that account
  answers `410` with code `account_closed`; under `keep_days` it is admitted
  inside the window. Before, the breach was ignored.
- A challenge awaiting review whose pass locked the account is judged until
  it is decided: a breach fails it, and a flag holds its approval. Before, a
  breach or flag after its pass was ignored.

### 2026-10-21: an account closes once its program fails on it, whatever came next

- A passed step's rules breached before its next step went live now close the
  account by the template's `failed_account_end` whatever came next: a next
  step waiting for its go-live, one waiting in line, or an activation offer
  not taken. Before, only the first did. `account.closed` says
  `program_ended` when the close comes due.
- A next step that ends before it went live and gives its activation back to
  the passed challenge, on the same account, no longer closes the account: the
  trader can activate again. A next step on a fresh account still closes its
  own account.

### 2026-10-20: a next step goes live only once the passed challenge is judged in full

When a step passes into a next step, on the same account or a fresh one:
- The next step stays `activation_pending`, and the program stays on the
  passed challenge, until every post-trade check on the passed challenge is
  decided: a flagged trade your firm reviews, a rule window still open, a
  consequence not applied yet. A breach those checks find in that time
  ends the program as the 2026-10-19 entry describes.
- `challenge.started` arrives when the next step goes live, with the status
  it starts in, for a fresh account's step too. Before, a fresh account's
  step sent it when the engine opened the account.
- A next step that ends before it goes live (its own breach or the passed
  challenge's while it waits, a staff end, a cancel while it waits in line
  for a slot) is heard started just before its end, in the same
  transaction: `challenge.started` in the status it waited in
  (`activation_pending`, or `awaiting_slot`), without `opening_balance` if
  the engine never opened it, then its end. You never receive an end for a
  challenge you were not told started.
- On the same account the engine opens the next step at its install, and
  from then the next step's own rules judge the account while it waits. A
  breach of them ends it `failed`: `challenge.failed` and
  `challenge.status_changed` say `own_breach`, and nothing is returned. The
  account is suspended.
- While it waits the next step is judged as it runs: its day closes are
  decided on their own day, and its time limit and inactivity clock run
  from its start. One of them running out in the wait ends it `failed`
  with `own_deadline`, and nothing is returned.
- A target the next step reaches while it waits passes it when it goes
  live, dated at the check that reached it, once everything of its wait is
  decided: each day close, and each check held for review in the wait.
- A next step waiting in line for a slot starts only once the passed
  challenge is judged in full, too. A purchase or an attachment at a full
  limit is answered after the challenges waiting in line have taken their
  turns, each by that decision.

### 2026-10-20: a next step goes live `suspended` until its account is released

- A fresh account's next step goes live `suspended`: `challenge.started`
  and `challenge.status_changed` (reason `fresh_account_activation`) say so.
  `challenge.status_changed` to `active` (reason `account_resumed`) follows
  once the engine releases the account. Before, it said `active` before the
  account was released.
- A next step on the same account whose pass locked the account, or that
  follows a step that ended before it went live, goes live the same way:
  `challenge.started` and `challenge.status_changed` (reason
  `same_account_activation`) say `suspended`, and `active` (reason
  `account_resumed`) follows the release. A next step whose pass left the
  account trading is unchanged: `active` at once.
- If the engine refuses the release, the step stays `suspended` until staff
  resume it.

### 2026-10-20: an order is admitted on what the account's feed offers, before its first tick

A new order on the Trader API (`POST /trader/v1/orders`) is admitted when
the account's feed offers the instrument to trade, whether or not the feed
has ticked it yet:
- `DATA_ONLY_SYMBOL` is no longer answered. A route the feed offers to trade
  always quotes.
- `PRICE_UNAVAILABLE` on an order is now always the engine's, stated by the
  order's rejection: a market order with no price yet is refused there, and
  a limit order is accepted and rests. The gateway no longer answers it
  before the order reaches the engine.
- `SYMBOL_NOT_ON_FEED` is unchanged.

### 2026-10-20: `challenge.started` says the status a challenge opens in

- `challenge.started` for a reset or re-attachment run carries the run's
  actual status: `reset_pending` until its account is reactivated, then
  `challenge.status_changed` to `active`. Before, it said `active` before the
  run was.
- A sponsored challenge that waited for a slot opens `suspended`: its account
  stays held until the engine releases it, then `challenge.status_changed`
  says `active`. Before, it said `active` before the account was released.

### 2026-10-19: a next step on the same account starts when its rules are in force

When a step passes into a next step on the same account:
- The next step is `activation_pending` until the engine has applied its
  rules on the account. Then `challenge.started` and
  `challenge.status_changed` say the status it starts in: `active`, or
  `suspended` if a hold on the passed challenge still stands, which the next
  step then carries. `challenge.status_changed` gives the reason
  `same_account_activation`. Before, the next step was `active` from the
  pass.
- Until then the passed step's rules that the engine enforces still judge
  the account. A breach of them ends the program, and the passed challenge
  stays `passed`:
  - a next step not yet live ends `failed`: `challenge.failed` and
    `challenge.status_changed` say `passed_step_breached`;
  - a next step waiting for a slot ends `cancelled`, with the reason
    `passed_step_breached`;
  - an activation not yet taken is forfeited: `challenge.status_changed` on
    the passed challenge says `passed` with the reason
    `activation_forfeited`, and its `activation_status` reads `forfeited`.
  The account is suspended in each case.
- A next step whose install the engine declines as stale ends `failed` with
  the reason `stopped_by_the_platform`: an activation it was made by returns
  to the passed challenge, still paid, and can be taken again.
- Once the next step is live, a resume of the passed challenge is refused
  (`409`): resume the next step.

### 2026-10-18: a platform preset reads as its published offer in `GET /v1/templates` and `GET /v1/templates/{template_id}`

A platform preset is a template your firm reads but did not author. Both
responses now describe it from its newest published public revision: the
revision a start pins, which `ruleset_revision_id` and `disclosure_hash`
already named.
- Its name, description, account size, price, profit split, fee refund,
  billing period, firm and family are that revision's. The detail's
  description and steps already were.
- `updated_at` is when that revision was published. `created_at` is still
  when the template was created.
- Before, a preset's list entry and its detail's header read the preset's
  current authoring row. A preset the platform had edited but not yet
  republished showed terms that no start would sell.
- Your own templates are unchanged: you still read them as you author them,
  unpublished edits included. No field is added or removed.

### 2026-10-18: an erased trader's challenge is `withdrawn`, and their account closes as `owner_erased`

These values are added now. The platform writes them from the release that
erases a trader's accounts: when a trader deletes their Noctant account, every
account of theirs at your firm is flattened and closed, and their challenges
end.
- A challenge status `withdrawn`: the platform ended the challenge because its
  trader's account was erased. It is not a failure: no failed-account close
  comes for it, and a start of your month it used stays used.
  `challenge.status_changed` says `withdrawn` with the reason `owner_erased`.
  `withdrawn` is final.
- The account is flattened and then closed. Its positions close with the
  `close_reason` `owner_erased` (the `position.closed` and trade webhooks, the
  account's trades, and the Trader API stream). Working orders the flatten
  cancels name `cancelled_by` `system`. `account.closed` says the `reason`
  `owner_erased`.
- The OpenAPI document's challenge status enum gains `withdrawn`, and so does
  the TypeScript SDK's `ChallengeInstanceStatus`; a strict client generated
  from the document needs it. The Trader API document's `close_reason` gains
  `owner_erased`. Treat an unknown status, `reason` or `close_reason` as one
  you do not handle.
- `operator-webhook-receiver` 0.2.3 reads them; 0.2.2 and earlier refuse an
  `account.closed` whose `reason` is `owner_erased` and a challenge event
  whose `status` is `withdrawn`.

### 2026-10-18: `GET /v1/templates` pages by keyset (breaking)

Changed in place, before the first external partner is live. Three breaks:
- `offset` is refused: `400` `validation_failed`, whose detail names it, as
  is any other parameter the list does not read.
- `total_count` is removed from the response.
- A `limit` above 500 is refused: `400` `validation_failed` (before, up to
  1,000 was served).

And the list now:
- pages like the other lists: `limit` (1 to 500, 100 when omitted),
  `before_created_at` and `before_template_id`, answered with `next_cursor`.
  Send a page's `next_cursor` fields as the next request's
  `before_created_at` and `before_template_id`; the last page has no
  `next_cursor`. One of the two without the other is `400`
  `validation_failed`.
- is newest first, by `created_at` and then `template_id`. Before, the
  firm's own templates came first.

The TypeScript SDK gains `TemplateListCursor`. In `operator-api-client`,
`list_templates` takes `operator_api_contracts::ListTemplatesQuery`, and
`query.next_page(next_cursor)` asks for the next page.

### 2026-10-18: no challenge starts for an erased trader

From the release that erases a trader's accounts:
- `POST /v1/challenges`, `POST /v1/challenges/{instance_id}/reset` and
  `POST /v1/challenges/{instance_id}/unfail` answer `410` `person_erased`
  where they would otherwise start a challenge, once the trader deleted
  their Noctant account. Nothing is written, and no
  start of your month is used. Provision the person anew under a new
  external id if they come back.
- A challenge of theirs that waits for a slot ends `cancelled`, an un-fail's
  too. `challenge.status_changed` says `cancelled` with the reason
  `owner_erased`, where a cancel by you says `waiting_cancelled`.
- If the close for a failed challenge was already on its way to the engine
  when the trader was erased, the account closes by that close, and
  `account.closed` says `program_ended`. Every other account of theirs closes
  as `owner_erased`, once.

### 2026-10-17: an approval whose post-trade checks are behind is retryable

- `POST /v1/challenges/{instance_id}/approve` answers `409`
  `judgment_catching_up` when the attempt's post-trade rule checks have not
  finished with its latest trades yet. Nothing was decided: send the same
  request again after a short backoff (seconds).
  `FirmErrorCode::is_retryable()` and the Rust client's `retryable()` include
  it.
- Before, the same moment answered the final `409` `conflict` with a
  "not judged through this decision yet" sentence. Anything the checks are
  owed is still that final `conflict`: a rule window that has not closed, a
  consequence on the attempt that has not been applied yet, or a pinned
  calendar the checks cannot read. An approval with an unresolved rule flag is
  refused before this, unchanged ("has unresolved rule flags").
- The payout routes keep `409` `payout_held`; its `detail` now tells the two
  apart. While the checks catch up it says to send the same request again
  shortly; while something is owed it says to send it again later, naming
  what it waits for.
- The OpenAPI document's `FirmErrorCode` enum and its `409` code list grow by
  `judgment_catching_up`; a strict client generated from it needs the new
  value. The TypeScript SDK's `FirmErrorCode` union gains it.

### 2026-10-17: only an operation that reads a body lists 413

- `413` `payload_too_large` is listed only on the operations that take a
  request body. An operation without one never reads a body, so a body sent
  to it is not read and cannot be too long. The document's `info.description`
  says so beside `413`.
- What the API answers does not change. A client generated from the document
  loses `413` from the error statuses of the 56 operations without a request
  body; the TypeScript SDK's `413: FirmError` entries go from those
  operations' error types.

### 2026-10-15: a challenge door waiting on the account's last command says `account_command_applying`

- `POST /v1/challenges`, `POST /v1/challenges/{instance_id}/reset` and
  `POST /v1/challenges/{instance_id}/unfail` decide only once the account's
  last platform command is applied. When it is not applied within the door's
  short wait, or another command reaches the account meanwhile, they answer
  409 with `error` `account_command_applying`. Before this change they
  answered 409 `conflict`, and only the sentence said to retry. Nothing was
  decided: the same request succeeds once the command is applied. The
  document already listed `account_command_applying` under 409 on every
  operation, and it is unchanged.
- `POST /v1/challenges` refused because a command on the account is not
  confirmed by the engine now answers `account_command_applying`. It answered
  `account_birth_applying`, which now means only that the account's creation
  is not confirmed yet.

### 2026-10-15: a person who deleted their account is refused on every door that names them

- `POST /v1/users/{global_user_id}/kyc-status` for a person who deleted their
  account answers 410 `person_erased`. Before, the report was admitted.
- `POST /v1/accounts` repeated for such a person, whose account already
  exists, answers 410 `person_erased`. Before, it answered 200 with the
  account.
- Connecting a claimable launch whose external user id belongs to such a
  person answers 410 `person_erased`, whoever's browser connects. Before, a
  different browser user was answered 409
  `operator_identity_claimed_elsewhere`.
- A request that names a person this firm never provisioned still answers 404.

### 2026-10-15: each refusal status is described once

- The OpenAPI document describes each error status once, under
  `components.responses`, and every operation references it. The codes each
  status is answered with are listed once, in the document's
  `info.description`, instead of on every operation.
- `405` `method_not_allowed` is listed on no operation: a declared
  operation's own method never answers it, only a method its path does not
  take. The API still answers it so, as the entry below says.
- A client generated from the document keeps every operation's statuses and
  error body, without `405`; its per-status comments no longer repeat the
  codes. The TypeScript SDK's types are a third smaller.

### 2026-10-15: every refusal names its code

- A request body longer than the operation reads answers `413`
  `payload_too_large` in the error body. Before, it answered `413` with a
  plain-text sentence.
- A path the API does not have answers `404` `not_found`, and a path asked
  with a method it does not take answers `405` `method_not_allowed` (its
  `Allow` header names the methods it takes). Before, both answered with an
  empty body.
- The error vocabulary gains `token_expired` and `already_redeemed` (`410`):
  the codes a launch link that expired, or was already used, is refused
  with. The hosted launch page already showed them; they are now codes like
  every other refusal's.
- The document lists `405` and `413` on every operation, each with its code,
  and the two launch link codes under `410`.

### 2026-10-14: `manual-fail` ends a challenge that has not started, and says what it gives back

- `POST /v1/challenges/{instance_id}/manual-fail` now ends a challenge in
  `activation_pending`: a fresh account's next step that waits for the engine
  to apply its rules. Before this change it answered `409`, and a step whose
  rule set the engine refused could not be ended.
- Such a step becomes `failed` before it started, and its account is
  suspended. Its activation goes back to the passed challenge, whose
  `activation_status` is `pending` again. Activating it again starts a new
  step and records no second activation fee. `challenge.failed` and
  `challenge.status_changed` carry the reason `pending_reattachment_cancelled`,
  the same reason as an ended `reset_pending` challenge.
- An ended `reset_pending` challenge gives back what its start took: the start
  of your month it retained is released, and any simulated fee it recorded is
  returned.
- A release of an account whose challenge is `reset_pending` or
  `activation_pending` still answers `409` `suspended_by_its_challenge`. Its
  `detail` now names `manual-fail`. Before, it named
  `POST /v1/challenges/{instance_id}/resume`, which does not lift such a
  challenge.
- `manual-fail` on a challenge that has ended still answers `409`. Its message
  now says that the challenge has ended.

### 2026-10-14: `payout_kyc_required` answers 409, and the document lists every status

- A payout refused because the trader's KYC does not admit it now answers
  409, not 403. The `error` is `payout_kyc_required`, as before. A client that
  switches on `error` sees no change; one that branched on 403 for this
  refusal now finds it beside `payout_held`, the other payout refusal of the
  payout's own state.
- The OpenAPI document lists, on every operation, every status the API answers
  with, and names the `error` codes each status carries. It newly lists 410
  (`gone`, `webhook_replay_history_unavailable`, `launch_unavailable`,
  `account_closed`, `person_erased`), 500 (`internal_error`) and 504
  (`command_receipt_timeout`), which the API already answered. A client
  generated from the document gains those response types. No other answer
  changed.

### 2026-10-13: an order carries no free text; `comment` and `client_order_id` leave the Trader API

- `POST /trader/v1/orders` no longer reads `comment` or `client_order_id`.
  A body that still carries either is refused with `400` `invalid_request`,
  and the message names the field ("comment is not a field of this
  request."). Before this change both were accepted and neither was ever
  returned.
- Your reference for an order is its `Idempotency-Key`: the `202` returns it
  as `command_id` beside the `order_id`.
- The Trader Stream's `order_pending` loses `client_order_id`, which it
  never set.

### 2026-10-12: `account.closed` can name the reason `platform`

- `account.closed` gains the `reason` `platform`: the platform retired an account its current cell never carried; no firm or rule closed it. Its `final_balance` is the last balance the platform stated for the account, exact; an engine close still states the engine's balance at the close. No money moves at either. Treat an unknown `reason` as a close you do not need to tell apart.
- `operator-webhook-receiver` 0.2.2 reads it; 0.2.1 and earlier refuse an `account.closed` whose `reason` is `platform`.

### 2026-10-12: `challenge.start_held`, a start held by a refused rule set

- A new webhook event type, `challenge.start_held`. A firm on its preset's
  Default subscription receives it from this build wherever the preset
  receives challenge events; an Explicit list receives it once the firm adds
  it. A challenge goes live when the engine applies its rule set. When the
  engine refuses the set (a calendar it cannot bind, a rule it cannot
  enforce), it holds the account and the challenge waits. Before this change
  you heard nothing until a set applied (`challenge.started`).
- It is sent once per challenge, when the start has waited 15 minutes since the
  refusal was recorded. It names the refusal: `reason_code` (the engine's
  stable code) and `reason` (its words). It also carries `held_since` (when
  the refusal was recorded), `reported_at` (when the platform found the start
  still held, this event's instant) and the challenge's `status`.
- The start waits; it has not failed. A `challenge.started` still arrives
  if the engine later applies a set. A later refusal of the same challenge
  sends no second `challenge.start_held`.

### 2026-10-12: webhook event types and challenge statuses are closed sets

- Every webhook `event_type` the API reads or states is the closed set
  `WebhookEventType` in the OpenAPI document. This covers a delivery's
  `event_type`, the delivery list's `event_type` filter, a replay's
  `event_types` and a subscription's `event_types`. A filter or a list naming
  a type outside the set is refused with `400` when it is read. Before this
  change a well-formed filter of an unknown type returned an empty page.
- A signed envelope's `type` stays open text: verify an envelope of a type
  your build does not know yet, and ignore it.
- A challenge's `status` (the challenge view, `challenge.started`,
  `challenge.failed`, `challenge.status_changed` and its `previous_status`,
  `challenge.settled`) is the closed set `ChallengeInstanceStatus`. The words
  are the ones you already receive.

### 2026-10-11: `alert_offer`, the source of an order a fired alert offered

- The Trader API's order `source` gains a sixth value, `alert_offer`: the
  trader created the order by confirming the order a fired alert offered.
  It appears on `GET /trader/v1/accounts/{account_id}/orders`, on
  `GET /trader/v1/orders/{order_id}` and on the Trader Stream's
  `order_pending`. Readers that enumerate `source` should accept it.
- Before this change, such an order made every order read of its account
  fail, and the stream could not carry the order's `order_pending`.

### 2026-10-11: every `side` is a closed set; a backtest trade reads `long` or `short`

- Every `side` in the API and its webhooks is now declared as a closed set
  in the OpenAPI document: an order's `side` (`AccountOrderView`,
  `order.filled`) is `Side`, `buy` or `sell`; a position's or a trade's
  (`AccountPositionView`, `AccountTradeView`, `HistoricalSessionTradeView`,
  `position.*`, `trade.closed`, `backtest.trade.closed`) is `PositionSide`,
  `long` or `short`. The generated TypeScript SDK declares both types.
- A backtest trade's `side` reads `long` or `short`, as a live trade's
  always has. Before this change a backtest trade would have read `Long` or
  `Short`; no partner received one (no backtest trade had been delivered).
- A live order, position or trade reads exactly as before.

### 2026-10-11: `FEED_MISCONFIGURED`, a feed whose promise exceeds its tolerance

- A new rejection code, `FEED_MISCONFIGURED`. A price feed states how late
  what it delivers may trail its source (its promise), and its registry how
  late the platform tolerates (5 s for the platform's feed). When the
  promise is the longer, a healthy feed would read behind, so the
  configuration is at fault: an order or a close on that feed is refused
  `FEED_MISCONFIGURED`, never `FEED_BEHIND`, and the reason names both
  numbers. It lasts until one of the two changes.
- A forced close refused this way waits, as one refused `FEED_BEHIND` does,
  and completes when the feed's statement fits its tolerance again.
- The platform's feed states a promise of 5 s against its 5 s tolerance, so
  it is judged and accepted. A feed that states no promise is judged as
  before.

### 2026-10-10: a challenge cancelled while it waits is `cancelled`, not `failed`

- Ending a challenge that waits for a slot
  (`POST /v1/challenges/{instance_id}/manual-fail` while its status is
  `awaiting_slot`) leaves it `cancelled`: the new last status of a challenge
  that never started. `challenge.status_changed` says `cancelled` (reason
  `waiting_cancelled`, as before); it said `failed`.
- The account is as it was before the wait: a challenge you attached and then
  cancelled releases the hold it put on the account (the account trades
  again), and a reset you cancelled gives the trader's
  failed challenge back as it failed, so it can be reset again. Nothing is
  owed: no failed-account close for the cancelled challenge, and the start of
  your month a reset retained is released.
- The account's read (`GET /v1/accounts/{program_account_id}`) keeps
  `program_type` `challenge`, its `program_origin`, and a
  `challenge_instance_id` naming the cancelled challenge: they keep stating
  the challenge chain that exists and drive no rule. Your next attachment to
  that account is taken as a first attachment.
- A restore (un-fail) that waits and is ended is withdrawn: the challenge is
  `failed` again from its original failure, and its failed-account end keeps
  its original day.
- Treat an unknown challenge status as one you do not handle; `cancelled` is
  final.

### 2026-10-10: a feed is behind when its own statement of delivery is late

- `FEED_BEHIND` is decided by what the instrument's price feed states about
  its delivery, not by when it last ticked. The feed's adapter states every
  second the instant through which it has delivered everything, and a fill is
  refused `FEED_BEHIND` when that instant is older than the feed's bound: 5 s
  for the platform forex feed (it was 60 s after the feed's last tick). A tick
  that arrives while the feed's delivery is unproven proves nothing.
- A forced close (a breach, a stop-out, a close-all) deferred while the feed
  was behind completes as soon as the feed states its delivery again, at the
  price it then holds. It no longer waits for the instrument's own next price.
- `NO_QUOTE_SINCE_FEED_RESUMED` follows the interval the feed states it could
  not deliver: a price from before that interval's end is refused until the
  instrument quotes again.
- A replay session or a backtest is never refused `FEED_BEHIND`: its tape is
  everything its feed delivered. Over a stored interruption a replay fills at
  the last price before it; it was refused after 60 s without a price.

### 2026-10-10: a replay refuses fills inside a stored price-feed interruption

- A replay session states its instrument's price feed as the platform stated
  it at that time. While the replay stands inside a stored interruption of
  the feed, an order or a close is refused `FEED_BEHIND`; once the replay has
  passed it, a price from before it is refused `NO_QUOTE_SINCE_FEED_RESUMED`
  until the instrument's next price. Before this change a replay filled
  across an interruption at the last price before it.
- The replay chart marks the interruption, as the live chart does.
- A backtest is unchanged: it fills at every step of its tape.

### 2026-10-10: an account whose challenge waits for a slot is yours to reach

- While a challenge you attached, reset or restored waits for a slot on one of
  your accounts (your template's `live_limit` with `when_reached:
  allow_waiting`), the account answers as any of your accounts:
  `GET /v1/accounts/{program_account_id}` and its orders, trades and positions
  answer `200`, and `PATCH /v1/accounts/{program_account_id}/status` places
  and releases your hold. Until this change they answered `404`.
- A release of such an account when nothing holds it answers `409` with the
  new code `account_awaits_slot`: the challenge has not started, so no resume
  lifts it. It starts by itself when a slot frees;
  `POST /v1/challenges/{instance_id}/manual-fail` ends the wait.
- A close is refused (`409`, `conflict`) while a challenge waits on the
  account, naming that challenge: end the wait first, then close.
- Once the wait is ended the account stays reachable: it can be closed, and
  you hear `account.closed` as for any close.
- An order placed while a reset waits is listed under the attempt that was
  current when it was placed.
- `no_predecessor_on_program_account` is no longer a value of
  `PendingReadinessBlocker`: no pending re-attachment can be in that state.

### 2026-10-10: readiness routes are not answered from outside

- A request whose path ends in `/health/ready` answers `404` on every Noctant
  host. Until this change `GET /health/ready` on the API host answered `200`
  with the states of the API's dependencies. Readiness is the platform's own
  internal check and was never part of the Operator API contract.
- The public check that the API host is up is its liveness route:
  `GET /health/live` answers `200` with an empty body.
- A monitor that starts getting 404 on a readiness URL should be reading
  `/health/live`.

### 2026-10-10: the launch link carries its token after `#`

- `launch_url` is now `<page>#nlt_<64 hex>`: the one-time token is the URL
  fragment, the one part of a URL a browser sends to no server. It was the last
  path segment (`<page>/nlt_...`). Code that reads the token out of the path
  stops working. Nothing of yours needs the token: redirect the browser to
  `launch_url` as it is.
- Redirect to it whole. A redirect that drops the fragment or encodes the `#`
  (`%23`) produces a link the terminal refuses with "This launch link is
  invalid or expired."
- A link minted before this change is refused after it. Links live 15 minutes,
  so this reaches only a launch in flight during the deploy; a link minted
  between the Operator API's deploy and the web terminal's fails the same way.
  Mint a new link.
- In proof reports the redacted form of a launch URL is
  `.../launch#<redacted>` (it was `.../launch/<redacted>`). The SDK example's
  `redactLaunchUrl` keeps only the link's origin.

### 2026-10-09: a closed account's events stay replayable

- `POST /v1/webhooks/replays` covers a closed account as it covers an open one:
  every event created while your sponsorship of the account was in force, up
  to and including the `account.closed` that ended it. Before this change a
  replay asked after the close enqueued nothing for that account
  (`enqueued_count` 0).
- An event the platform stated before the close is delivered even when it is
  processed after the close. Its `created` is the event's own instant, as
  always, so it sorts before the `account.closed`.
- Nothing created after the close is delivered or replayed, and an account you
  do not sponsor still answers `404`.

### 2026-10-09: a held price is refused by its cause

- An engine refusal of an order or a close for want of a usable price names its
  cause in `reason_code` (a command outcome's `engine_command_outcome_reason_code`,
  and an order's cancellation): `MARKET_CLOSED` (the instrument's market is
  closed), `FEED_BEHIND` (new: the price feed has delivered nothing for a while,
  so no held price is known to be current), `PRICE_UNAVAILABLE` (no price is
  held at that instant) or `NO_QUOTE_SINCE_OPEN` (new: the market reopened after
  a closure and the instrument has not quoted since).
- A quiet instrument on a live feed is no longer refused: it fills at its last
  price, as long as that price is from the market's current session.
- `PRICE_STALE` stays in the history of refusals made before this change and is
  no longer emitted. Treat an unknown `reason_code` as a refusal you do not
  need to tell apart.

### 2026-10-09: a price from before a feed interruption is not filled

- A new `reason_code`, `NO_QUOTE_SINCE_FEED_RESUMED`: the instrument's price
  feed stopped delivering for a while and is delivering again, and the
  instrument has not quoted since. Its last price is from before the
  interruption, so an order or a close on it is refused until its next price.
  A forced close (a breach, a stop-out, a close-all) owed on it completes at
  that next price.
- `FEED_BEHIND` keeps its meaning: the feed is not delivering now.
- A quiet instrument on a feed that kept delivering still fills at its last
  price.
- An order or a close is decided against the price the engine holds when it
  decides. One submitted an instant before a newer price arrived was refused
  `PRICE_UNAVAILABLE` (a defect since the change below, a few percent of market
  orders on a fast instrument); it fills at that price now. `PRICE_UNAVAILABLE`
  means only that no price is held for the instrument.

### 2026-10-08: four new `close_reason` values

- A position's `close_reason` (the `position.closed` and trade webhooks, and
  the account's trades) names an administrative close by its cause:
  `operator_requested` (your key's or staff's close-all), `step_passed_lock`
  (a passed step closed the account's positions before the next step),
  `account_held` (a hold placed on the account flattened it) and
  `account_suspended` (the attempt's verdict suspended the account and
  flattened it).
- `administrative` stays in the history of closes made before this change and
  is no longer emitted. Treat an unknown `close_reason` as a close you do not
  need to tell apart.

### 2026-10-08: `operator-webhook-receiver` 0.2.0, `operator-api-client` 0.2.0 (breaking)

- `WebhookVerifier::new`, `new_with_tolerance` and `from_signing_secret` take
  the receiver's `ReceiverScope`, and so do
  `OperatorAppCredentials::webhook_verifier` and
  `webhook_verifier_with_tolerance`. The receiver runtimes built through
  `from_verified_credentials` take it from `GET /v1/me`; the
  `from_credentials` ones take it as an argument.
- An event of another org is refused before the event id is recorded
  (`WebhookReceiverError::OutOfScope`, category `scope`) and answered `4xx`.
  Before, it failed in processing and was answered `5xx`, so it was redelivered.
- The order is signature, scope, type, then headers and payload.
  `WebhookHeaders::from_optional` no longer fails: a missing header is judged
  by the verifier, after the signature.
- `WebhookReceiverError::refusal()` returns a `WebhookRefusal` (`signature`,
  `scope`, `unknown_type`, `malformed`). Noctant's own receivers answer a
  rejected delivery with it as `refusal` in their JSON body.
- `ReceivedWebhookEvent::org_id()` gives every event's org.

### 2026-10-08: `order.cancelled` and `order.expired` state the order's facts

- `order.cancelled` states `symbol` and `cancelled_by`: who cancelled the
  order, `user` (the trader: their cancel or their close-all), `system` (the
  platform: a breach, a fill it refused, a sibling or parent order that went,
  an attempt's suspension) or `operator` (your firm or its staff: a close-all,
  or a hold placed on the account).
- `order.expired` states `symbol` and `time_in_force`, the one that expired the
  order (`gtd` or `day`), as the orders read names it.
- All three are absent only on facts delivered before this change: a replay
  re-sends a fact exactly as it was delivered. `operator-webhook-receiver`
  0.2.1 reads them (additive; the new enum is `operator-api-contracts`'
  `OrderCanceller`).

### 2026-10-06: `GET /v1/me` states the webhook subscription as its mode and its event types

- Recorded late, on 2026-10-30.
- `operator.webhook.event_types` (a list, or `null` for every event type) is
  replaced by `operator.webhook.subscription`, always present: `mode` is
  `default` (the event types of your operator's preset, so a type added to the
  preset reaches you) or `explicit` (a fixed list), and `event_types` are the
  types delivered today.
- `null` no longer means every type: a `default` subscription delivers its
  preset's types.
- Staff set it with `PATCH /v1/operators/{operator_id}/webhook-event-types`,
  whose body is `{"mode": "default"}` or
  `{"mode": "explicit", "event_types": [...]}`; another field is refused.

### 2026-10-05: every error answers one body, and a refusal that decided nothing says so by its code

- Recorded late, on 2026-10-30.
- Every error answer is the one `FirmError` body of the OpenAPI document:
  `error`, a code of a closed vocabulary, and `detail`, a sentence for a human.
  The codes' words did not change. A `401` answers `unauthorized` and a `503`
  `service_unavailable`.
- A refusal that decided nothing answered the bare `409` `conflict`; it answers
  its own code: `journal_catching_up`, `account_command_applying`,
  `guards_not_acknowledged` or `account_birth_applying`. Send the same request,
  with the same key, again after a short backoff. A `409` `conflict` is still
  final.

### 2026-10-01: account operations use `program_account_id` as their account handle

- Recorded late, on 2026-10-30.
- The seven `/v1/accounts/by-external-id/{status,close-all,commands/{command_id},summary,positions,orders,trades}`
  operations were removed. Look the account up once with
  `GET /v1/accounts/by-external-id?external_account_id=...`, then call
  `/v1/accounts/{program_account_id}/...`. A call to a removed path now answers
  `400` `validation_failed`: `by-external-id` is read as a `program_account_id`
  and is not a UUID.
- The bodies of challenges, simulated payout requests, account corrections,
  launch sessions and webhook replays name the account by `program_account_id`;
  it was `external_account_id`. Every account-scoped webhook carries
  `program_account_id`.
- `GET /v1/webhooks/deliveries` takes no account filter; it never had one.
- Undeclared fields are ignored when the operation's body or query does not
  refuse unknown fields. `CreateWebhookReplayRequest` does not refuse them:
  a replay still sending `external_account_id` has no account filter and covers
  every account of the operator within the requested event types and time window.
