Docs

Errors

Every error code, what causes it, and whether to retry.

Every failure has the same shape:

{
  "error": {
    "code": "ACCOUNT_EXECUTING",
    "message": "An order is in flight. Stopping now would leave its outcome unknown.",
    "details": []
  },
  "requestId": "3f8a1c92-7b4e-4d81-a2f6-0c5e9b3d7a14"
}

Match on code. It is stable across releases. message is written for a human and will change. details appears on validation failures and names the fields; on a failed order, details[0] carries its orderId, state and retryable.

requestId is on every error response, and is recorded against every audit row. Quote it in a support request and the entire call can be found.

The codes

Authentication and authorization

CodeStatusCauseRetry?
UNAUTHENTICATED401Missing, malformed, unknown, revoked or expired key — deliberately one code for all of them (one exception: an admin key past its expiry gets ADMIN_KEY_EXPIRED)No. Fix the key.
MISSING_SCOPE403Valid key, wrong scope. Names the scope it wants.No. Issue a key with the scope.
KEY_LIMIT_EXCEEDED403The key's limits do not allow this opening order: too large, a symbol or account it may not trade, or its daily allowance is used. details[0] has limit, requested, allowed and, for one account of a multi-account order, accountId. Nothing was sent. Also answered when a key with limits tries to change, rotate or re-enable a TradingView hook it did not create; then details is empty and nothing changed. Not KEY_LIMIT (409), which is about how many keys a workspace has.No. Change the order, use another key, or change the key's limits.

One code for every authentication failure is intentional: distinguishing "unknown key" from "wrong secret" lets someone with a list of candidate keys sort the real from the fake. See Authentication.

MCP sign-in

/mcp and /mcp/insights answer a connected app's request with a WWW-Authenticate challenge rather than one of the codes above:

AnswerMeaning
401 with WWW-Authenticate: Bearer resource_metadata="…"No credential was sent. Sign in.
401 with error="invalid_token"The sign-in is expired or revoked, or the token is for the other server. Sign in again.
403 with error="insufficient_scope"Signed in, but not allowed to trade. Reconnect; the sign-in page opens with Trading preselected.

See MCP server for what triggers each one.

Requests

CodeStatusCauseRetry?
INVALID_REQUEST400Body or parameters failed validation (details names the fields), the body is not valid JSON, or — on an order — its stops are on the wrong side or a partial close would leave less than the symbol's minimum. Also answered with 413 for a body too large and 415 for the wrong content type.No. Fix the request.
NOT_FOUND404No such endpoint — or no such API key, member, invitation, TradingView hook or invoice for your workspace.No.
ACCOUNT_NOT_FOUND404No such account for your tenantNo.
POSITION_NOT_FOUND404No such open position on that account — usually it is already closedNo. Read the account's positions.
ORDER_NOT_FOUND404No such order for your tenantNo.
WAVE_NOT_FOUND404No such multi-account order for your tenantNo.
RATE_LIMITED429Too many requests of one class. The retry-after header says how many seconds to wait, and x-ratelimit-class names the class that was hit. See Rate limits.Yes, after retry-after.

ACCOUNT_NOT_FOUND is also what you get for an account that exists but belongs to somebody else. Returning 403 would confirm that a guessed id is real. POST /v1/accounts/{id}/password, POST /v1/accounts/{id}/server and DELETE /v1/accounts/{id} answer a missing account with NOT_FOUND rather than ACCOUNT_NOT_FOUND; treat the two the same.

Rate limits

Limits are per workspace and per class of work, so polling never spends the budget your orders need. Each is a steady rate per minute plus a short burst.

ClassWhat it coversLimit
TradingOrders, closes, stop changes, cancelsThe larger of 60 a minute and twice the plan's account allowance, with a burst of the larger of 10 and the account allowance: for example Production 500 a minute + 250, Business 2,000 + 1,000
Multi-account ordersCreating and cancelling multi-account orders10 a minute + 2
ReadsEvery GET600 a minute + 100
Live readsGET …/quotes and GET …/figures?fresh=true, charged only when the account's terminal is actually asked (shared and cached answers are free). Per account, across every caller and key, besides counting as reads. A refusal carries x-ratelimit-class: live.300 a minute + 50
Other writesEverything else120 a minute + 20

Every response names its class in x-ratelimit-class and the ceiling in x-ratelimit-limit. Console sessions have buckets of their own.

