Skip to main content

Error Model

Senticore supports a common error envelope while existing integrations retain their legacy response fields during migration.

  1. The HTTP error envelope — for request-level failures (bad shape, auth, not found, rate limit). A { "ok": false, "error": { … } } object.
  2. The SubmitResponse — for POST /api/v1/trading/actions (and MM/BSL order entry). Even a rejected order returns a flat SubmitResponse with a top-level error string and a derived code. Agent/control-plane errors also retain a flat code and error string.

Send X-SC-Error-Format: envelope-v1 to select the common JSON error format:

{
"ok": false,
"error": {
"code": "nonce_replayed",
"message": "Use an unused nonce inside the account window and re-sign the action.",
"retriable": false,
"requestId": "req_example",
"details": { "nonceConsumed": false, "nonceFloor": 42, "nonceWindow": 256 }
}
}

Treat messages as human guidance, never as a parseable contract. Exact messages can change; branch on code and structured details. Quote requestId in support requests. Store/crypto/parser internals belong in correlated server logs. Existing flat responses retain their fields and gain request correlation; the canonical format preserves reconciliation fields under error.details.

Legacy representations emit Deprecation / Sunset metadata. The earliest retirement review is 2027-03-07; removal also requires a release decision and evidence of client migration. Existing signed routes continue processing the original path, because redirecting an HMAC-signed POST changes its signature.

ConditionCanonical HTTP statusClient action
Validation / insufficient trading balance400Correct the action or fund the account, then re-sign as needed.
Invalid or expired authentication401Refresh credentials and sign a fresh request.
Permission failure403Use an authorized credential/account.
Unknown market, order or resource404Refresh identifiers and reconcile existing orders.
Idempotency / nonce conflict409Reconcile the relevant key or nonce before retrying.
Halted market / unavailable engine503Check readiness. Honor Retry-After when present; otherwise back off with jitter. Reconcile mutating requests before resending.
Rate limit429Honor Retry-After and reported budget headers when present; otherwise back off with jitter.

Default legacy lane statuses remain compatible where they differ. A previous HTTP 200 admission acknowledgement cannot change when a later terminal reject arrives; inspect receipts and private execution events. Payload-size/media-type errors retain 413/415, and timeout does not prove rejection or an unused nonce.

Decide before retrying​

Classify the result before choosing a recovery action:

ObservationWhat it establishesNext action
No HTTP response / client timeoutDelivery or admission is unknownKeep the exact signed bytes and idempotency key. Query receipt, order, and private execution state before resending.
HTTP 200 with accepted: trueIngress accepted the actionWait for a terminal receipt, fill, cancel, or rejection. Do not treat the acknowledgement as a fill.
HTTP 409 nonce/idempotency conflictThe submitted identity cannot be reused blindlyRead the nonce window or replay the matching idempotency request; change and re-sign only when the response requires it.
HTTP 429A limiter rejected this attemptHonor a real delay/budget hint when present; otherwise use capped exponential backoff with jitter. Preserve order identity.
HTTP 503The service could not prove a safe result or accept new workCheck readiness and reconcile the request. Honor Retry-After when present; it is not guaranteed.
Terminal business rejectionThe action will not execute as submittedFollow the structured code: correct fields, fund/reduce, choose an unused nonce, refresh market data, or wait for readiness.

Never automatically create a new nonce, signature, or idempotency key merely because the transport timed out. Doing so can turn one uncertain action into two distinct actions.

1. HTTP error envelope​

{
"ok": false,
"error": {
"code": "BAD_REQUEST",
"message": "Check the request fields against the endpoint schema.",
"retriable": false,
"requestId": "01J9Z6X7Q2K8N4M0",
"details": { "cause": "unknown_field" }
}
}
  • The wrapper is { ok, error }. Inner fields are camelCase.
  • Inspect retriable together with the code and details. A conflict may require a changed nonce/signature or reconciliation; retrying unchanged bytes blindly cannot repair it.
  • requestId is always present — quote it in support requests.
  • details is omitted when there's nothing structured to add; details.cause carries the machine reason for shape/validation failures.

