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.

requestId is on every response, success or failure, 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 themNo. Fix the key.
MISSING_SCOPE403Valid key, wrong scope. Names the scope it wants.No. Issue a key with the scope.

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.

Requests

CodeStatusCauseRetry?
INVALID_REQUEST400Body or parameters failed validation. details names the fields.No. Fix the request.
NOT_FOUND404No such endpoint.No.
ACCOUNT_NOT_FOUND404No such account for your tenantNo.

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.

Accounts

CodeStatusCauseRetry?
ACCOUNT_EXISTS409That login + server is already connected for your tenantNo. Use the existing account.
SECRET_STORE_UNAVAILABLE503The credential could not be stored, so the account was not createdYes, with backoff.

Trading

CodeStatusCauseRetry?
SEND_FAILED502Proven 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_UNRESOLVED502We 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_DISABLED409The account's kill switch is onNo.
NO_RUNTIME409Ready with no terminal — the state is staleRestart the account.

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 allowance — orders or waves this month, accounts in one wave, or accounts connected at once. Closing, cancelling and moving stops are never refused for this.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.

Billing

CodeStatusCauseRetry?
BILLING_NEEDS_THE_OWNER403The plan is changed by the workspace owner, signed in — never with an API keyNo.
PLAN_NOT_PURCHASABLE400Free, Enterprise or an unknown plan cannot be bought by checkoutNo.
CONTRACT_PLAN409The workspace is on a contract plan, changed by talking to usNo.
ALREADY_ON_PLAN409Already paying for that plan by cardNo.
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 /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 configured on this serverNo.
PROVIDER_ERROR502The payment provider refused or failed; nothing was charged hereYes, shortly.

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 active keysNo. 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.

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 cooling now would lose its outcomeYes, once the order settles.
SCHEDULER_FAILED502We could not start a runtime. Ours, not yours.Yes, with backoff.
RESTART_FAILED502The account cooled but would not come back upYes, 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

Retry 502, 503 and 500 with exponential backoff and jitter. Do not retry 4xx; nothing about the request will have improved.

import random, time

RETRYABLE = {500, 502, 503}

def call_with_retry(send, attempts=5):
    for attempt in range(attempts):
        response = send()
        if response.status_code not in RETRYABLE:
            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")

warm is safe to retry as-is: it is idempotent, and a second call to an account that came up 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