Accounts

CodeStatusCauseRetry?
ACCOUNT_EXISTS409That login + server is already connected in your workspaceNo. Use the existing account — its id is in details. To change its password, POST /v1/accounts/{id}/password.
SECRET_STORE_UNAVAILABLE503The credential could not be stored, so the account was not createdYes, with backoff.
ACCOUNT_EXECUTING409On POST /v1/accounts/{id}/password or DELETE /v1/accounts/{id}: an order is in flight, so nothing was changedYes, once the order settles.
ACCOUNT_HAS_POSITIONS409On DELETE /v1/accounts/{id}: our last look found open positions. details has how many.No. Close them first, or delete with force=true — they stay open at the broker, but can no longer be shown or closed here.
NO_PASSWORD409On POST /v1/accounts/{id}/server: no password is stored for this account (it was disconnected)No. Send the password with the server to /password.
DEMO_ONLY402On a trial: a live account is not connected — the trial is for demo accountsNo. Choose a plan to connect live accounts.
TRIAL_ACCOUNT_USED402On a trial: that demo login was already used in another trialNo. Use another demo account, or choose a plan.
ACCOUNT_CHANGED409On POST /v1/accounts/{id}/password: the account was disconnected, or its password replaced, while the request ran. Nothing was changed.Yes: read the account, then reconnect or replace again.
ACCOUNT_IN_WAVE409On DELETE /v1/accounts/{id}: a multi-account order that includes this account has not finished. details has its id.Yes, once it settles — or cancel it.

Trading

CodeStatusCauseRetry?
SEND_FAILED503Proven never to have reached the brokerYes. The only safe resend.
ORDER_REJECTED422The broker refused it, with a reason and its own retcodeOnly if retryable is true
ORDER_UNRESOLVED503We do not know whether it executedNever. Poll the order.
IDEMPOTENCY_KEY_REUSED409That key was used for a different orderNo. Use a new key.
IDEMPOTENCY_IN_FLIGHT409The first request with that key is still runningYes, shortly.
TRADING_DISABLED409Trading is switched off for the account, or for the whole workspace by its kill switch — closes includedNo.
ACCOUNT_NOT_READY409The account is not ready: offline in cold mode, still connecting, in a needs-attention state, or being brought online by another request — and, on /quotes, not online (ready, executing or degraded); on /figures?fresh=true, not readyPoll its status; retry once it is ready. Not if it needs attention.
NO_RUNTIME409The account reported online but is not reachable — or, on /calculate, it is offline, or bringing it online for an order failedRestart the account, or bring it online.
MT5_UNAVAILABLE503On /calculate, /quotes or /figures?fresh=true: the account's terminal did not answerYes, shortly.
CAPABILITY_UNAVAILABLE501On /quotes or /figures?fresh=true: the account's terminal is from before that feature. It gains it at its next update.Not now. Meanwhile use the positions' currentPrice, or /figures without fresh.

422 rather than 400 for a rejection: the request was well formed and we understood it. The broker declined it, which is a different problem with a different fix.

ORDER_UNRESOLVED is the one to handle deliberately. See Trading and history.

Plans

CodeStatusCauseRetry?
QUOTA_EXCEEDED402Opening this would pass the plan's limit. On paid plans, orders beyond the allowance are billed per order up to twice the allowance, then new opening orders are refused; on Free and on a trial they are refused at the allowance, and a workspace with no plan cannot open at all. Also: multi-account orders this month, accounts in one multi-account order, or accounts connected at once. Closing is never refused for billing, and neither are cancelling or moving stops.No. Change plan, or wait for next month.
FEATURE_NOT_IN_PLAN402The plan does not include this — waves, pending orders, always-on accounts. Names the plans that do. Winding down what you already have is never refused.No. Change plan.
CREDIT_LIMIT402Connecting an account, reconnecting one, switching one to always_on, or subscribing, resuming or resetting a copy-trading follower would take a per-account plan's usage beyond its price — accounts past those included, always-on accounts, followers past those included, counted for a whole month — above what the workspace may run now: the plan's price a month (Production: $99), plus what it paid in renewals and usage charges in the last 90 days. The message says the limit and where you are. Nothing already running is changed, and lowering a mode is never refused. Enterprise agreements set their own.No. It grows as payments go through — or switch an account off always-on or disconnect one, or contact us to raise it.
WORKSPACE_BUSY503Another change to this workspace's accounts was still being made after 10 seconds, so this one was not startedYes, shortly.
PAYMENT_OVERDUE402A usage charge could not be collected (declined for a week, or not confirmed with your bank within a month), so opening is paused: opening orders, multi-account orders, connecting new accounts and switching to always_on. Closing positions, cancelling orders and moving stops are never refused, and a disconnected account can still be reconnected (not as always-on) to close what it holds.No. Pay it on Billing (try the card again, or confirm it with your bank); opening resumes as soon as it is paid.

