Error Model
Senticore supports a common error envelope while existing integrations retain their legacy response fields during migration.
- The HTTP error envelope — for request-level failures (bad shape, auth, not found, rate
limit). A
{ "ok": false, "error": { … } }object. - The
SubmitResponse— forPOST /api/v1/trading/actions(and MM/BSL order entry). Even a rejected order returns a flatSubmitResponsewith a top-levelerrorstring and a derivedcode. 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.
| Condition | Canonical HTTP status | Client action |
|---|---|---|
| Validation / insufficient trading balance | 400 | Correct the action or fund the account, then re-sign as needed. |
| Invalid or expired authentication | 401 | Refresh credentials and sign a fresh request. |
| Permission failure | 403 | Use an authorized credential/account. |
| Unknown market, order or resource | 404 | Refresh identifiers and reconcile existing orders. |
| Idempotency / nonce conflict | 409 | Reconcile the relevant key or nonce before retrying. |
| Halted market / unavailable engine | 503 | Check readiness. Honor Retry-After when present; otherwise back off with jitter. Reconcile mutating requests before resending. |
| Rate limit | 429 | Honor 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:
| Observation | What it establishes | Next action |
|---|---|---|
| No HTTP response / client timeout | Delivery or admission is unknown | Keep the exact signed bytes and idempotency key. Query receipt, order, and private execution state before resending. |
HTTP 200 with accepted: true | Ingress accepted the action | Wait for a terminal receipt, fill, cancel, or rejection. Do not treat the acknowledgement as a fill. |
| HTTP 409 nonce/idempotency conflict | The submitted identity cannot be reused blindly | Read the nonce window or replay the matching idempotency request; change and re-sign only when the response requires it. |
| HTTP 429 | A limiter rejected this attempt | Honor a real delay/budget hint when present; otherwise use capped exponential backoff with jitter. Preserve order identity. |
| HTTP 503 | The service could not prove a safe result or accept new work | Check readiness and reconcile the request. Honor Retry-After when present; it is not guaranteed. |
| Terminal business rejection | The action will not execute as submitted | Follow 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
retriabletogether with the code and details. A conflict may require a changed nonce/signature or reconciliation; retrying unchanged bytes blindly cannot repair it. requestIdis always present — quote it in support requests.detailsis omitted when there's nothing structured to add;details.causecarries 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 status | error.code | Meaning |
|---|---|---|
| 400 / 422 | BAD_REQUEST | Malformed request or strict schema/shape rejection |
| 401 | UNAUTHORIZED | Authentication required or signature invalid |
| 403 | FORBIDDEN | Authenticated but not authorized for this action |
| 404 | NOT_FOUND | Resource/route unknown |
| 409 | CONFLICT | Idempotency or state conflict (retriable) |
| 429 | RATE_LIMITED | Rate limit exceeded (retriable) |
| 406 | NOT_ACCEPTABLE | Content negotiation failed |
| 503 | SERVICE_UNAVAILABLE | Temporarily unavailable (retriable) |
| 500 | INTERNAL_ERROR / SERVER_ERROR | Internal error (retriable) |
Legacy submit responses expose business reasons as SubmitResponse.code
(§2). With envelope-v1, read the equivalent
error.code and error.details.
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
walletselects the verified account. Bothwalletand itsaddresscompatibility 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.
| Code | HTTP | Meaning |
|---|---|---|
APP_KEY_INVALID | 401 | The app key is unknown or revoked; the request does not fall back to anonymous |
APP_NOT_ACTIVE | 403 | The app is pending or suspended |
APP_CHANNEL_MISMATCH | 403 | Origin and key identify different apps |
APP_ATTRIBUTION_REJECTED | 403 | Payload appId/appFee does not match the channel's app, or the surcharge payout is not the registered payout account |
APP_LIMIT_EXCEEDED | reject | Per-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:
| Code | HTTP | Meaning |
|---|---|---|
AGENT_NOT_FOUND | 404 | Agent id does not exist |
AGENT_REVOKED / AGENT_EXPIRED | 403 | Agent lifecycle no longer permits use |
INVALID_SCOPE | 403 | Required independent scope is missing |
AGENT_POLICY_REJECTED | 403 | Account, market, IP, or notional policy failed |
AGENT_POLICY_RATE_LIMITED | 429 | Per-agent policy budget exhausted |
API_CREDENTIAL_REQUIRED | 401 | Machine agent request omitted its credential |
API_CREDENTIAL_INVALID | 401 | Credential/signature material is invalid |
API_CREDENTIAL_REVOKED | 403 | Credential was revoked or its overlap ended |
ACCOUNT_MISMATCH | 403 | Agent/credential is bound to another trading account |
INVALID_SIGNATURE | 400 | Delegated payload signature is invalid |
DUPLICATE_NONCE | 409 | Delegated nonce was already consumed |
MACHINE_AUTH_REPLAY | 409 | Credential/timestamp/SC-Nonce tuple was already used; allocate a fresh tuple and recompute transport authentication |
PUBLIC_WALLET_FILTER_UNSUPPORTED | 400 | Public trade history is anonymous; authenticate and use account-scoped fills |
STORE_READ_FAILED | 500 | Account/control-plane read failed; retain requestId, back off and retry the read |
NONCE_EXHAUSTED | 429 | Delegated nonce allocation or policy budget is exhausted |
TRADING_ACCOUNT_CLOSING | 403 | Closing/archived account attempted a non-reducing action |
AGENT_AUTH_FAILED | 403 | This 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_HALTED | reject | Trading 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.
| Code | Retriable | Meaning |
|---|---|---|
nonce_below_floor | No (pick fresh) | Below the window floor — permanently consumed/stale |
nonce_outside_window | After floor advances | Too far ahead; fill lower nonces first |
nonce_replayed | No (pick fresh) | In-window but already used/in-flight |
nonce_mismatch | After resync | Uncategorized nonce reject; use nextUsableNonce |
insufficient_balance | No | Balance below order requirement |
risk_would_exceed_available_usdc | No | Projected aggregate USDC requirement exceeds available free USDC |
insufficient_usdc | No | USDC balance is insufficient for the order |
insufficient_spot_asset | No | Spot ask exceeds the configured base-asset balance |
insufficient_shares | No | Outcome ask exceeds available YES/NO inventory; on a spot market this indicates a wrong client envelope |
cross_shard_credit_kill_switch | After operator recovery | Cross-shard credit admission is disabled by the safety switch |
cross_shard_account_cap_exceeded | After exposure changes | Account locked-credit cap would be exceeded |
cross_shard_market_cap_exceeded | After exposure changes | Market locked-credit cap would be exceeded |
cross_shard_credit_rejected | After state/policy changes | Other cross-shard credit admission rejection |
post_only_requires_passive_limit_order | No | Post-only requires a non-market passive limit order |
post_only_would_cross | No | Post-only order would have crossed |
fok_not_filled | No | Full requested quantity was not immediately available |
reduce_only_qty_exceeds_position | No | Reduce-only quantity exceeds the current same-outcome position |
reduce_only_invalid_side | No | Reduce-only is invalid for that side/action |
invalid_stp_mode | No | Unknown STP value; use cancel_maker, cancel_taker, reject, or skip_self |
spot_notional_below_minimum | No | Spot order is below the market minimum notional |
price_must_be_positive · qty_must_be_positive | No | Zero or negative price / quantity on a place or quote leg |
price_not_aligned_to_tick | No | Price is not aligned to the market tick size |
qty_not_aligned_to_lot | No | Quantity is not aligned to the market lot size |
qty_exceeds_max | No | Quantity exceeds the market maximum |
fee_exceeds_notional | No | Calculated fee exceeds order notional |
duplicate_order_id | No | Deterministic order id already exists |
idempotency_key_conflict | No | The same idempotency key was reused with a different payload |
trading_halted | After readiness recovers | Global trading safety gate is halted; stop placement and poll readiness |
unauthorized_cancel | No | Caller does not own or control the target order |
order_not_open | No | Target order is already terminal |
invalid_amend | No | Amend request is invalid for the target order |
account_market_open_order_cap_exceeded | After cancellation | Per-account, per-market open-order cap reached |
account_open_order_cap_exceeded | After cancellation | Per-account open-order cap reached |
price_level_order_cap_exceeded | After cancellation | Per-price-level order cap reached |
book_depth_exceeded | After book changes | Configured book-depth cap reached |
self_trade_prevented | No | Self-trade-prevention triggered |
order_not_found | No | Cancel/amend target unknown or already terminal |
invalid_signature | No | Signature verification failed |
market_not_found | No | Market id not recognized |
expired | No | Action or order expiry passed |
(no code yet) market N lifecycle=S is not accepting new orders | After the market reopens | Market 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 T | No | Action 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) | No | YES/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_violation | No | Strict schema/shape rejection |
rate_limited | Yes | Honor Retry-After when present; otherwise back off with jitter |
backpressure | Yes | Ingress backlog limit hit; back off and retry |
runtime_unavailable | Yes | Write 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. Onlyrejectedentries carryrejectCode/rejectReason.nonceConsumedis definitive in terminalactionResults. Missing/null is unknown and must not be interpreted as reusable.rejectCodeis 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.rejectReasonis a fixed public sentence for the cause (windowed-nonce rejects keep theirnonceFloor=/nonceWindow=/nonceHoles=tokens); it does not carry the engine text. The engine text is in the receiptdetailofGET /api/v1/bsl/receipts. The price cap, tick/lot and max-quantity rejects all arrive asENGINE_REJECTED; no finer code is assigned yet.
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.
| Code | Name | Meaning | Client action |
|---|---|---|---|
| 1 | InvalidMagic | Frame magic / handshake bytes wrong | Fix the codec |
| 2 | UnsupportedVersion | Protocol version not accepted | Negotiate the advertised version |
| 100 | BackpressureFull | Shard or ingress queue saturated | Back off with jitter; keep the session |
| 101 | RateLimitExceeded | Session / account / key budget exhausted | Pace against the budget view; keep the session |
| 102 | IdempotencyReplay | Same idempotency key inside the replay window | Treat as already answered; fetch state |
| 103 | TradingHalted | Venue or market halted | Stop placing; poll readiness; not load |
| 104 | LedgerDegraded | Write admission closed pending ledger recovery | Pause; wait for recovery; not load |
| 105 | WriteLeaseFenced | Writer lease changed under the socket; the socket is closed | Reconnect; never auto-replay the rejected frame |
| 106 | DurabilityUnknown | Command may be durable but the boundary could not be proven | Reconcile by idempotency key / gateway seq; never blind-retry |
| 108 | IpcTransportUnavailable | The gateway could not reach the sequencer over IPC (previously reported as 100 BackpressureFull) | Back off with jitter; reconcile by idempotency key before resending |
| 200 | AuthFailed | Credential, signature or session identity invalid | Fix credentials; do not retry |
| 201 | NonceReuse | Legacy catch-all nonce reject; the detail text carries nonceFloor and nonceWindow | Resync the nonce cursor |
| 202 | SessionViolation | Identity 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 applied | Reconnect with a fresh session; refresh the connectivity bundle; for Direct TCP use an Ed25519 session key (AuthSidecarV2) |
| 203 | NonceBelowFloor | Nonce below the replay-window floor | Re-sign with an unused in-window nonce |
| 204 | NonceSpent | Nonce inside the window but consumed | Re-sign with a different unused nonce |
| 205 | NonceAboveWindow | Nonce at or above the window edge (… above replay window); the reject carries usable nonceHoles | Take a nonce from nonceHoles, or fence, then re-sign |
| 300 | MarketUnknown | Market id or compact market index unknown | Refresh exchange-info / connectivity bundle |
| 301 | AccountUnknown | Account or compact account index unknown | Onboard 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-RemainingX-RateLimit-Reset-Ms— milliseconds until the window resets, a small number like1000. It is not an absolute epoch timestamp.Retry-After— whole seconds, emitted only on429/503.
Three layers can answer before or instead of the API, and two of them do not send JSON:
| Source | Status | Body |
|---|---|---|
| API rate limiter (per IP) | 429 | JSON 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) | 429 | The proxy's default HTML error page, not JSON |
Cloudflare (in front of api.sentico-labs.xyz) | 403 and others | text/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-Afteron429/503when 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_floorornonce_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-Idand bodyrequestId; - stable error
code,retriable, and non-sensitive structureddetails; - 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.