Every service method follows one calling convention. Learn it once; all services read the same way: input object, optional options, typed result.
The call shape
client.<service>.<method>(input, options?): Promise<Result>input carries request fields. Account-scoped services always accept optional account ("main", "active", or { subaccountId }). Some reads take no input, so the argument is
optional.
options is last and controls the transport, not the payload. Reads use PolyesterRequestOptions. Mutations use PolyesterMutationOptions.
Result is a plain parsed object. Prices and quantities are decimal strings. Timestamps are
epoch milliseconds unless the field name ends in TsSec or TsNs. Enums are string literal
unions. Wire shapes do not leak through.
interface PolyesterRequestOptions {
signal?: AbortSignal; // cancel the request
}
interface PolyesterMutationOptions extends PolyesterRequestOptions {
stepUpToken?: string | null; // MFA fresh step-up proof (sent as X-Auth-Step-Up)
}Cancellation
Every method accepts an AbortSignal, same idea as fetch:
const controller = new AbortController();
const promise = client.marketOverview.list({}, { signal: controller.signal });
controller.abort();Aborts are not PolyesterErrors. Detect them with isAbortError(err) and treat them as flow
control:
import { isAbortError } from "@polyester/sdk";
try {
await promise;
} catch (err) {
if (isAbortError(err)) return; // caller cancelled
throw err;
}Stable mutation identifiers
Mutations use stable identifiers for different guarantees. A single-order clientOrderId is an
account-scoped duplicate guard, not a replay key. Reusing a retained value returns CONFLICT_DUPLICATE_CLIENT_ORDER_ID instead of the earlier result. Other request IDs can replay or
deduplicate the operation described by their endpoint:
| Mutation | Key field |
|---|---|
orders.create | clientOrderId (no replay) |
orders.batchCreate | batch requestId (replay); item clientOrderId is correlation only |
orders.modify, orders.cancelAll, order batch mutations | requestId |
Each deliberate orders.cancelAllAfter heartbeat | requestId |
triggers.create | clientTriggerId |
internalTransfers.create | idempotency key (built in) |
tradingWithdraws.create* | idempotency key (built in) |
The SDK generates order requestId values and clientTriggerId values when omitted, but it does
not synthesize clientOrderId. Create and persist a client order ID before the first attempt so an
uncertain outcome can be reconciled without inventing a second logical order:
const clientOrderId = crypto.randomUUID();
await client.catalog.ensureReady();
const symbolId = client.catalog.market.requireSymbolIdByPairSymbol("BTC-USDT");
await client.orders.create({
symbolId,
side: "buy",
qty: "0.01",
execution: { type: "limit_gtc", price: "60000" },
clientOrderId,
});
// If the response is uncertain, look up the order by clientOrderId before resubmitting.
// Reuse of a retained clientOrderId returns CONFLICT_DUPLICATE_CLIENT_ORDER_ID.clientOrderId while the first create is unresolved can place a
second order. Persist the original value for reconciliation. A duplicate conflict does not replay
the original outcome.A batch timeout is an unknown outcome, not proof that nothing committed. Give every batch-create
item a stable clientOrderId, reconcile every item, then retry the unchanged batch with the same requestId. For cancelAllAfter, create a new ID for each deliberate heartbeat and reuse it only
for an ambiguous retry of that heartbeat.
Retrying safely
The error tree splits on retryability.
TransientError (network, timeout, rate limit, unavailable) may have failed before or after the
backend applied a mutation. Do not automatically retry a single-order create. Reconcile by clientOrderId; a retained ID returns a duplicate conflict instead of replaying the result. For
mutations with replayable request IDs, keep the key constant.
RequestError is permanent. An identical retry fails the same way. Fix the input, auth, or
state instead.
const clientOrderId = crypto.randomUUID();
try {
await client.orders.create({
symbolId,
side: "buy",
qty: "0.001",
execution: { type: "limit_gtc", price: "10000", postOnly: true },
clientOrderId,
});
} catch (err) {
const details = await client.orders.getDetails({
clientOrderId,
includeExecutionHistory: false,
});
if (details) console.log("create already accepted", details.order.orderId);
else throw err; // decide whether and when a new logical order is appropriate
}Use a retry helper only for an operation whose requestId contract permits replay, such as orders.cancelAll. Keep its request ID unchanged for the same logical mutation.
RateLimitError may carry retryAfterMs when the backend suggests a wait. See Error handling and the Errors reference.
Step-up on protected mutations
A mutation can require a fresh multi-factor proof. On StepUpRequiredError, complete an MFA challenge and retry with options.stepUpToken:
import { StepUpRequiredError } from "@polyester/sdk";
try {
await client.apiKeys.create(payload);
} catch (err) {
if (!(err instanceof StepUpRequiredError)) throw err;
const challenge = await client.mfa.beginChallenge({ purpose: "freshStepUp" });
const { stepUpToken } = await client.mfa.verifyTotpChallenge({
challengeId: challenge.challengeId,
code: userEnteredCode,
});
await client.apiKeys.create(payload, { stepUpToken });
}The step-up token is one-use and bound to a single request. See the authentication model for session elevation vs fresh step-up, and MFA for challenge methods.