Billing

CodeStatusCauseRetry?
BILLING_NEEDS_THE_OWNER403The plan is changed by the workspace owner, signed in — never with an API keyNo.
PLAN_NOT_PURCHASABLE400Free, the trial, Enterprise, a plan no longer sold (Starter, Scale) or an unknown plan cannot be bought by checkout — whether as a first purchase, a change of plan or paying again for the plan you are onNo. Choose a plan from GET /v1/plans.
CONTRACT_PLAN409The workspace is on a contract plan, changed by talking to usNo.
ALREADY_ON_PLAN409Already paying for that plan by cardNo.
REDUCE_USAGE_FIRST409Changing to a plan whose usage allowance is below what the workspace runs now (always-on accounts, accounts past those the new plan includes). The message says what to reduce. Nothing was started.After reducing it: switch accounts to warm-on-demand or disconnect some.
CARD_SUBSCRIPTION_ACTIVE409Paying by card; cancel that before paying in cryptoNo.
CRYPTO_PERIOD_RUNNING409A crypto-paid month is running; switch to card after it endsAfter it ends.
DOWNGRADE_AT_PERIOD_END409A cheaper plan in crypto is bought once the current month endsAfter it ends.
NOTHING_TO_CANCEL409No paid planNo.
COIN_NOT_ACCEPTED400That coin is not offered; see GET /v1/billing/crypto/coinsNo. Choose another.
BELOW_COIN_MINIMUM409The price is below NOWPayments' minimum for that coin, so it would arrive shortNo. Choose another coin.
CARD_UNAVAILABLE / CRYPTO_UNAVAILABLE503That payment method is not open on this server: not configured, or, for crypto, switched off (pay by card instead).No.
NO_ANNUAL_PRICE400That plan is billed monthly onlyNo. Choose monthly.
NO_AGREEMENT404Paying an agreement: this workspace has noneNo.
PAID_BY_TRANSFER409The agreement is invoiced and paid by transfer, not by cardNo.
ALREADY_PAYING409The agreement is already paid by cardNo.
NOT_CANCELLING409Taking back a cancellation when none is scheduledNo.
PROVIDER_REFUSED409The card provider did not take a cancellation backNo. Contact us.
SUBSCRIPTION_LIVE409Redeeming a coupon while paying for a plan — cancel that subscription firstNo.
PLAN_ARRANGED409Redeeming a coupon on a workspace with a plan arranged with usNo. Get in touch.
CHECKOUT_PENDING409Redeeming a coupon while a card payment for a plan is being set up for this workspaceAfter finishing that payment, or once it lapses (a day).
CHECKOUT_LIMIT429Too many checkouts started in the last 24 hours: 5 per workspace, 10 per workspace owner's emailAfter 24 hours, or contact us.
PROVIDER_ERROR503The payment provider refused or failed; nothing was charged hereYes, shortly.
COUPON_INVALID400That code does not exist, has expired or has been used upNo.
TOO_MANY_ATTEMPTS429Too many wrong codes tried in the last hour — or the card was tried again for a usage charge 3 times in the last hourAfter an hour.

Instruments

CodeStatusCauseRetry?
UNKNOWN_INSTRUMENT400An order named an instrument that is not on GET /v1/instrumentsNo. Name a listed instrument, or send the broker symbol.
INSTRUMENT_NOT_OFFERED422This account's broker does not offer the instrument. Nothing was sent.No.
INSTRUMENT_AMBIGUOUS422The broker has more than one plausible symbol for it; details lists them. Nothing was sent.No. Send the one you mean as symbol.
SYMBOLS_NOT_SYNCED409The account has never been online, so its broker's symbols are not known yetYes, once the account has been online.
SESSIONS_NOT_SYNCED409On GET /v1/accounts/{id}/sessions: the account's market hours have not been read yet. They are read about a minute after it first comes online.Yes, shortly.
SYMBOL_NOT_FOUND404On GET /v1/accounts/{id}/sessions: the account's broker has no such symbolNo. Check GET /v1/accounts/{id}/symbols.
SYMBOLS_UNAVAILABLE503On POST /v1/accounts/{id}/symbols/sync: the terminal did not list its symbols. The previous list is unchanged.Yes, shortly.