Default generic error codes​

Generic request errors retain the status-class codes below. More specific business conditions use their documented code or details.rejectCode; negotiated submit errors use the common reject-code namespace.

HTTP statuserror.codeMeaning
400 / 422BAD_REQUESTMalformed request or strict schema/shape rejection
401UNAUTHORIZEDAuthentication required or signature invalid
403FORBIDDENAuthenticated but not authorized for this action
404NOT_FOUNDResource/route unknown
409CONFLICTIdempotency or state conflict (retriable)
429RATE_LIMITEDRate limit exceeded (retriable)
406NOT_ACCEPTABLEContent negotiation failed
503SERVICE_UNAVAILABLETemporarily unavailable (retriable)
500INTERNAL_ERROR / SERVER_ERRORInternal error (retriable)

Legacy submit responses expose business reasons as SubmitResponse.code (§2). With envelope-v1, read the equivalent error.code and error.details.

400 vs 422 on the submit path

Strict schema/shape failures (unknown field, missing field, bad enum) map to BAD_REQUEST and typically surface the cause in details.cause. Business rejections on the submit endpoint (insufficient balance, stale nonce) come back as HTTP 400 with a SubmitResponse, not the envelope — see below.

Wallet-scoped reads​

GET /api/v1/fees/standing, /api/v1/fees/statements, /api/v1/referrals, and /api/v1/points require account credentials with read scope. Send a private read-session token in Authorization: Bearer <token>, or use HMAC v2.

  • Missing, invalid, or expired read tokens return 401 UNAUTHORIZED.
  • Revoked or expired agents, a valid credential for another wallet, or one without read scope return 403 FORBIDDEN. Changing a selector never changes the authenticated identity.
  • Omitting wallet selects the verified account. Both wallet and its address compatibility alias must match that account when supplied.
  • Malformed authenticated selectors return 400 BAD_REQUEST.

These failures use the shared {ok:false,error:{code,message,retriable,requestId}} envelope with retriable:false. Renew credentials after 401; do not retry a 403 with the same identity and selector. Successful and failed responses use Cache-Control: private, no-store. The points check applies even while points are beta-disabled. POST /api/v1/referrals/claim keeps its wallet-signed body.

Application identity and attribution​

Returned when a request identifies an app (browser origin, x-sentico-app-key, or a FIX/FIXP/BSL logon key) or when an order payload carries appId/appFee. See Apps & Brokers.

CodeHTTPMeaning
APP_KEY_INVALID401The app key is unknown or revoked; the request does not fall back to anonymous
APP_NOT_ACTIVE403The app is pending or suspended
APP_CHANNEL_MISMATCH403Origin and key identify different apps
APP_ATTRIBUTION_REJECTED403Payload appId/appFee does not match the channel's app, or the surcharge payout is not the registered payout account
APP_LIMIT_EXCEEDEDrejectPer-app order notional, daily budget, order-to-trade ratio or request budget breached, or an unbounded spot market order on an app channel

Delegated agent/control-plane errors​

