This guide is for market makers and trading bots that use REST or the trading WebSocket, and for the funding and account tools around them. It covers the one-time move of Testnet from release 0.92.5 to 0.92.9.
It lists only the changes that need action from you. Every other change is in the changelog.
Who needs to act
- Everyone signing requests: a 5-second freshness window, an optional receive-window header, no nonce on HTTP, and a fresh signed nonce on every WebSocket login. Typed REST errors and structured rate-limit refusals also change. See Authentication and request freshness, REST validation and error handling, and Rate-limit responses.
- WebSocket traders: optional per-command receive windows and the dead-man switch on the trading socket. See Trading WebSocket additions.
- SBE codec users: semantic version 1.2.0 of schema 7, with timed command templates and the
CancelAllAftertemplates. See SBE timed command templates and Dead-man switch on the trading socket. - Batch-replace users: admission is answered before completion, pending items are retried in a fresh batch, and batch status is kept for 60 seconds. See Batch replacements and order reconciliation.
- Trigger users: pausing or canceling a trigger needs
TRADE_SPOT, path and body IDs must agree, and cancel-all does not stop triggers. See Trigger controls and shutdown safety. - Funding and account tools: explicit API-key permissions, restarted lifecycle pagination, a renamed transfer label, and auto-withdraw pause instead of disable. See Account permissions and policy tools and Funding, transfers and withdrawals.
- QuickSwap brokers: specific outcomes for small amounts and returns, referral codes, and broker vault balances. See QuickSwap integrations.
Migration checklist
- Update request freshness checks and separate HTTP signing from WebSocket login signing.
- Generate a fresh signed nonce for every WebSocket connection attempt.
- Handle receive-window expiry, typed validation errors and rate-limit refusals.
- Reconcile batch replacements within their 60-second retention window.
- Check the API-key permissions used by funding and account tools.
- Restart lifecycle pagination without saved pre-upgrade tokens.
- Refresh public contract bindings, including optional SBE additions if used.
Authentication and request freshness
These changes apply to every API-key signed request. See Ed25519 API keys and the API key replay policy.
HTTP freshness window is 5 seconds
Changed: the default acceptance window for a signed HTTP request changes from 10 seconds to 5 seconds. A timestamp may be at most 1 second ahead of server time, previously 10 seconds.
Send X-API-KEY-ID, X-API-TIMESTAMP (Unix milliseconds) and X-API-SIGNATURE.
What to do: synchronize clocks and sign immediately before sending. Sign the body bytes actually sent, including any decimal padding.
HTTP requests drop the nonce header
Removed: the X-API-NONCE header on HTTP requests. The nonce header now belongs only to signed
WebSocket logins; see Fresh signed nonce for WebSocket logins.
What to do: remove X-API-NONCE from HTTP requests.
Optional receive-window header
Added: the optional X-API-RECV-WINDOW header accepts one whole number of milliseconds from 1 to 60000. When you send it, sign its exact header value as an additional line immediately after
the timestamp. When you omit it, keep the existing canonical payload.
Join lines with LF, without a trailing LF. Keep the existing query canonicalization and Ed25519 signature encoding. HTTP signing keeps the percent-decoded public path; WebSocket signing uses the escaped upgrade path.
TIMESTAMP_MS
METHOD
PATH
CANONICAL_QUERY
SHA256_BODY_HEXTIMESTAMP_MS
RECV_WINDOW_MS
METHOD
PATH
CANONICAL_QUERY
SHA256_BODY_HEXThe RECV_WINDOW_MS line appears only when you send X-API-RECV-WINDOW. Without the header, the
0.92.9 payload is the same as in 0.92.5.
What to do: choose a receive window appropriate for the request. A longer window does not
increase the 1-second future-clock allowance. Session-token requests must not send X-API-RECV-WINDOW; that header belongs to API-key signing.
Requests expire before submission
Changed: the default window now also bounds waiting before submission. Previously a request accepted on arrival could be submitted after it became stale. Expiry does not cancel an accepted order and does not replace GTD expiry.
What to do: if a request may already have been submitted, keep your existing outcome-unknown reconciliation rules. Handle these refusals:
TIMESTAMP_SKEW· HTTP 401
The timestamp is too old for the window or more than 1 second ahead; nothing was processed. Correct the clock if needed and re-sign with a fresh timestamp.RECV_WINDOW_INVALID· HTTP 401
The window header is empty, repeated, malformed or out of range. Correct the header before retrying.REQUEST_DEADLINE_EXCEEDED· HTTP 400
An authenticated order or trigger request expired before submission; nothing was submitted. Re-sign with fresh timing if the request is still wanted.REPLAY_CAPACITY_EXHAUSTED· HTTP 429
Capacity for remembering signed requests is exhausted; nothing was processed. Back off and re-sign; a shorter window releases capacity sooner.
Fresh signed nonce for WebSocket logins
Changed: signed WebSocket upgrades require a cryptographically random nonce that is included in the signature. A previously reusable or unsigned nonce is no longer enough.
What to do: for every connection attempt to /ws/trade.proto or /ws/trade.sbe:
- Generate 16 cryptographically random bytes, encoded as exactly 32 lowercase hex characters.
- Build the signed
GETpayload using the escaped upgrade path, the canonical query and the SHA-256 of an empty body. Insert the receive-window line after the timestamp if you send its header. - Append LF followed by
ws-nonce:<nonce>, with no trailing LF. - Send that nonce in
X-API-NONCE, alongside the key ID, timestamp and signature.
The nonce header was not part of the signed payload.
TIMESTAMP_MS
GET
/ws/trade.proto
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855The signed payload ends with the nonce line. This example sends no explicit window.
TIMESTAMP_MS
GET
/ws/trade.proto
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
ws-nonce:32_LOWERCASE_HEX_CHARACTERSThe final line is a placeholder: replace it with a newly generated nonce. Do not use the timestamp as the nonce. Never reuse a login nonce after a refused upgrade, disconnect or reconnect. The same signing change applies to signed market-data WebSocket upgrades.
A refused upgrade is an HTTP response before the socket opens. Its body contains a code as text, not an order acknowledgement or a REST problem:
MISSING_HEADERS· HTTP 401
Supply a correctly formed nonce and the required signing headers.SIGNATURE_INVALID· HTTP 401
Check the path, query, nonce suffix and signature.REPLAY_DETECTED· HTTP 401
Generate a fresh nonce, timestamp and signature.REPLAY_CAPACITY_EXHAUSTED· HTTP 429
Wait briefly, then create a fresh signed login.AUTH_TEMPORARILY_UNAVAILABLE· HTTP 503
Back off, then create a fresh signed login.
Trading login reuse is refused even when you reconnect through another endpoint. Login freshness authenticates the upgrade; it does not give every later order a deadline. See Connection and authentication and the Protobuf trade WebSocket.
Trading WebSocket additions
Protobuf command receive windows
Changed: Protobuf client frames add timestamp_ms (field 3, int64) and recv_window_ms (field 4, uint32). How a command is timed depends on what you set:
- Timestamp unset or zero, window zero
No per-command deadline. - Timestamp set, window zero
5000 ms window. - Timestamp set, window 1 to 60000
Explicit window in milliseconds. - Window without timestamp, invalid timing, or timestamp over 1000 ms ahead
BAD_REQUEST; the sequence is not consumed. - Command expires on arrival or before submission
REQUEST_DEADLINE_EXCEEDED(308); the sequence is consumed and nothing is submitted.
Ping ignores these fields.
What to do: after an expiry at sequence 7, send the next command at sequence 8. After malformed timing at sequence 7, correct it and reuse sequence 7. The timing fields are optional: an existing untimed session remains untimed.
Decoded Protobuf example:
{
"seq": "7",
"correlationId": "quote-7",
"timestampMs": "CURRENT_UNIX_MILLISECONDS",
"recvWindowMs": 2000,
"createOrder": {
"order": {
"symbolId": 1,
"side": "BUY",
"baseQtyScaled": "10000",
"limitGtc": { "priceTicks": "70000000000000" }
}
}
}Use the Pair's actual quantity scale and prices. See the Protobuf trade WebSocket.
SBE timed command templates
Changed: schema ID 7 remains at version 36; its semantic version advances from 1.0.0 to 1.2.0.
Existing untimed messages keep their layouts. New templates add timestampMs and recvWindowMs to
six commands:
| Template | ID |
|---|---|
CreateOrderTimed | 11 |
CancelOrderTimed | 12 |
ModifyOrderTimed | 13 |
BatchCreateOrdersTimed | 14 |
BatchReplaceOrdersTimed | 15 |
BatchCancelOrdersTimed | 16 |
Their timing rules and sequence handling match Protobuf command receive windows. The suffix adds 12 bytes to the corresponding fixed block, before variable data or groups. One timing pair covers an entire batch. Acknowledgement templates stay unchanged.
What to do: refresh your codecs to use these additions. Timing is excluded from batch retry identity, so refresh the timing while preserving the command's retry identity and payload. See Order entry and the Message reference.
Dead-man switch on the trading socket
Added: arm, refresh or disarm the same account timer that POST /v1/orders/cancel-all-after and CancelAllAfter set, directly on
the trading socket.
Protobuf adds ClientFrame.cancel_all_after (field 18) and AckResponse.cancel_all_after (field 8). Decoded Protobuf example:
{
"seq": "42",
"correlationId": "heartbeat-42",
"cancelAllAfter": {
"requestId": "heartbeat-42",
"timeoutSec": 60,
"symbolId": 1,
"side": "SELL"
}
}requestId is required. Omit subaccountId: the socket trades for its key's account. Use timeoutSec: 0 to disarm; 10 through 120 arms or refreshes. Other values return CANCEL_ALL_AFTER_TIMEOUT_INVALID (222).
SBE adds CancelAllAfter template 10, CancelAllAfterAck template 30, CommandType.CANCEL_ALL_AFTER = 10, RateLimitOperation.CANCEL_ALL_AFTER = 8, and statuses ARMED = 1 and DISABLED = 2. A zero symbolId means every Pair; a null or zero side means both
sides.
What to do: refresh well inside the timeout, for example every 15 to 30 seconds for a 60-second timer. The timer belongs to the account and is shared with HTTP: closing the socket neither fires nor disarms it. Every arm restarts it from application time, and an arm also replaces its scope. Arms, refreshes and disarms spend cancellation quota.
requestId each time: an identical HTTP retry can replay an earlier answer without
refreshing the timer.See Dead-man switch.
Batch replacements and order reconciliation
These changes concern POST /v1/orders/replace-batch and BatchReplaceOrders. See Batch orders.
Admission is answered before completion
Clarified, with improved immediate status reads: a batch replace answers admission before each accepted item's amend or replace completes. A refresh that follows soon after can therefore be refused while the earlier change is pending. The code depends on how the refresh identifies the order:
- By
clientOrderIdORDER_MODIFY_IN_PROGRESS(226). - By the returned
replacementOrderIdORDER_PLACEMENT_PENDING(227), or 226.
227 means placement is not yet confirmed; the order may already be on the book.
What to do: inspect each item's admission result. Retry only explicitly rejected items with 226 or 227, after a short, capped backoff with jitter, in a new batch with a fresh request ID. Reusing the rejected batch's ID replays its rejection. Never resend admitted items as a fresh batch.
REST retry example:
POST /v1/orders/replace-batch
Content-Type: application/json
{
"requestId": "refresh-2-retry-1",
"symbol": "BTC-USDT",
"items": [
{"clientOrderId": "quote-bid-1", "newPrice": "100.10"}
]
}Keep REST symbol and newPrice; the binary and Protobuf contracts use symbolId and newPriceTicks. These are existing interface differences, not renames in this release.
Batch status is readable at once and kept 60 seconds
Changed: an accepted batch can be read as soon as its admission response returns. Previously an immediate read could report not found despite acceptance.
Read status with GetBatchReplaceStatus (POST /orders.v1.OrdersReadService/GetBatchReplaceStatus), passing the returned server-issued batchRequestId as an exact decimal fixed64 string in Protobuf JSON. This is a public RPC read, not
a new REST alias. The execution phase then advances asynchronously.
What to do:
- Retention and identical-request admission replay last 60 seconds from acceptance.
UNAVAILABLEmeans status cannot currently be established: retry the same status read within that window.NOT_FOUNDmeans no retained batch for that account and identity. It does not establish rejection.- Once retention ends, reconcile the individual old and replacement orders. Never resubmit an accepted batch because its status is no longer found.
- A refused item's
updatedTsNscan reflect a later observation time. Do not use it as the refusal's identity.
If the original response was lost, send the identical request with the same request ID within the replay window, or read the batch status when its identity is known. Never use a fresh request ID to resolve an unknown outcome.
Trigger controls and shutdown safety
See Triggers.
Pausing or canceling a trigger requires TRADE_SPOT
Changed: pausing or canceling a trigger now requires TRADE_SPOT on the API key's policy. A
read-only key can no longer stop a trigger. These risk-reducing operations do not require the
trigger's Pair to remain in the key's market allowlist. This covers POST /v1/triggers/{trigger_id}/pause, DELETE /v1/triggers/{trigger_id}, PauseTrigger and CancelTrigger.
What to do: grant TRADE_SPOT to the key responsible for trigger shutdown. A key without TRADE_SPOT, including a key with no policy, gets HTTP 403 POLICY_SPOT_TRADE_DENY; correct the
policy rather than retrying. See API key policies.
Path and body trigger IDs must agree
Changed: modifying, pausing or resuming a REST trigger rejects a non-empty body triggerId that
differs from the path ID. Previously the path silently won. HTTP 400 PATH_BODY_ID_MISMATCH (95)
changes neither trigger. The rule applies to PUT /v1/triggers/{trigger_id}, POST /v1/triggers/{trigger_id}/pause and POST /v1/triggers/{trigger_id}/resume.
What to do: omit the redundant body ID or send the same ID.
A different body triggerId was ignored and the path ID won.
PUT /v1/triggers/<trigger-id>
Content-Type: application/json
{"triggerId":"<other-trigger-id>","maxSlippageBps":50}Send no body ID, or the same ID as the path.
PUT /v1/triggers/<trigger-id>
Content-Type: application/json
{"maxSlippageBps":50}This example tightens an existing wider market-child cap to 0.5%; it is not a universal patch for every trigger type. Changing a child limit price remains unsupported: cancel and recreate the trigger.
For Subaccount triggers, a session-token caller must name subaccountId. A Subaccount-bound API key
may omit it or name its own Subaccount; a root-scoped trading key must omit it.
Cancel-all does not stop triggers
Clarified, not a new behavior: POST /v1/orders/cancel-all and dead-man-switch
expiry cancel open orders, including trigger-generated orders, but do not cancel triggers.
- Standalone stop-loss, take-profit, trailing triggers and TWAPs can place new orders afterward.
- A ladder stops once its levels are canceled.
- An attached exit leg ends when its working exit order is canceled; with
oco, its sibling legs end too. That position loses their protection. - Other attached legs keep protecting the parent's filled quantity. A parent's legs end if it is canceled before any fill.
What to do: to stop automated trading, list and cancel live triggers first, then cancel open orders and reconcile. Do not treat a dead-man-switch heartbeat as protection against all future trigger activity. See Cancel all orders.
Successful REST trigger cancellation still returns status: "CANCELED"; Protobuf uses STATUS_CANCELED. REST trigger-event spellings such as FIRE_REFUSED are unchanged; corrected
descriptions are not enum migrations.
Parent-order trigger lists include moved legs
Changed: trigger lists filtered by a parent order
(GET /v1/triggers, ListTriggers) now include
attached legs that moved to that parent through replacement and then ended.
What to do: reconcile against the latest returned parent relationship rather than assuming an absent leg never existed.
REST validation and error handling
See Error codes and Error responses.
More specific order and trigger error codes
Changed: invalid order fields now keep specific public codes instead of being collapsed into generic validation failures. HTTP status remains 400.
INVALID_SIDE
Invalid side.INVALID_STP_MODE
Invalid self-trade prevention mode.FEE_ASSET_INVALID
Invalid fee asset.INVALID_CLIENT_ORDER_ID
Invalid client order ID, includingnewClientOrderId.INVALID_MODIFY_BEHAVIOR
Invalid modify behavior.TRIGGER_PRICE_SOURCE_UNSUPPORTED
Invalid trigger price source.CANCEL_ALL_AFTER_TIMEOUT_INVALID
Invalid dead-man-switch timeout.
Malformed REST trigger decimals now use INVALID_ARGUMENT, previously BAD_REQUEST. Malformed
order decimals keep INVALID_ARGUMENT. Field violations now identify the REST field, including
nested batch indexes, for example items[0].limitGtc.price or trigger.stopLoss.triggerPrice.
What to do: keep violations[].fieldPath and the specific code. Branch on codes, not diagnostic
messages. Orders still express execution through objects such as limitGtc and marketIoc; do not
introduce separate REST order-type or time-in-force fields.
A zero Subaccount selector depends on the API
Changed: order and trigger requests now refuse an explicitly supplied zero Subaccount selector
instead of treating it as omitted. On REST, IDs are base58-encoded, so zero is written "1" and a
literal "0" is a malformed value. In Protobuf, zero is an explicitly present fixed64 zero.
Clarified: what a zero selector does on each API:
- Orders and triggers, reads and writes
Refused: REST returns 400INVALID_SUBACCOUNT_ID, ConnectRPC returnsinvalid_argument. Leaving the field out selects the Main Account, or the Subaccount the API key is bound to. - Balance and transfer reads
Refused with 400INVALID_SUBACCOUNT_ID. - Fee rate and policy selectors
Refused with 400AUTH_INVALID_REQUEST. - Address book, deposit addresses, transfer source, withdrawals and auto-withdraw
Zero means "not set": the default account applies, subject to permissions. - Transfer destination
Zero is refused.
What to do: omit subaccountId when selecting the Main Account or a key's bound account. Never
serialize a default zero as a substitute for omission.
On an order request, an encoded zero was treated as omitted.
{
"subaccountId": "1",
"requestId": "quote-7"
}Leave the field out. On an order request, sending "1" returns 400 INVALID_SUBACCOUNT_ID.
{
"requestId": "quote-7"
}Trailing zeros beyond a field's precision
Changed: transfer and withdrawal amounts and policy notional caps now accept trailing zeros
beyond their precision, as order prices and quantities already did. For example, "1.500000000000000000000" is accepted as "1.5".
Clarified: for every field, trailing zeros beyond the precision are accepted and never rounded, and any nonzero digit beyond it is refused. Precision depends on the field:
- Transfer and withdrawal amounts
18 decimal places. - Order and trigger prices
9 decimal places. - Quantities
The Pair's base Asset precision. Amounts in the quote Asset follow the quote Asset's precision. - External withdrawals
Also limited to the destination token's own precision. - Policy notional caps
6 decimal places.
The "18" in the API reference describes numeric precision, not a text length limit.
What to do: send values within each field's precision. Do not rely on the server to round.
Oversized bodies return REQUEST_TOO_LARGE
Changed:
- Auth request bodies over 128 KiB · HTTP 413
AUTH_REQUEST_TOO_LARGEbecomesREQUEST_TOO_LARGE; signed requests also receive a structured problem. Parse the problem and reduce the body. - Balance, history, equity and transfer read bodies over 1 MiB · HTTP 413
PAYLOAD_TOO_LARGEbecomesREQUEST_TOO_LARGE. Update your code matching and keep HTTP 413 handling.
Auth path and body IDs must agree
Changed: conflicting resource IDs on affected auth operations now return HTTP 400 AUTH_PATH_BODY_ID_MISMATCH (58). This applies to MFA, member and invite, and policy-assignment
operations, and to address-book copies.
What to do: omit redundant body IDs or match the path.
Wallet login with an already linked signer
Changed: a wallet login
(POST /v1/auth/login/wallet) with a signer
already linked to another account now returns HTTP 409 AUTH_WALLET_ALREADY_LINKED (55), instead
of an internal failure.
What to do: request a new challenge for the address that account already uses. See Session login.
Trading-policy denials stay typed
Changed: established trading-policy denials remain typed denials when they cannot be rechecked immediately.
What to do: correct permission failures; reserve retry loops for explicit transient responses.
Rate-limit responses
Structured 429 problems
Changed: REST order and trigger writes and public sign-in and catalog calls now receive a structured HTTP 429 problem when the public API rejects them for rate limiting, replacing earlier HTML or generic JSON responses. A refusal returned by the operation itself keeps its own body and headers.
Example fragment:
{
"status": 429,
"code": "RATE_LIMIT_EXCEEDED",
"rate_limit": {
"reason": "QUOTA_EXCEEDED",
"scope": "CLIENT_IP"
}
}What to do: handle the HTTP status, the problem code and the optional rate_limit fields
together.
- Honor
Retry-Afterwhen present. - Request-count refusals can carry
X-RateLimit-*; operation quota refusals can carryRateLimit-*and richer details. - Concurrency or instantaneous refusals may carry neither retry nor quota headers. Back off without assuming a server-supplied delay.
REPLAY_CAPACITY_EXHAUSTEDis a separate retryable 429, withoutRetry-After; it is not a bad-credentials error.- Capacity refusals such as
AUTH_RESOURCE_EXHAUSTEDare not ordinary quota exhaustion. Inspect the code before retrying.
Read the configuration with GET /v1/rate-limits and the
authenticated GET /v1/trading/rate-limits (GetRateLimitConfig and GetTradingRateLimits in ConnectRPC). REST and WebSocket aliases of the same trading action share quota; adding sockets or
keys does not multiply account capacity. This migration implies no new fixed trading quota. See Rate limits.
Market data, shapes and pagination
Market overview honors symbolId filters
Changed: GET /v1/spot/markets/overview (ListMarketOverview):
- Repeated
symbolIdfilters are now honored instead of ignored. Check the selected market before using a row's prices. - An invalid REST
symbolIdnow returns 400BAD_REQUEST; unknown or disabled selected IDs yield no row. Send positive uint32 IDs and accept an emptymarketsresult. - A Pair repeated through
symbols,symbolId, or both is now returned once; previously a Pair repeated insymbolswas refused with HTTP 400. Expect one row per Pair.
GET /v1/spot/markets/overview?symbolId=1&symbolId=23&limit=50Clarified: an unfiltered overview already includes configured disabled markets and excludes
markets past delisting. That is a documentation correction, not a new enabled-market filter.
Overview defaults to 50 rows, maximum 2000, ordered by descending 24-hour USD volume unless you
request otherwise. Continue with nextPageToken as pageToken, keeping the filters and ordering;
the page size may change.
Order-book symbol lookup accepts any case
Changed: REST order-book symbol lookup
(GET /v1/orderbook/{symbol}) accepts case variants
and returns the canonical symbol.
What to do: normalize cache keys using the returned symbol. See Order book.
Spot-config assets add includedInEquity
Added: assets in GET /v1/spot/config (GetSpotConfig) add the
optional includedInEquity.
What to do: include assets explicitly marked true in equity. false contributes no value; an
absent value means unknown.
Historical heatmap pages start from a full book
Changed: historical heatmap pages
(GetOrderbookHeatmap)
now reconstruct their starting book, including quiet windows, and keep raw best prices where
available. Unanchored history before the oldest available snapshot is omitted.
What to do: apply deltas oldest-first over the returned base keyframe, and stop at an empty continuation token. Accept an empty chain for a window before replayable history.
No blanket REST format migration
Clarified: there is no blanket REST casing, ID or decimal-format migration in this release. Keep REST lower-camel-case fields, opaque public IDs and human-unit decimal strings. Protobuf trading messages keep their fixed64 IDs and scaled integers.
Account permissions and policy tools
See API key policies and Subaccounts and roles.
Funding-tool reads need explicit API-key permissions
Changed: API keys now need explicit account permissions for deposit-address, Subaccount and GuardSigner reads, and for Main Account address-book reads. A key with no policy loses those previously available calls.
Existing policies receive the corresponding access grants on upgrade, but new policies and full
replacements of actions must keep the permissions your client needs.
CREATE_DEPOSIT_ADDRESS
Create a deposit address:POST /v1/chain/deposit-addresses,CreateDepositAddress.READ_DEPOSIT_ADDRESSES
List deposit addresses:GET /v1/chain/deposit-addresses,ListDepositAddresses.READ_SUBACCOUNTS
List and get Subaccounts:ListSubaccounts,GetSubaccount.READ_SUBACCOUNT_ACTIVITY
Subaccount activity:ListSubaccountActivity.READ_SUBACCOUNT_MEMBERS
Member expansion on Subaccount detail. Listing members (ListSubaccountMembers) is newly available to API keys.READ_SUBACCOUNT_INVITES
Outgoing invites and invite expansion:ListSubaccountInvites.READ_GUARD_SIGNER_STATUS
GuardSigner status for accounts within ownership and key scope:GetGuardSignerStatus.READ_ADDRESS_BOOKorMANAGE_ADDRESS_BOOK
Main Account address-book reads:ListAddressBookEntries.READ_WITHDRAW_VALIDATION
Validate a withdrawal destination, previously allowed byEXTERNAL_WITHDRAW:POST /v1/chain/withdraws:validate-destination,ValidateWithdrawDestination.
What to do: attach an API-key policy where none exists and include only the permissions you
need. Missing grants return AUTH_API_KEY_ACCESS_DENIED, with two exceptions:
- Address-book reads
AUTH_SUBACCOUNT_ACCESS_DENIEDon both APIs: HTTP 403 on REST,permission_deniedon ConnectRPC. If the caller is not allowed to know the address book exists, the answer is 404 instead. - Destination validation
PERMISSION_DENIED.
The Subaccount detail read omits unauthorized member and invite expansions rather than failing the whole read.
GuardSigner status stays with the owning Main Account
Changed: a key can no longer read GuardSigner status for a Subaccount owned by another Main Account merely because the Subaccount is shared with it.
What to do: use the owning account's credential or an authorized interactive session. See GuardSigner.
Newly available API-key account reads
Added: these permissions are optional. Each grants its named calls only, and belongs on an API-key policy, not a Subaccount policy.
READ_API_KEYS
List and get keys within scope:ListApiKeys,GetApiKey.READ_API_POLICIES
List and get API-key policies within scope:ListApiPolicies,GetApiPolicy.READ_SUBACCOUNT_POLICIES
List and get Subaccount policies within scope:ListSubaccountPolicies,GetSubaccountPolicy.READ_SUBACCOUNT_PERMISSIONS
Read what the key itself may do:GetEffectiveSubaccountPermissions.READ_VIP_STATUS
Main Account VIP status:GetVIPStatus.READ_SOCIAL_VERIFICATION
Main Account social-link status:GetSocialVerification.READ_PROFILE
Main Account profile and username history:GetProfile,GetUsernameHistory.READ_ACCOUNT_DIRECTORY
Account lookup:ResolveAccount.READ_DAILY_CLAIMS
Main Account daily claim status:GetDailyClaimStatus.CLAIM_DAILY_REWARD
Claim the Main Account's daily reward:ClaimDailyReward.READ_CHECKPOINTS
Main Account checkpoints:ListMyCheckpoints.READ_REWARDS
Main Account reward awards:ListMyRewardAwards.
Main-Account-only calls still refuse Subaccount-scoped keys, even if the permission is present. Account and policy management remain interactive-session operations. The new reward acknowledgement call and reward destination changes are session-only.
Destination validation needs READ_WITHDRAW_VALIDATION
Changed: destination validation now requires READ_WITHDRAW_VALIDATION, rather than EXTERNAL_WITHDRAW alone. Existing policies with withdrawal permission receive the read grant on
upgrade.
What to do: keep READ_WITHDRAW_VALIDATION when you replace actions.
Clear a notional cap with null
Added: PATCH /v1/auth/policies/subaccounts/{policy_id} can clear the notional cap with {"maxOrderNotional":null}, using the latest revision in If-Match. Omitting the field keeps the cap. Zero is not a clear instruction. A cleared policy
still reads back maxOrderNotional: "0".
PATCH /v1/auth/policies/subaccounts/<policy-id>
If-Match: "<latest-revision>"
Content-Type: application/json
{"maxOrderNotional":null}Funding, transfers and withdrawals
Transfer reason 1074 is now system_adjustment
Changed: REST and JSON transfer streams rename code 1074 from operator_restore to system_adjustment. Numeric code 1074 stays unchanged. This affects GET /v1/transfers, ListTransfers and the ledger transfers stream.
{ "transferCode": "operator_restore", "isDebit": false }{ "transferCode": "system_adjustment", "isDebit": false }What to do: update handwritten label and filter mappings, and use isDebit for direction; a
system adjustment can debit or credit the account.
New transfer reason codes
Added: these published codes.
| Numeric code | REST/JSON label |
|---|---|
| 1066 | trading_withdraw_fee_leg |
| 1067 | quickswap_polyester_fee |
| 1068 | quickswap_broker_fee |
| 1069 | quickswap_affiliate_fee |
| 1072 | fee_sweep |
| 1073 | fee_refill |
| 1075 | withdraw_failed |
Protobuf enum names use uppercase. withdraw_failed records funds debited for a rejected or
execution-failed withdrawal; do not treat it as an automatic Trading or Funding refund.
Lifecycle history includes received transfers
Changed: account-scoped GET /v1/chain/flows (ListFlows) includes
transfers received from other accounts, in addition to owned flows. Each row has participation: "OWNER" or "RECIPIENT". A movement within one account remains an owner row; each
flow appears once.
What to do: pass participation=OWNER to keep the old owner-only result set.
Returned owned flows only.
GET /v1/chain/flows?ownerAccountId=<account-id>The same owner-only result set needs the participation filter.
GET /v1/chain/flows?ownerAccountId=<account-id>&participation=OWNERA non-default participation filter requires an account selector. Pagination tokens now bind the
account selector, participation, every filter including scope, the sort direction and the ordering
timestamp. Discard old tokens and restart from the first page. Keep the query unchanged while
continuing; only limit may change. A token used with another query is invalid. Continue until nextPageToken is empty. See Track funding flows.
Internal-transfer receipts
Added: internal-transfer creation
(POST /v1/trading/internal-transfers)
returns flowId. The new GET /v1/trading/internal-transfers (GetInternalTransfer)
reads one accepted receipt by exactly one of requestId or transferId, with an optional source subaccountId.
This read requires an interactive session; API keys are not accepted. Unknown, pending or inaccessible receipts return not found. It has no pagination and moves no funds.
What to do: persist requestId, transferId and flowId. Reconcile through the receipt and GET /v1/chain/flows/{flow_id}, rather than
resubmitting or matching amounts. See Internal transfers.
Withdrawal assetId is marked required
Clarified: the withdrawal request schema now correctly marks payload.assetId required. This
documents the existing asset requirement.
What to do: keep supplying it when signing withdrawals.
Rejected withdrawals refund to the wallet
Changed: a rejected withdrawal stays open while its refund is pending, then ends refunded when net funds reach the sending smart-account wallet as zTokens. Its request fee (the route fee locked when the withdrawal was requested) is kept. The refund is not automatically credited back to Trading or Funding.
A withdrawal whose destination execution failed after submission ends failed, with ZIPPER_WITHDRAW_EXECUTION_FAILED, and is handled separately; do not infer a wallet refund.
The request fee can now have REST status KEPT (Protobuf REQUEST_FEE_STATUS_KEPT), and the
refunded step adds refundAddress. That step's amount is net of the kept fee.
What to do: reconcile the actual refund location and the kept fee. Terminal processing does not necessarily mean successful delivery. See Withdraw from Trading.
Wallet balances read
Added: GET /v1/chain/wallet/balances (GetWalletBalances),
optionally with subaccountId, returns nonzero wallet holdings ordered by zippedAssetId. Entries
contain zippedAssetId, unifiedAssetId, chainId, ztokenAddress and a whole-unit decimal amount. API keys need READ_BALANCES.
Destination memo rules
Added: GET /v1/chain/deposit-withdraw/config (GetDepositWithdrawConfig)
includes memoRequirement, memoLabel and memoFormatHint.
What to do: recognize REQUIRED, OPTIONAL, NOT_SUPPORTED and the unknown MEMO_UNSPECIFIED. These rules apply to payout destinations, including QuickSwap return and refund
destinations, not to deposit instructions. Do not infer "optional" from an unknown value or require
a tag on every Network. Apply the Network's actual rule.
Auto-withdraw REST paths
Added: REST paths for auto-withdraw, if you use it. See Auto-withdraw.
- Read rule
GET /v1/auto-withdraw/rule - Set rule
PUT /v1/auto-withdraw/rule - Pause or resume
PUT /v1/auto-withdraw/rule/enabled - Preview the next run
GET /v1/auto-withdraw/preview - Read run history
GET /v1/auto-withdraw/runs
Pause replaces DisableAutoWithdrawRule
Removed: DisableAutoWithdrawRule. Replace it with SetAutoWithdrawRuleEnabled with enabled: false, or the REST pause path.
POST /chain.withdraw.v1.AutoWithdrawService/DisableAutoWithdrawRule
Content-Type: application/json
{"expectedVersion":3}PUT /v1/auto-withdraw/rule/enabled
Content-Type: application/json
{"enabled":false,"expectedVersion":3}Pausing keeps the schedule and destinations and needs no fresh step-up. Resuming requires a fresh
step-up and revalidates destinations. API keys can read with READ_AUTO_WITHDRAW, but cannot write
rules.
What to do: send the latest expectedVersion; a real state change creates a new version.
Requesting the state a rule already has leaves it unchanged. A rule runs only when enabled is
true and pause is absent.
Auto-withdraw refusal codes
Changed: rule refusals use new codes.
| Previous code | New code |
|---|---|
CONFLICT | VERSION_CONFLICT |
AMOUNT_BELOW_MINIMUM | MIN_AMOUNT_BELOW_FLOOR |
INVALID_DESTINATION_ADDRESS | DESTINATION_INVALID |
UNSUPPORTED_ASSET or INVALID_DESTINATION_CHAIN | ROUTE_UNSUPPORTED |
Problems identify field_path; below-floor refusals also expose a decimal min_floor. Rules add createdByAccountId; destinations can include minFloor. Unknown floors are omitted.
What to do: update your code matching to the new codes.
Auto-withdraw previews and run steps
Added: previews add status (WILL_PAY, ROLLS_OVER, NO_DESTINATION, PAUSED, NO_RULE), nextRunAt and, per Asset, payable, heldBack, holdReason, and the optional minimum, valueUsd, pricedAt and networkFee. A preview is an estimate from current balances and prices.
Unknown optional amounts, times and fees are absent, never synthetic zeroes.
Run steps add the actual networkFee when it is reported at finality. A failed withdrawal can keep
that fee.
What to do: do not treat every failed step as funds restored to the account.
Broker vault destination cooling period
Added: broker vault destinations have a 24-hour whitelist cooling period. An early withdrawal
returns EXTERNAL_DESTINATION_COOLING_DOWN, with payable_from when known. Previews hold with destination_cooling_down and an optional payableFrom; runs skip that payout and keep the funds
for a later run. Ordinary accounts are unaffected.
A broker author's role change pauses their rules with authority_revoked. An authorized Owner or
Finance member must resume them with a fresh step-up.
QuickSwap integrations
These changes concern QuickSwap funding and conversion tools, if you use them. They do not change
ordinary spot order payloads. See Other APIs and QuoteQuickSwap, CreateQuickSwap, POST /v1/quickswaps:quote and POST /v1/quickswaps.
Small amounts, returns and compliance transfers end with specific outcomes
Changed: QuickSwap now answers small-amount, return and compliance cases with specific outcomes. They arrive in two different forms, and your client should keep them apart:
- Error codes refuse a request. Real QuickSwap failures use QuickSwap's own error codes.
- Lifecycle values are response fields that describe a swap's progress: reasons, statuses, address and withdrawal states, withdrawal roles and broker roles. They are not error codes.
On the order and trading WebSocket APIs, a market maker only ever sees order error codes, including
the order errors named QUICKSWAP_*, never QuickSwap's lifecycle values.
QuickSwap error codes:
AMOUNT_OUT_OF_RANGE
A market quote whose guarantee is tiny or rounds to zero, which previously failed without a useful answer. Increase the amount; do not retry unchanged.PROTECTION_UNACHIEVABLE
The same case for a TWAP quote. Increase the amount or adjust protection or slicing; do not retry unchanged.REFERRAL_NOT_SUPPORTED,REFERRAL_CODE_UNKNOWN,REFERRAL_PARTNER_INACTIVE
Referral refusals; see Referral codes on quote and create.
QuickSwap lifecycle values:
- Intermediate funds too small for the next market leg
Resolve as below minimum, rather than a protection failure or an unnecessary wait. Recognize reasonINTERMEDIATE_BELOW_MINIMUMwhen a refund can be requested; otherwise reasonREFUND_BELOW_MINIMUMwith withdrawal stateNOT_SENT. - Dust-only source returns and intermediate refunds
Can now end terminal instead of remaining executing. Accept terminal statusRETURNEDorREFUNDEDwith reasonREFUND_BELOW_MINIMUM; a roundedunsentAmountcan be zero. - A credited deposit that cannot be traded
Can now return automatically instead of remaining held. Handle statusRETURNINGthenRETURNED, with reasonMARKET_CLOSED,PROTECTION_NOT_MET,DEPOSIT_BELOW_MINIMUMorDEPOSIT_ABOVE_MAXIMUMas applicable. - A blocked deposit transferred out after compliance review
Ends with terminal statusCLOSED(17), withdrawal roleCOMPLIANCE_TRANSFER(4) and lifecycle reasonZIPPER_COMPLIANCE_TRANSFERRED(105). This is not a return to the user.
A compliance-transfer withdrawal is finalized but omits recipient, memo, amount and network fee. Its
transaction identifies the transfer; the deposit's disposition remains ON_HOLD. Do not fill
absent values with a guessed payout.
Other changes:
- Minimum deposit
Can increase to guarantee at least one destination token base unit. Read fresh quote limits instead of hardcoding old minimums. - Market closure
Waits a bounded time from deposit credit, including for TWAP, rather than the TWAP duration. Continue tracking the existing swap; do not submit another to resolve it.
Quotes are estimates
Clarified: quoted output and fee amounts are estimates. Fee rates are fixed, but actual fees and output can be above or below the quote as the deposited amount and execution change.
What to do: use protection fields for guarantees, and the actual execution, withdrawal and charged-fee fields for accounting.
Referral codes on quote and create
Added: optional referralCode on quote and create, quote.referral with baseBrokerBps, discountBps and commissionBps, and fee kind REFERRAL (6). Referral commission comes from the
broker fee and is separate from affiliate fees.
What to do: do not count referral commission as a broker vault credit. Handle the error codes REFERRAL_NOT_SUPPORTED, REFERRAL_CODE_UNKNOWN and REFERRAL_PARTNER_INACTIVE before retrying
without a code.
Broker vault balances
Added: GET /v1/quickswap/brokers/{broker}/vault-balances (ListVaultBalances)
returns current broker vault balances, for broker members and broker API keys. Its settled amount includes pendingOutgoing.
What to do: subtract pending outgoing once for the uncommitted balance. This excludes order holds and is not a withdrawability guarantee. Reads are not an atomic snapshot across vaults. Broker withdrawals and payout settings require Owner or Finance authority.
Other account-tool additions
Version check
Added: the unauthenticated GET /v1/system/version (GetVersion) returns the string
fields version, commit and environment.
What to do: use it to identify the build before testing. It does not prove every endpoint is healthy.
Changelog post revisions
Added: post summaries from GET /v1/changelog/posts (ListPosts) add revision and updatedAt.
What to do: cache posts by slug and revision; publication time alone no longer detects a
corrected post. If a post changes during change-list pagination
(GET /v1/changelog/changes), its changes can
repeat: deduplicate them by their stable change IDs.
Daily Testnet claims
Added: CLAIM_OUTCOME_UNKNOWN from POST /v1/trading/daily-claim/claim (ClaimDailyReward) means a
recorded claim's payout is not confirmed. Reward REST refusals now consistently use problem JSON.
What to do: read the status with GET /v1/trading/daily-claim or retry the same
daily claim; do not create another payment attempt.
Telegram social linking
Added: Telegram adds TELEGRAM, STATUS_AWAITING_CONFIRMATION, POST /v1/auth/social/verification:confirm and POST /v1/auth/social/verification:unlink,
plus display and action fields. Interactive confirmation requires enrolled MFA and a fresh step-up.
Existing trading request authentication does not become interactive MFA.
Final client checks
Compare fresh and expired HTTP requests
Compare a fresh signed HTTP request with a deliberately expired request, and distinguish authentication skew from pre-submission expiry.
Test WebSocket logins
Test a fresh signed WebSocket login and a replayed login. Reconnect with a new nonce.
Check command timing on the socket
Verify that expired commands advance the sequence, malformed timing does not, and the socket remains usable.
Reconcile mixed batch results
Test mixed batch admission results and reconcile without resubmitting accepted items.
Rehearse a shutdown
Confirm the shutdown key can cancel triggers, then cancel open orders.
Check funding tools
Verify funding-tool permissions and restart lifecycle pagination from page one.
Decode the new contract
Decode new fields and enum values without losing unknown-field tolerance or optional-field presence.