Multi-account orders

CodeStatusCauseRetry?
WAVE_ALREADY_RELEASING409Cancelling a multi-account order whose orders have already started going outNo. Close the resulting positions instead.
INSTRUMENT_UNAVAILABLE422A multi-account order named an instrument that one or more of its accounts cannot trade. details lists each account with code (INSTRUMENT_NOT_OFFERED, INSTRUMENT_AMBIGUOUS with candidates, or SYMBOLS_NOT_SYNCED) and message. Nothing was placed.No. Leave those accounts out, bring a never-synced account online once, or send symbol.

Copy trading

CodeStatusCauseRetry?
COPY_STRATEGY_NOT_FOUND404No such strategy in this workspace.No.
COPY_SUBSCRIPTION_NOT_FOUND404No such subscription in this workspace.No.
MASTER_NOT_ALWAYS_ON409Making a strategy from an account that is not always_on. A master traded by hand never sends fxapis an order, so the idle cooler would stop it.No. Switch its mode with POST /v1/accounts/{id}/mode first.
NETTING_NOT_SUPPORTED409The master or the follower is a netting account. Copy trading copies between hedging accounts only.No.
STRATEGY_EXISTS409This account is already a strategy.No.
ACCOUNT_IS_FOLLOWER409Making a strategy from an account that already follows one. An account cannot copy and be copied.No. Delete that subscription first.
ACCOUNT_IS_MASTER409Subscribing, or resuming a subscription on, an account that is a strategy's master; or moving a strategy's master off always_on (POST /v1/accounts/{id}/mode) before the strategy is archived.No.
MODE_CHANGE_CONFLICT409POST /v1/accounts/{id}/mode while other changes to the same account kept landing first. Very rare.Yes, after reading the account.
FOLLOWER_IS_MASTER409Subscribing an account to its own strategy.No.
SUBSCRIPTION_EXISTS409This account already follows this strategy.No.
SUBSCRIPTIONS_ACTIVE409Archiving a strategy one or more subscriptions still copy.No. Pause or delete them first.
STRATEGY_ARCHIVED409Subscribing to, or resuming, an archived strategy.No.
SUBSCRIPTION_STOPPED409Changing settings or status on a subscription a risk limit stopped.No. Reset it with POST /v1/copy/subscriptions/{id}/reset first.
COPY_TRADING_PAUSED503Making a strategy, subscribing, resuming a strategy or subscription, or resetting one while copy trading is paused on this server (before scheduled maintenance). Reading, changing settings, pausing and deleting still work; GET /v1/copy/status says whether it is paused.Yes, once GET /v1/copy/status answers paused: false.
SUBSCRIPTION_DELETING409Changing or resetting a subscription that is being deleted (its positions are still being closed).No. Delete it again to retry closes that failed; it is removed once they are.
NOT_STOPPED409Resetting a subscription that is not stopped.No.
SUBSCRIPTION_CHANGED409A PATCH's settings were merged against a version of the subscription that changed underneath it.Yes — fetch it and try again.

None of these is retryable as sent: each names something about the strategy, subscription or account that the request itself must change first.

TradingView alert hooks

These are returned to TradingView by the hook URL, and recorded in the hook's log.

CodeStatusCauseRetry?
HOOK_LIMIT409Creating a hook when the workspace already has 25No. Delete one you no longer use.
HOOK_DISABLED409The hook is paused — or the API key that made it has been revoked, and the hook stopped with itNo. Resume it; for a revoked key's hook, make a new hook.
INVALID_ALERT400The alert's message could not be read as an orderNo. Fix the alert message.
SENDER_NOT_ALLOWED403The hook only accepts TradingView's webhook addresses, and this came from elsewhereNo.
RATE_LIMITED429More than 30 alerts to one hook in a minuteNo — TradingView does not retry.

Webhooks and events

