Kalshi Errors - TundraSoft/tundra-connect GitHub Wiki
Kalshi Errors
Kalshi throws KalshiError for invalid configuration, local order
pre-flight validation, and vendor responses — see
errors/Base.ts and
errors/KalshiErrorCodes.ts.
import { KalshiError, KalshiErrorCodes } from '@tundraconnect/kalshi/errors';
const error = new KalshiError('NOT_FOUND');
console.log(error.message);
console.log(KalshiErrorCodes.NOT_FOUND);
Codes
Kalshi returns a machine-readable error envelope ({"error": {"code", "message", "details"?}}, occasionally just {"error": "..."}) — most of
these codes are keyed off the HTTP status, with detail carrying the
vendor's error.message (or the bare string) when present.
| Code | Meaning |
|---|---|
CONFIG_MISSING_PRIVATE_KEY |
A /portfolio/* method was called but auth/auth.accessKey/auth.privateKeyPem was never configured. |
CONFIG_INVALID_BATCH_SIZE |
cancelAllOrders({ batchSize }) was given a non-positive or fractional batch size. |
CONFIG_INVALID_PRIVATE_KEY |
auth.privateKeyPem isn't a structurally valid PKCS#8 PEM (missing BEGIN/END markers or invalid base64). A structurally-valid-but-not-actually-RSA key instead fails later, on the first authenticated call — see the API docs' "Configuration" section for why that can't be checked synchronously. |
ORDER_REJECTED |
submitOrder()/amendOrder(): a locally-refused (price/count out of range) or venue-rejected order. submitOrders(): a locally-refused order (checked before any request is sent) or a whole-batch rejection — a single row's rejection surfaces as rejected: true on that result instead, never a throw. detail carries the reason either way. |
INVALID_REQUEST |
A 400/422 response outside the order-submission paths above — detail carries the vendor's error text. |
NOT_FOUND |
A 404 response (getMarket()/getEvent()/getSeries() map this to null instead of throwing). |
AUTH_FAILED |
A 401 response — bad/expired credentials, or a signature that doesn't verify (see KalshiAuth.ts's doc comment for the quirks that cause this silently: mismatched timestamp, a query string left in the signed path, seconds instead of milliseconds). |
RATE_LIMITED |
A 429 response. |
RESPONSE_ERROR |
A success response's body failed schema validation. |
TIMEOUT |
No answer from Kalshi within the timeout (seconds); timeoutSeconds says which deadline passed. |
NETWORK_ERROR |
The request failed before any response (DNS, TLS or connection failure); the original error is the cause. |
SERVICE_UNAVAILABLE |
A 5xx response. |
UNKNOWN_ERROR |
An unmapped status was returned, or an unknown code was supplied. |
TIMEOUT, NETWORK_ERROR, RATE_LIMITED and SERVICE_UNAVAILABLE are
transient: they mean "no answer yet — try again later". err.transient
is true for exactly these and false for every other code (a definite
refusal, or a misconfiguration that retrying will not fix). The set is also
exported as KALSHI_TRANSIENT_CODES from @tundraconnect/kalshi/errors.
| Context | Meaning |
|---|---|
timeoutSeconds |
Set on TIMEOUT: the deadline, in seconds, that passed with no answer (the call's own timeout, else the client's timeout option). |
The resolved code is also available as a public, readonly error.code
property — branch on failure mode without matching against .message:
if (error instanceof KalshiError && error.code === 'ORDER_REJECTED') {
// ...
}
Use getContextValue() to read diagnostic metadata. This connect never
places auth.privateKeyPem or the KALSHI-ACCESS-SIGNATURE header value
into an error's context or message — _isSensitiveHeader redacts the
signature header the same way every other connect's vendor-specific auth
headers are redacted, so error.message/error.toJSON() are always safe
to log.
try {
await client.submitOrder({
ticker,
side: 'BUY',
price: 0.42,
count: 3,
orderType: 'GTC',
});
} catch (error) {
if (error instanceof KalshiError) {
console.log(error.code, error.getContextValue('detail'));
}
}
Backing off after a 429
A rate-limited request throws RATE_LIMITED. Two context values tell you what to
do next:
| Context | Meaning |
|---|---|
retryAfterSeconds |
Seconds the vendor asked you to wait, parsed by RESTler from Retry-After (delta seconds or an HTTP-date), X-RateLimit-Reset-After, RateLimit-Reset, or an epoch-seconds X-RateLimit-Reset. undefined when the response carried none — never a guess. |
retried |
Set only when maxRetryWait is configured: true if RESTler already waited once and was throttled again, false if it did not wait (the hint exceeded the cap, or there was no hint to wait on). |
Nothing is retried by default. Opt in with two client options (seconds,
each 0–120):
maxRetryWait— when a 429 carries a hint no longer than this, RESTler waits that long and retries once; otherwise it throwsRATE_LIMITEDimmediately.0disables the retry.defaultRetryWait— the wait used when a 429 carries no hint. Without it a hintless 429 is never retried: RESTler does not invent a delay. Only consulted whenmaxRetryWaitis set, and still capped by it.