Skip to main content

Errors, Retries and Rate Limits

Last updated November 4, 2026

Every error code, which ones to retry, and the three headers that keep a retry safe: Retry-After, Idempotency-Key and If-Match.

Errors

Every error comes in the envelope with its code in error.code. Codes are stable; messages can change.

HTTPCodeMeaning and what to do
400bad_requestThe request could not be read: malformed JSON, a wrong content type, a body that is not an object, a bad parameter or cursor, or an invalid path. Fix the request.
401invalid_tokenThe access token is missing, expired or revoked. Get a new token and retry.
403permission_deniedThe client lacks the permission error.data.permission names. Give it on Connected Apps, or do not send the request.
403entitlement_requiredYour plan does not include this part of the API. error.data.product is bots or backtick; error.data.access write means the plan reads bots but cannot change them; a reason of sweeptick_run means it does not run SweepTicks. Nothing in the program fixes this.
403constraint_violationThe target is outside the client's limit to certain accounts or to the bots it created. Change the limits, or choose another target.
403subscription_limitTurning the bot on, or creating or changing a Multi-Entry bot, would use more bots than your plan allows. Turn another bot off, use fewer entries or change the plan.
403account_disabledThe bot's broker account is not enabled for trading. Enable it on the website.
403step_up_requiredThe action needs a fresh sign-in check. Finish it on the website.
404not_foundNothing with that id is in your account, or the client may not see it. Check the id.
405method_not_allowedNot a method this path serves; the Allow header names them.
409idempotency_conflictThe Idempotency-Key was used with a different request. Use a new key.
409idempotency_in_progressThe first request with this key is still running, or ended without a known result; see Idempotency keys.
409not_editableThe Backtick is queued, running or completed. Clone it and edit the copy.
409pause_lockedAn emergency pause, a halt or a sitewide pause is active. Resume trading on the website once the lock is lifted.
409bot_changed_since_requestThe bot changed after the request was made, so the approval was not carried out. Read the bot and send the request again.
409pending_action_existsA request for this bot (or to resume trading) already waits for your approval. Poll the action error.data.actionId names only if it is this client's own (another's answers 404); otherwise retry later.
409bot_archivedRestore the bot on the website before changing it.
409family_live_edit_unsupportedThe Multi-Entry bot is live: on, switched off in the last ten minutes, or with an entry that still has a trade or an order working. Switch it off and wait until its entries are idle, or edit it on the website.
409backtick_quotaA Backtick limit was reached: positions, queued work or run rate, as error.data.reason says. Wait or reduce the Backtick.
409invalid_stateThe Backtick's state does not allow this: in the trash, already run, not queued, scheduled to run, or its positions generated by a SweepTick. error.data.reason says which, such as scheduled or sweep_managed.
410action_expiredThe request waiting for approval expired or was already decided. Send a new one if it is still wanted.
412version_mismatchThe If-Match value is out of date; see Versions.
422validation_failedSettings are not valid; error.fieldErrors lists each. POST /bots/validate checks settings without saving.
422family_edit_unsupportedAn entry of a Multi-Entry bot is edited through its parent bot (error.data.parentBotId, when the client may see it), a Multi-Entry bot cannot be turned on through the API, or a batch of Backtick changes named a Multi-Entry position. Send it to the right target, or use the website.
428idempotency_key_requiredAdd an Idempotency-Key header and send it again.
428precondition_requiredRead the target, then send its version in an If-Match header.
429rate_limitedToo many requests, too many running at once, too many waiting for approval, or too many scheduled runs. Retry after Retry-After. error.data.reason names the limit when it is not a plain request count.
503api_disabledThe API is temporarily switched off. Retry later.
503writes_disabledChanges are temporarily switched off; reads, disable and pause still work. Retry the change later.
503service_unavailableA temporary problem on our side. Retry shortly, after Retry-After when given. On a Backtick change, outcome says what happened: saved, unchanged or unknown.
503lock_busyAnother change to the same bot, to the trading switch, or to the account's Backtick runs (a run, cancel, trash or permanent delete) is in progress, or the run you are cancelling is starting. Nothing was done. Retry after Retry-After (2 seconds, or 5 for a starting run).
500internal_errorSomething went wrong on our side. Retry once; if it repeats, contact support with the requestId.

What to retry

  • Retry after waiting: rate_limited, lock_busy, service_unavailable, idempotency_in_progress while stale is false, and internal_error once. Keep the same Idempotency-Key on a retry of the same request.
  • Read first, then decide: version_mismatch, bot_changed_since_request, and idempotency_in_progress once stale is true.
  • Never retry as it is: every 400, 403, 404, 405, 422 and 428 answer. Fix the request or the setup.

Rate limits

