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
| Code | Status | Cause | Retry? |
|---|---|---|---|
UNAUTHENTICATED | 401 | Missing, malformed, unknown, revoked or expired key — deliberately one code for all of them | No. Fix the key. |
MISSING_SCOPE | 403 | Valid 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
| Code | Status | Cause | Retry? |
|---|---|---|---|
INVALID_REQUEST | 400 | Body or parameters failed validation. details names the fields. | No. Fix the request. |
NOT_FOUND | 404 | No such endpoint. | No. |
ACCOUNT_NOT_FOUND | 404 | No such account for your tenant | No. |
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
| Code | Status | Cause | Retry? |
|---|---|---|---|
ACCOUNT_EXISTS | 409 | That login + server is already connected for your tenant | No. Use the existing account. |
SECRET_STORE_UNAVAILABLE | 503 | The credential could not be stored, so the account was not created | Yes, with backoff. |
Trading
| Code | Status | Cause | Retry? |
|---|---|---|---|
SEND_FAILED | 502 | Proven never to have reached the broker | Yes. The only safe resend. |
ORDER_REJECTED | 422 | The broker refused it, with a reason and its own retcode | Only if retryable is true |
ORDER_UNRESOLVED | 502 | We do not know whether it executed | Never. Poll the order. |
IDEMPOTENCY_KEY_REUSED | 409 | That key was used for a different order | No. Use a new key. |
IDEMPOTENCY_IN_FLIGHT | 409 | The first request with that key is still running | Yes, shortly. |
TRADING_DISABLED | 409 | The account's kill switch is on | No. |
NO_RUNTIME | 409 | Ready with no terminal — the state is stale | Restart 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
| Code | Status | Cause | Retry? |
|---|---|---|---|
QUOTA_EXCEEDED | 402 | Opening 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_PLAN | 402 | The 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
| Code | Status | Cause | Retry? |
|---|---|---|---|
BILLING_NEEDS_THE_OWNER | 403 | The plan is changed by the workspace owner, signed in — never with an API key | No. |
PLAN_NOT_PURCHASABLE | 400 | Free, Enterprise or an unknown plan cannot be bought by checkout | No. |
CONTRACT_PLAN | 409 | The workspace is on a contract plan, changed by talking to us | No. |
ALREADY_ON_PLAN | 409 | Already paying for that plan by card | No. |
CARD_SUBSCRIPTION_ACTIVE | 409 | Paying by card; cancel that before paying in crypto | No. |
CRYPTO_PERIOD_RUNNING | 409 | A crypto-paid month is running; switch to card after it ends | After it ends. |
DOWNGRADE_AT_PERIOD_END | 409 | A cheaper plan in crypto is bought once the current month ends | After it ends. |
NOTHING_TO_CANCEL | 409 | No paid plan | No. |
COIN_NOT_ACCEPTED | 400 | That coin is not offered; see GET /billing/crypto/coins | No. Choose another. |
BELOW_COIN_MINIMUM | 409 | The price is below NOWPayments' minimum for that coin, so it would arrive short | No. Choose another coin. |
CARD_UNAVAILABLE / CRYPTO_UNAVAILABLE | 503 | That payment method is not configured on this server | No. |
PROVIDER_ERROR | 502 | The payment provider refused or failed; nothing was charged here | Yes, shortly. |
Console, keys and members
| Code | Status | Cause | Retry? |
|---|---|---|---|
UNTRUSTED_ORIGIN | 403 | A state-changing request carried a member's session from a site other than the console | No. |
KEY_LIMIT | 409 | The workspace already has 50 active keys | No. Revoke one. |
KEY_REVOKED | 409 | A revoked key cannot be renamed or re-scoped | No. Create a new one. |
MEMBERS_NEED_A_PERSON | 403 | Members are managed by a signed-in owner or admin, never with an API key | No. |
NOT_ALLOWED | 403 | Your role cannot do that to that member | No. |
ALREADY_MEMBER | 409 | That address is already in this workspace | No. |
EMAIL_IN_USE | 409 | That address has an fxapis account, so it cannot be invited | No. |
ALREADY_ACCEPTED | 409 | The invitation was used; remove the member instead | No. |
INVITATION_INVALID | 404 | Any invitation link that is not good — forged, used, withdrawn, expired | No. Ask for a new one. |
Lifecycle
| Code | Status | Cause | Retry? |
|---|---|---|---|
NEEDS_OPERATOR | 409 | The account is in a state a human must fix — wrong password, 2FA, certificate, trading disabled | No. See below. |
NO_SECRET | 409 | No credential stored. The account was disconnected, or creation failed part-way. | No. Reconnect it. |
ACCOUNT_EXECUTING | 409 | An order is in flight, and cooling now would lose its outcome | Yes, once the order settles. |
SCHEDULER_FAILED | 502 | We could not start a runtime. Ours, not yours. | Yes, with backoff. |
RESTART_FAILED | 502 | The account cooled but would not come back up | Yes, with backoff. |
INTERNAL_ERROR | 500 | Unexpected. 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.