Delegated trading and /api/agents/* retain the legacy flat control-plane shape { "ok": false, "code": "...", "error": "...", "requestId": "...", "retriable": false }. With envelope-v1, code/message/correlation are in the common nested envelope. The following codes are the documented internal-beta contract and are checked against the server source plus endpoint regression tests:

CodeHTTPMeaning
AGENT_NOT_FOUND404Agent id does not exist
AGENT_REVOKED / AGENT_EXPIRED403Agent lifecycle no longer permits use
INVALID_SCOPE403Required independent scope is missing
AGENT_POLICY_REJECTED403Account, market, IP, or notional policy failed
AGENT_POLICY_RATE_LIMITED429Per-agent policy budget exhausted
API_CREDENTIAL_REQUIRED401Machine agent request omitted its credential
API_CREDENTIAL_INVALID401Credential/signature material is invalid
API_CREDENTIAL_REVOKED403Credential was revoked or its overlap ended
ACCOUNT_MISMATCH403Agent/credential is bound to another trading account
INVALID_SIGNATURE400Delegated payload signature is invalid
DUPLICATE_NONCE409Delegated nonce was already consumed
MACHINE_AUTH_REPLAY409Credential/timestamp/SC-Nonce tuple was already used; allocate a fresh tuple and recompute transport authentication
PUBLIC_WALLET_FILTER_UNSUPPORTED400Public trade history is anonymous; authenticate and use account-scoped fills
STORE_READ_FAILED500Account/control-plane read failed; retain requestId, back off and retry the read
NONCE_EXHAUSTED429Delegated nonce allocation or policy budget is exhausted
TRADING_ACCOUNT_CLOSING403Closing/archived account attempted a non-reducing action
AGENT_AUTH_FAILED403This credential lacks permission: the credential lacks the required scope, or the agent key and the HMAC credential belong to different wallets (observed on production 2026-09-13)
TRADING_HALTEDrejectTrading is halted. Wait for trading to resume before retrying. — the global safety gate is closed; stop placement and poll GET /api/v1/status (tradingHalted, cancelOnlyAvailable, expectedReopenMs)

DUPLICATE_NONCE includes lastNonce in the flat legacy response and error.details.lastNonce in the common envelope. Choose a strictly greater delegated-auth nonce below the reserved u64 maximum; do not regex the message. This is distinct from the action replay window and SC-Nonce tuple replay.

For /api/v1/trading/orders/batch, an item that lacks trade, cancel, or mint_redeem appears as a failed item in the HTTP 200 batch response; it does not gain fallback authority through quote.

2. Submit responses​

Without error-format negotiation, POST /api/v1/trading/actions retains the flat SubmitResponse fields, with optional reconciliation values set to null when unavailable. Request correlation is additive.

Accepted (HTTP 200, not 202):

{
"accepted": true, "ok": true, "seq": 84213377,
"derivedOrderId": "0x7d3a1f90…", "error": null, "code": null,
"nonceConsumed": null, "nextUsableNonce": null,
"nonceFloor": null, "nonceWindow": null, "nonceHoles": null
}

ok mirrors accepted. derivedOrderId is set only for order-placing actions, null otherwise. accepted: true is an ingress acknowledgement, so nonceConsumed remains null until a terminal engine result is available.

Legacy rejected response (status varies by lane; canonical nonce conflict is 409):

{
"accepted": false, "ok": false, "seq": null, "derivedOrderId": null,
"error": "stale nonce: account=0x1111… nonce=41 next_nonce=42",
"code": "nonce_mismatch", "nonceConsumed": false,
"nextUsableNonce": 42, "nonceFloor": null, "nonceWindow": null,
"nonceHoles": null
}

code, nextUsableNonce, nonceFloor, and nonceWindow are derived from the rejection — every nonce reject carries the window fields so you can resynchronize without guessing. On production the window-edge reject (numeric reason 205 on the binary lanes) reads … above replay window and carries nonceHoles; those nonces are directly usable, so take one of them instead of guessing. The same window view is on GET /api/v1/accounts/{account} (nonceFloor, nonceHoles, nonceUsedInWindow, nextNonce). See Order Concurrency & Nonces.

Submit code values​

These are the actual codes the submit path emits. Treat anything not listed (or null) as non-retriable unless the HTTP status says otherwise, and keep the raw error text in your logs.

CodeRetriableMeaning
nonce_below_floorNo (pick fresh)Below the window floor — permanently consumed/stale
nonce_outside_windowAfter floor advancesToo far ahead; fill lower nonces first
nonce_replayedNo (pick fresh)In-window but already used/in-flight
nonce_mismatchAfter resyncUncategorized nonce reject; use nextUsableNonce
insufficient_balanceNoBalance below order requirement
risk_would_exceed_available_usdcNoProjected aggregate USDC requirement exceeds available free USDC
insufficient_usdcNoUSDC balance is insufficient for the order
insufficient_spot_assetNoSpot ask exceeds the configured base-asset balance
insufficient_sharesNoOutcome ask exceeds available YES/NO inventory; on a spot market this indicates a wrong client envelope
cross_shard_credit_kill_switchAfter operator recoveryCross-shard credit admission is disabled by the safety switch
cross_shard_account_cap_exceededAfter exposure changesAccount locked-credit cap would be exceeded
cross_shard_market_cap_exceededAfter exposure changesMarket locked-credit cap would be exceeded
cross_shard_credit_rejectedAfter state/policy changesOther cross-shard credit admission rejection
post_only_requires_passive_limit_orderNoPost-only requires a non-market passive limit order
post_only_would_crossNoPost-only order would have crossed
fok_not_filledNoFull requested quantity was not immediately available
reduce_only_qty_exceeds_positionNoReduce-only quantity exceeds the current same-outcome position
reduce_only_invalid_sideNoReduce-only is invalid for that side/action
invalid_stp_modeNoUnknown STP value; use cancel_maker, cancel_taker, reject, or skip_self
spot_notional_below_minimumNoSpot order is below the market minimum notional
price_must_be_positive · qty_must_be_positiveNoZero or negative price / quantity on a place or quote leg
price_not_aligned_to_tickNoPrice is not aligned to the market tick size
qty_not_aligned_to_lotNoQuantity is not aligned to the market lot size
qty_exceeds_maxNoQuantity exceeds the market maximum
fee_exceeds_notionalNoCalculated fee exceeds order notional
duplicate_order_idNoDeterministic order id already exists
idempotency_key_conflictNoThe same idempotency key was reused with a different payload
trading_haltedAfter readiness recoversGlobal trading safety gate is halted; stop placement and poll readiness
unauthorized_cancelNoCaller does not own or control the target order
order_not_openNoTarget order is already terminal
invalid_amendNoAmend request is invalid for the target order
account_market_open_order_cap_exceededAfter cancellationPer-account, per-market open-order cap reached
account_open_order_cap_exceededAfter cancellationPer-account open-order cap reached
price_level_order_cap_exceededAfter cancellationPer-price-level order cap reached
book_depth_exceededAfter book changesConfigured book-depth cap reached
self_trade_preventedNoSelf-trade-prevention triggered
order_not_foundNoCancel/amend target unknown or already terminal
invalid_signatureNoSignature verification failed
market_not_foundNoMarket id not recognized
expiredNoAction or order expiry passed
(no code yet) market N lifecycle=S is not accepting new ordersAfter the market reopensMarket status is not open. No stable code assigned yet. With the canonical HTTP contract on the JSON lanes it returns HTTP 503; on the BSL compact lane it is an HTTP 400 ingress reject. Stop quoting the market and re-read the catalog
(no code yet) market N expired for trading at TNoAction ts is at or after expiryTsMs, even while status still reads open. No stable code assigned yet (text matching classifies it as expired). Stop quoting the market
(no code yet) price above binary cap: PriceMicros(N)NoYES/NO limit price above 1000000 (1.000 USDC) on a binary or prediction market; 1000000 itself is accepted. Maps to engine_rejected
unknown_variant · unknown_field · missing_field · schema_violationNoStrict schema/shape rejection
rate_limitedYesHonor Retry-After when present; otherwise back off with jitter
backpressureYesIngress backlog limit hit; back off and retry
runtime_unavailableYesWrite runtime in recovery/unavailable

3. MM/BSL per-action rejections​

The order-entry batch surface reports per-action outcomes when you request the full result mode. In private beta, treat full as a provisioned/verified contract rather than the default happy path — the tested beta submit mode is x-bsl-result-mode: ack with x-senticore-response-mode: detailed.

full responses carry an actionResults array of HotpathOrderResult:

{
"ok": true,
"responseMode": "full",
"actionResults": [
{
"seq": 4811, "clientOrderId": "mm-quote-1", "status": "rejected",
"nonceConsumed": false,
"rejectCode": "POST_ONLY_WOULD_CROSS",
"rejectReason": "post-only order would cross the spread",
"filledQty": 0, "leavesQty": 0, "feeMicro": 0, "fills": [],
"engineTsMs": 1781190000123, "serverTsMs": 1781190000125
}
]
}
  • status ∈ filled, partially_filled, resting, canceled, applied, rejected. Only rejected entries carry rejectCode / rejectReason.
  • nonceConsumed is definitive in terminal actionResults. Missing/null is unknown and must not be interpreted as reusable.
  • rejectCode is a coarse engine label: NONCE_REJECTED, INSUFFICIENT_BALANCE, INSUFFICIENT_SPOT_ASSET, RISK_WOULD_EXCEED_AVAILABLE_USDC, ORDER_NOT_FOUND, POST_ONLY_WOULD_CROSS, SELF_TRADE_PREVENTED, INVALID_STP_MODE, QUEUE_LIMIT, KILL_SWITCH, ENGINE_REJECTED. rejectReason is a fixed public sentence for the cause (windowed-nonce rejects keep their nonceFloor= / nonceWindow= / nonceHoles= tokens); it does not carry the engine text. The engine text is in the receipt detail of GET /api/v1/bsl/receipts. The price cap, tick/lot and max-quantity rejects all arrive as ENGINE_REJECTED; no finer code is assigned yet.
Whole request at ingress, single action at apply

On the BSL compact lane, ingress checks (shape, signature, timestamp, single-account rule, market lifecycle and expiry, nonce window, rate limits, ingress risk pre-check) reject the whole HTTP request on the first failing action: ok: false, acceptedActions: 0, seqs: [], raw text in error, no code field, no receipt, and no nonce consumed. After admission each action is applied on its own: an action that fails in the engine (price cap, tick/lot, max quantity, post-only cross, insufficient USDC/base asset/YES-NO shares) is dropped with receipt status: "dropped", applied: false and the engine text in detail, while the other actions of the request are unaffected. A QuoteReplace is one action: if one leg fails, all its legs are dropped. The receipt carries no reject code; see Where a reject appears for the formats and the substrings to match.

Request-level status: a dropped action returns HTTP 409; if apply does not complete within the durable-ack timeout the request returns HTTP 503 with actionResults possibly null or partial.

See Raw Signed Actions for the full HotpathOrderResult schema.

4. Numeric reject reasons (BSL Direct TCP, FIX tag 9101, private WebSocket)​

The binary lanes and the private WebSocket carry one stable numeric taxonomy (senticore_protocol_internal::RejectReason). FIX 4.4 exposes the same number in SenticoreRejectReason(9101) next to the standard OrdRejReason(103)=99; BSL Direct TCP puts it in GatewayReject.reason; the private WebSocket reuses it in error.data.code where an order-entry reason applies.

CodeNameMeaningClient action
1InvalidMagicFrame magic / handshake bytes wrongFix the codec
2UnsupportedVersionProtocol version not acceptedNegotiate the advertised version
100BackpressureFullShard or ingress queue saturatedBack off with jitter; keep the session
101RateLimitExceededSession / account / key budget exhaustedPace against the budget view; keep the session
102IdempotencyReplaySame idempotency key inside the replay windowTreat as already answered; fetch state
103TradingHaltedVenue or market haltedStop placing; poll readiness; not load
104LedgerDegradedWrite admission closed pending ledger recoveryPause; wait for recovery; not load
105WriteLeaseFencedWriter lease changed under the socket; the socket is closedReconnect; never auto-replay the rejected frame
106DurabilityUnknownCommand may be durable but the boundary could not be provenReconcile by idempotency key / gateway seq; never blind-retry
108IpcTransportUnavailableThe gateway could not reach the sequencer over IPC (previously reported as 100 BackpressureFull)Back off with jitter; reconcile by idempotency key before resending
200AuthFailedCredential, signature or session identity invalidFix credentials; do not retry
201NonceReuseLegacy catch-all nonce reject; the detail text carries nonceFloor and nonceWindowResync the nonce cursor
202SessionViolationIdentity switch, non-monotonic gateway sequence, stale mapping — or, on production (re-checked 2026-09-13), a wallet-signed AuthSidecar V1 frame, which is still not appliedReconnect with a fresh session; refresh the connectivity bundle; for Direct TCP use an Ed25519 session key (AuthSidecarV2)
203NonceBelowFloorNonce below the replay-window floorRe-sign with an unused in-window nonce
204NonceSpentNonce inside the window but consumedRe-sign with a different unused nonce
205NonceAboveWindowNonce at or above the window edge (… above replay window); the reject carries usable nonceHolesTake a nonce from nonceHoles, or fence, then re-sign
300MarketUnknownMarket id or compact market index unknownRefresh exchange-info / connectivity bundle
301AccountUnknownAccount or compact account index unknownOnboard the account through BSL HTTP first

5. Rate-limit headers​

Every submit response carries rate-limit headers (see Rate Limits for the actual budgets):

  • X-RateLimit-Limit, X-RateLimit-Remaining
  • X-RateLimit-Reset-Ms — milliseconds until the window resets, a small number like 1000. It is not an absolute epoch timestamp.
  • Retry-After — whole seconds, emitted only on 429 / 503.
Not every error body is JSON

Three layers can answer before or instead of the API, and two of them do not send JSON:

SourceStatusBody
API rate limiter (per IP)429JSON error envelope with Retry-After, X-RateLimit-Reset-Ms, X-RateLimit-Source; the legacy x-ratelimit-after header can also be present. Older builds returned the plain text Too Many Requests! Wait for {n}s
Edge proxy request limiter (per IP)429The proxy's default HTML error page, not JSON
Cloudflare (in front of api.sentico-labs.xyz)403 and otherstext/plain such as error code: 1010

error code: 1010 means Cloudflare's bot protection refused the client signature. It was observed on 2026-09-23 for a GET /api/v1/health sent with Python urllib's default User-Agent; the same request with an explicit User-Agent returned 200. Send an explicit, descriptive User-Agent header from every client (for example acme-mm/1.4 ([email protected])).

Check Content-Type before parsing. Treat a non-JSON 403/429/5xx as a transport-level failure: do not burn a fresh nonce or signature on it, back off with jitter, and reconcile any mutating request by its receipt or idempotency key before resending. A Cloudflare 403 does not reach the API, so the action it carried was not admitted.

Retry semantics​

GET /api/v1/vaults remains public when both wallet and address are absent. Supplying either selector requires read credentials: missing credentials return 401, and another account or missing read scope returns 403. Empty selectors do not bypass authentication; with credentials, invalid or repeated selectors return 400. Omitting both selectors always requests the unfiltered catalogue. These responses carry Cache-Control: private, no-store.

  • Retry only retriable: true (envelope) / retriable submit codes.
  • Exponential backoff with jitter, starting ~250 ms, capping ~5 s.
  • Respect Retry-After on 429 / 503 when the server provides it. When it is absent, use capped exponential backoff with jitter; do not invent a one-second server hint.
  • Use idempotency keys so retries are safe (Idempotency).
  • Surface non-retriable errors to the strategy without automatic retry; never retry a nonce_below_floor or nonce_replayed — pick a fresh in-window nonce instead.

Getting help without exposing credentials​

For integration incidents, email the verified product support address [email protected]. Include:

  • UTC timestamp and environment/host;
  • HTTP method and path template, with wallet/account/order identifiers redacted if the ticket does not require them;
  • HTTP status, X-Request-Id and body requestId;
  • stable error code, retriable, and non-sensitive structured details;
  • whether the client saw no response, an admission acknowledgement, or a terminal receipt, plus the last receipt/event cursor;
  • the idempotency-key fingerprint or an internal correlation label, not the raw idempotency key when it contains business-sensitive data.

Never send API secrets, passphrases, private/session keys, seed phrases, raw Authorization or SC-* headers, signatures, or private request bodies. See Troubleshooting HTTP integrations for a sanitized diagnostic template.