The API limits how fast one account, and one access token, can send requests. The limits differ by kind of request, so build your program to read the headers and wait when told to, not around a number.

  • Every answer to a request whose token was accepted carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (the Unix time, in seconds, at which the current window ends). When two limits apply, the headers show the tighter one. A refusal for the token itself (401 invalid_token, the 429 after repeated failed authentications, or 403 entitlement_required) carries none.
  • For changes, X-RateLimit-Remaining counts units of work, not calls: a batch of changes or a run of several Backticks spends more than one.
  • A refusal is 429 rate_limited with Retry-After, the seconds to wait. Wait that long, then retry the same request, never sooner. Treat the Retry-After of 503 lock_busy and of some 503 service_unavailable answers the same way.

When the limiter itself is unavailable, a change is not made: the answer is 503 service_unavailable with Retry-After. Reads, disabling a bot and pausing trading still go through. The token endpoint has limits of its own; see Authentication.

Idempotency keys

A request that creates, runs or starts something needs an Idempotency-Key header, so a retry after a network error never runs it twice:

  • The key is a value you choose, unique per request: 1 to 128 characters from letters, digits, ., _ and -. A random UUID is a good key. Without one the request answers 428 idempotency_key_required and does nothing.
  • The same key with the same request, from the same client, returns the first answer again and does nothing twice. A replayed answer carries Idempotent-Replayed: true and Idempotent-Original-Request-At, the UTC time of the first request.
  • The same key with a different request of the same kind (another bot, another body or another If-Match value) answers 409 idempotency_conflict. Keys are kept for 2 days, per kind of request, so the same key sent to another endpoint is a new request; use a fresh key every time anyway.
  • A request refused before anything was saved frees its key, so it can be sent again with the same key. Once a change has begun saving, the key keeps whatever answer it ends with, a service_unavailable answer included, and a retry returns that answer.
  • A request interrupted while saving has no answer to keep. A retry answers 409 idempotency_in_progress: for the first two minutes as a request still running, with Retry-After: 1 and error.data.stale false; after that as one that did not finish, with stale true. Read the bot or the Backtick to see what happened, then send a new request with a new key.

The requests that need the key: POST /bots, PATCH /bots/{botId}, POST /bots/{botId}/enable, POST /trading/resume, POST /backtests, POST /backtests/from-bots, POST /backtests/{uid}/clone, POST /backtests/compose, POST /backtests/sweeps, PATCH /backtests/{uid}/sweep, PATCH /backtests/{uid}, POST /backtests/{uid}/changes, POST /backtests/{uid}/positions, PATCH /backtests/{uid}/positions/{positionId}, DELETE /backtests/{uid}/positions/{positionId}, POST /backtests/{uid}/run, POST /backtests/run, POST /backtests/{uid}/positions/{positionId}/to-bot and POST /backtests/live-history. Every other request ignores the header and always runs: the reads, POST /bots/validate, disabling a bot, pausing trading, cancelling a run, and filing, trashing, restoring or permanently deleting a Backtick. Chart links and portfolio links take no key either: a second request makes a second link.

POST /rest/v1/bots HTTP/1.1
Host: api.tradesteward.com
Authorization: Bearer tsa_9f1c...
Content-Type: application/json
Idempotency-Key: create-spxic-20261104-01

{ "accountNum": "12344821", "stratCode": "SPXIC", "settings": { "...": "..." } }

accountNum is the full account number as GET /accounts returns it.

Versions and the If-Match header

Every bot carries a version and every Backtick an editVersion. An edit names the version it was based on, so two programs (or you on the website and your program) never overwrite each other:

  • Send the value you last read in the If-Match header: If-Match: 17. One pair of surrounding double quotes, the HTTP entity-tag form, is accepted too.
  • If the target changed in between, nothing is saved: the edit answers 412 version_mismatch with the version it is at now in error.data.current. Read the target again, decide whether your change still makes sense, and send it with the new version.
  • A missing header answers 428 precondition_required. If-Match is checked before the Idempotency-Key, so a stale edit never claims a key.
  • A successful edit answers with the new version. An edit that asks for nothing new answers changed: false, and the version does not move.

The requests that need If-Match: PATCH /bots/{botId}, PATCH /backtests/{uid}, PATCH /backtests/{uid}/sweep, POST /backtests/{uid}/positions, PATCH /backtests/{uid}/positions/{positionId}, DELETE /backtests/{uid}/positions/{positionId} and POST /backtests/{uid}/changes. A Backtick position has no version of its own: its Backtick's editVersion covers it, and a Multi-Entry bot's version covers its entries.

Stopping always works

POST /bots/{botId}/disable and POST /trading/pause have rate limits of their own and stay available while changes are switched off (writes_disabled) or while a client has used up its reads or changes. They stop new trades from being opened. The trading engine can take a short time to see a pause, so a trade that was about to open may still open. An order already at the broker may still fill, and open trades keep being managed until they close.