Errors, Retries and Rate Limits
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.
| HTTP | Code | Meaning and what to do |
|---|---|---|
| 400 | bad_request | The 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. |
| 401 | invalid_token | The access token is missing, expired or revoked. Get a new token and retry. |
| 403 | permission_denied | The client lacks the permission error.data.permission names. Give it on Connected Apps, or do not send the request. |
| 403 | entitlement_required | Your 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. |
| 403 | constraint_violation | The target is outside the client's limit to certain accounts or to the bots it created. Change the limits, or choose another target. |
| 403 | subscription_limit | Turning 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. |
| 403 | account_disabled | The bot's broker account is not enabled for trading. Enable it on the website. |
| 403 | step_up_required | The action needs a fresh sign-in check. Finish it on the website. |
| 404 | not_found | Nothing with that id is in your account, or the client may not see it. Check the id. |
| 405 | method_not_allowed | Not a method this path serves; the Allow header names them. |
| 409 | idempotency_conflict | The Idempotency-Key was used with a different request. Use a new key. |
| 409 | idempotency_in_progress | The first request with this key is still running, or ended without a known result; see Idempotency keys. |
| 409 | not_editable | The Backtick is queued, running or completed. Clone it and edit the copy. |
| 409 | pause_locked | An emergency pause, a halt or a sitewide pause is active. Resume trading on the website once the lock is lifted. |
| 409 | bot_changed_since_request | The bot changed after the request was made, so the approval was not carried out. Read the bot and send the request again. |
| 409 | pending_action_exists | A 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. |
| 409 | bot_archived | Restore the bot on the website before changing it. |
| 409 | family_live_edit_unsupported | The 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. |
| 409 | backtick_quota | A Backtick limit was reached: positions, queued work or run rate, as error.data.reason says. Wait or reduce the Backtick. |
| 409 | invalid_state | The 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. |
| 410 | action_expired | The request waiting for approval expired or was already decided. Send a new one if it is still wanted. |
| 412 | version_mismatch | The If-Match value is out of date; see Versions. |
| 422 | validation_failed | Settings are not valid; error.fieldErrors lists each. POST /bots/validate checks settings without saving. |
| 422 | family_edit_unsupported | An 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. |
| 428 | idempotency_key_required | Add an Idempotency-Key header and send it again. |
| 428 | precondition_required | Read the target, then send its version in an If-Match header. |
| 429 | rate_limited | Too 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. |
| 503 | api_disabled | The API is temporarily switched off. Retry later. |
| 503 | writes_disabled | Changes are temporarily switched off; reads, disable and pause still work. Retry the change later. |
| 503 | service_unavailable | A temporary problem on our side. Retry shortly, after Retry-After when given. On a Backtick change, outcome says what happened: saved, unchanged or unknown. |
| 503 | lock_busy | Another 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). |
| 500 | internal_error | Something 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_progresswhilestaleis false, andinternal_erroronce. Keep the sameIdempotency-Keyon a retry of the same request. - Read first, then decide:
version_mismatch,bot_changed_since_request, andidempotency_in_progressoncestaleis true. - Never retry as it is: every
400,403,404,405,422and428answer. 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-RemainingandX-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, the429after repeated failed authentications, or403 entitlement_required) carries none. - For changes,
X-RateLimit-Remainingcounts units of work, not calls: a batch of changes or a run of several Backticks spends more than one. - A refusal is
429 rate_limitedwithRetry-After, the seconds to wait. Wait that long, then retry the same request, never sooner. Treat theRetry-Afterof503 lock_busyand of some503 service_unavailableanswers 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 answers428 idempotency_key_requiredand 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: trueandIdempotent-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-Matchvalue) answers409 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_unavailableanswer 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, withRetry-After: 1anderror.data.stalefalse; after that as one that did not finish, withstaletrue. 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-Matchheader: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_mismatchwith the version it is at now inerror.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-Matchis checked before theIdempotency-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.