CodeStatusCauseRetry?
EVENT_NOT_IN_HISTORY400The after event id is older than your plan's event history keeps, or not one of this workspace's.No. Resume with since, and reconcile older state from the REST API.
EVENT_NOT_FOUND404No such event in your event history — or, for redeliver, none this endpoint was sent or receives.No.
DELIVERY_NOT_FOUND404No such delivery attempt to this endpoint in the last 30 days.No.
WEBHOOK_LIMIT409The workspace already has 10 webhook endpoints.No. Delete one you no longer use.
WEBHOOK_DISABLED409The endpoint is switched off or paused, so it cannot be tested or replayed to.No. Enable it first.

Console, keys and members

CodeStatusCauseRetry?
UNTRUSTED_ORIGIN403A state-changing request carried a member's session from a site other than the consoleNo.
KEY_LIMIT409The workspace already has 50 unrevoked keys (expired ones count until revoked). Not to be confused with KEY_LIMIT_EXCEEDED (403), a key's own trading limits.No. Revoke one.
KEY_REVOKED409A revoked key cannot be renamed or re-scopedNo. Create a new one.
MEMBERS_NEED_A_PERSON403Members are managed by a signed-in owner or admin, never with an API keyNo.
NOT_ALLOWED403Your role cannot do that to that memberNo.
ALREADY_MEMBER409That address is already in this workspaceNo.
EMAIL_IN_USE409That address has an fxapis account, so it cannot be invitedNo.
ALREADY_ACCEPTED409The invitation was used; remove the member insteadNo.
INVITATION_INVALID404Any invitation link that is not good — forged, used, withdrawn, expiredNo. Ask for a new one.
INVITATION_CLOSED409Sending again an invitation that was used, withdrawn or expiredNo. Invite again.
PASSWORD_BREACHED400Signing up or accepting an invitation with a password found in known data breachesNo. Choose another password.
EMAIL_UNAVAILABLE / INVITATIONS_UNAVAILABLE503Email or invitations are not configured on this server; copy the invitation link insteadNo.
TOO_SOON429The invitation was sent less than a minute agoYes, after a minute.
STEP_UP_REQUIRED403A sensitive change from a console session that has not confirmed it is its owner in the last two hours (ten minutes in the staff console). details[0].method is totp or password. Never answered to an API key.Yes, after POST /v1/session/step-up (the console does this for you).
STEP_UP_INVALID400The code (an authenticator code, or a backup code, each taken once) or password given to confirm was wrong, or missingYes, with the right one.
STEP_UP_LOCKED429Too many wrong confirmations: the account's sign-in pause for a password, the code's own pause for a code, or, re-entering the staff console, more than five tries a minute from one sessionYes, after retry-after.
STEP_UP_UNAVAILABLE503Re-entering the staff console could not be counted, so it was not checkedYes, in a minute.
MEMBER_INACTIVE403A key cannot be made in the name of a member who was removed, suspended or made a member while making itNo.
SESSION_ONLY403Sessions and confirmations belong to a signed-in person; an API key has noneNo.
SESSION_NOT_FOUND404No such session of yoursNo.
CANNOT_REVOKE_CURRENT409Ending the session you are using is signing outNo. Sign out instead.
ADMIN_KEY_REQUIRED403An ordinary API key called /v1/api-keys. Managing keys takes an admin key (fx_admin_…) or the console.No.
ADMIN_KEY_NOT_ALLOWED403An admin key called a /v1 route other than /v1/api-keys. It manages keys and nothing else; outside /v1 (including /mcp) it gets 401 UNAUTHENTICATED.No. Use an ordinary key.
ADMIN_KEY_IP_NOT_ALLOWED403The admin key does not accept requests from the address this one came from.No. Call from an allowed address, or have the owner make a key that allows it.
ADMIN_KEY_EXPIRED401The admin key is past its expiry.No. The owner makes a new one.
ADMIN_KEY_LIMIT409The workspace already has five active admin keys.No. Revoke one.
OWNER_ONLY403Only the workspace owner, signed in to the console, makes or revokes admin keys.No.

Signing in itself (/auth, used by the console) answers in its own shape, { code, message }:

CodeStatusCause
SIGN_IN_LOCKED429Ten wrong passwords for this account in 15 minutes; paused for 15 minutes (retry-after). Resetting the password lifts it.
CHALLENGE_REQUIRED / CHALLENGE_FAILED403Pass the human check (siteKey names it) and send its token in x-turnstile-token.
TWO_FACTOR_CODE_REQUIRED400Turning two-factor off, or making new backup codes, needs the current code (or a backup code) as well as the password.
TOTP_ALREADY_ENABLED400Two-factor is already on; turn it off before setting up a new app.
TOO_MANY_ATTEMPTS429More than five password checks a minute for one person (changing the password, turning two-factor on or off, new backup codes). Does not pause signing in.
TOTP_URI_NOT_OFFERED / CODE_CHECK_NOT_OFFERED403The setup key is shown once, at setup; codes are checked at sign-in or when the console asks you to confirm.
TRUST_DEVICE_NOT_OFFERED400fxapis asks for a code at every sign-in.

Lifecycle

CodeStatusCauseRetry?
NEEDS_OPERATOR409The account is in a state a human must fix — wrong password, 2FA, certificate, trading disabledNo. See below.
NO_SECRET409No credential stored. The account was disconnected, or creation failed part-way.No. Reconnect it.
ACCOUNT_EXECUTING409An order is in flight, and taking the account offline now would leave its result uncertainYes, once the order settles.
ACCOUNT_LEASED_ELSEWHERE409The account is already being brought online or taken offline by another request. Trading is unaffected.Yes, in a moment.
SCHEDULER_FAILED503We could not bring the account online. Ours, not yours.Yes, with backoff.
RESTART_FAILED503The account went offline but would not come back onlineYes, with backoff.
INTERNAL_ERROR500Unexpected. Already logged on our side with your requestId.Yes, with backoff.

NEEDS_OPERATOR deserves emphasis. Do not retry it, and do not put it in a loop with a backoff. Repeated failed logins are how a broker locks an account, and at that point fixing the password is no longer enough. Surface it to whoever owns the credentials.

Retrying

Decide by code, not by status. Retry 500, and a 503 whose code is not ORDER_UNRESOLVED, with exponential backoff and jitter. Never retry ORDER_UNRESOLVED — poll the order. Do not retry 4xx, except 429 after its retry-after; nothing else about the request will have improved. Do not retry 501 CAPABILITY_UNAVAILABLE either: the answer changes only when the account's terminal is updated.

Why 503, not 502. Every failure that is ours rather than yours — SEND_FAILED, ORDER_UNRESOLVED, SCHEDULER_FAILED, RESTART_FAILED, PROVIDER_ERROR — answers 503 with the JSON body. Until 3 October 2026 these were 502, and the network edge in front of the API replaced every 502 body with its own text, so a client could not tell a safe resend from one that must never happen. If you branched on 502, branch on code.

import random, time

def retryable(response):
    if response.status_code == 500:
        return True
    if response.status_code == 503:
        # SEND_FAILED is proven never to have reached the broker. ORDER_UNRESOLVED
        # may have executed: poll the order, never resend.
        return response.json().get("error", {}).get("code") != "ORDER_UNRESOLVED"
    return False

def call_with_retry(send, attempts=5):
    # For an order, `send` must carry the same Idempotency-Key on every attempt.
    for attempt in range(attempts):
        response = send()
        if not retryable(response):
            return response
        if attempt == attempts - 1:
            return response
        # Jitter, not a fixed schedule: every client backing off in lockstep
        # arrives together and reproduces the load that caused the failure.
        time.sleep(min(2 ** attempt, 30) * (0.5 + random.random()))
    raise AssertionError("unreachable")

Even a retried ORDER_UNRESOLVED is harmless when it carries the same Idempotency-Key: the key stays claimed and the same answer comes back. What must never happen is a resend under a new key.

The lifecycle 503s — SCHEDULER_FAILED, RESTART_FAILED — involve no order and are safe to retry with backoff too.

warm (bring online) is safe to retry as-is: a second call to an account that is already online returns alreadyRunning: true.

Order placement is not safe to retry blindly. Send an Idempotency-Key with every order: a retry with the same key returns the first result instead of placing a second trade. Retrying without one is the one place in this API where a careless retry costs money.

When the state, not the status, is the error

A 202 from warm means we accepted the work, not that it succeeded. The login can still fail afterwards, and it reports through the account's state rather than through an HTTP status nobody is listening to any more.

Poll GET /v1/accounts/{id}/status and treat invalid_credentials, needs_2fa, needs_certificate and trading_disabled as failures. Accounts and lifecycle has a polling loop that handles this correctly.

On this page