# Errors

> The problem details format, which failures are worth retrying, and every error code grouped by HTTP status with what to do about it.

When a request fails, the API returns problem details, a standard JSON error format (RFC 9457), with the content type `application/problem+json`. Every error has a stable `code`. Let your code check `code`, never the `title` or `detail`, which are written for people to read.

```json
{
  "type": "https://algotradertools.com/docs/errors#validation_failed",
  "title": "Validation failed",
  "status": 400,
  "code": "validation_failed",
  "detail": "lot_value: Use a number above 0 and up to 1000.",
  "errors": [
    { "path": "/lot_value", "code": "invalid", "message": "Use a number above 0 and up to 1000." }
  ],
  "request_id": "b7c1e2d4-5f6a-4b8c-9d0e-1f2a3b4c5d6e",
  "doc_url": "https://algotradertools.com/docs/errors#validation_failed"
}
```

| Field        | Description                                                                                                                                                                    |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `type`       | A URI for the error type. It points to this page, at the code's row                                                                                                            |
| `title`      | A short human-readable summary                                                                                                                                                 |
| `status`     | The HTTP status code, repeated in the body                                                                                                                                     |
| `code`       | The stable identifier to branch on. Every code is listed below                                                                                                                 |
| `detail`     | What went wrong in this particular case                                                                                                                                        |
| `errors`     | Only for `validation_failed`. One entry per problem, each with a `path` to the field as a JSON Pointer (`""` for the request as a whole), a field-level `code` and a `message` |
| `request_id` | The request's ID, also in the `X-Request-Id` header. Include it when you contact support                                                                                       |
| `doc_url`    | A link to this page, at the code's row                                                                                                                                         |

## Handling errors [#handling-errors]

Retry these:

* `429` after the number of seconds in `Retry-After`
* `500`, `502`, `503` and `504`, with exponential backoff

Don't retry other errors without changing the request first. A `400`, `401`, `403`, `404`, `405`, `406`, `409` or `415` will fail the same way again.

Retrying a `POST` is safe for accounts and followers. If the first attempt went through, the second fails with `already_connected` or `already_following` instead of creating a duplicate. A retried `POST /v1/copiers` can create a second copier. If you didn't get a response to one, list your copiers before you retry it.

TypeScript:

```ts
const API = "https://api.algotradertools.com/v1";
const MAX_ATTEMPTS = 5;

async function request(method: string, path: string, body?: unknown) {
  for (let attempt = 1; ; attempt++) {
    const response = await fetch(`${API}${path}`, {
      method,
      headers: {
        Authorization: `Bearer ${process.env.ATT_API_KEY}`,
        ...(body === undefined ? {} : { "Content-Type": "application/json" }),
      },
      body: body === undefined ? undefined : JSON.stringify(body),
    });
    if (response.status === 204) return null;
    const payload = await response.json();
    if (response.ok) return payload;

    const retryable = response.status === 429 || response.status >= 500;
    if (!retryable || attempt === MAX_ATTEMPTS) {
      throw new Error(`${payload.code}: ${payload.detail} (request ${payload.request_id})`);
    }
    const retryAfter = Number(response.headers.get("Retry-After"));
    const wait = retryAfter > 0 ? retryAfter * 1000 : 500 * 2 ** attempt;
    await new Promise((resolve) => setTimeout(resolve, wait));
  }
}
```

Python:

```python
import os
import time

import requests

API = "https://api.algotradertools.com/v1"
MAX_ATTEMPTS = 5


def request(method, path, body=None):
    for attempt in range(1, MAX_ATTEMPTS + 1):
        response = requests.request(
            method,
            f"{API}{path}",
            headers={"Authorization": f"Bearer {os.environ['ATT_API_KEY']}"},
            json=body,
            timeout=30,
        )
        if response.status_code == 204:
            return None
        payload = response.json()
        if response.ok:
            return payload
        retryable = response.status_code == 429 or response.status_code >= 500
        if not retryable or attempt == MAX_ATTEMPTS:
            raise RuntimeError(f"{payload['code']}: {payload['detail']} (request {payload['request_id']})")
        retry_after = float(response.headers.get("Retry-After") or 0)
        time.sleep(retry_after if retry_after > 0 else 0.5 * 2**attempt)
```

## 400 Bad Request [#400-bad-request]

| Code                                  | Meaning                                              | Fix                                                                                             |
| ------------------------------------- | ---------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| **`validation_failed`**   | A field is missing or invalid                        | Read `errors`: each entry points at a field and has a [field-level code](#field-level-codes)    |
| **`invalid_json`**        | The body isn't valid JSON                            | Check quoting and encoding. In shells, watch for unescaped quotes inside `-d '…'`               |
| **`invalid_cursor`**      | The `cursor` isn't one the API returned              | Pass `next_cursor` from the previous page unchanged, or leave `cursor` out to start again       |
| **`invalid_credentials`** | The broker rejected the login and password           | Check the account number and password, and that `credential_kind` matches the password you sent |
| **`server_not_found`**    | There's no broker server with that name              | Copy the server exactly as MetaTrader shows it in its login window                              |
| **`already_connected`**   | That MetaTrader account is already connected         | List your accounts; if it isn't yours, contact support                                          |
| **`proxy_unreachable`**   | Nothing could be reached through the account's proxy | Check the proxy's type, host and port, username and password, and that it's online              |
| **`connect_rejected`**    | The account was refused for another reason           | Read `detail`                                                                                   |

## 401 Unauthorized [#401-unauthorized]

| Code                                      | Meaning                         | Fix                                                            |
| ----------------------------------------- | ------------------------------- | -------------------------------------------------------------- |
| **`authentication_required`** | No API key was sent             | Send `Authorization: Bearer <key>` or the `X-API-Key` header   |
| **`invalid_api_key`**         | The key is unknown or malformed | Check for truncation and stray whitespace, or create a new key |
| **`api_key_revoked`**         | The key was revoked             | Create a new key and deploy it                                 |

## 403 Forbidden [#403-forbidden]

| Code                                 | Meaning                                                                 | Fix                                                                                        |
| ------------------------------------ | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| **`insufficient_scope`** | The key lacks the scope this endpoint needs                             | Use a key with the right [scope](/docs/authentication#scopes), for example `copiers:write` |
| **`plan_limit_reached`** | Every account slot of your plan is in use, or you don't have a plan yet | Disconnect an account, or choose a bigger plan in the app                                  |

## 404 Not Found [#404-not-found]

| Code                        | Meaning                                                                                 | Fix                                                     |
| --------------------------- | --------------------------------------------------------------------------------------- | ------------------------------------------------------- |
| **`not_found`** | No such account, copier or follower, or it belongs to someone else; or no such endpoint | Check the ID and the path. Paths have no trailing slash |

## 405 Method Not Allowed [#405-method-not-allowed]

| Code                                 | Meaning                                       | Fix                                                                       |
| ------------------------------------ | --------------------------------------------- | ------------------------------------------------------------------------- |
| **`method_not_allowed`** | The endpoint doesn't support this HTTP method | See the [API reference](/docs/api-reference) for the methods of each path |

## 406 Not Acceptable [#406-not-acceptable]

| Code                             | Meaning                            | Fix                                                      |
| -------------------------------- | ---------------------------------- | -------------------------------------------------------- |
| **`not_acceptable`** | The `Accept` header rules out JSON | Send `Accept: application/json`, or leave the header out |

## 409 Conflict [#409-conflict]

| Code                                   | Meaning                                                                   | Fix                                                                                                                  |
| -------------------------------------- | ------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| **`account_in_use`**       | The account is a copier's master or follower, so it can't be disconnected | [Delete the copier](/docs/copiers#delete-a-copier) or [remove the follower](/docs/followers#remove-a-follower) first |
| **`account_not_released`** | Only an account whose terminal was released can be reconnected            | Nothing to do: the account still has its terminal                                                                    |

## 415 Unsupported Media Type [#415-unsupported-media-type]

| Code                                     | Meaning                     | Fix                                   |
| ---------------------------------------- | --------------------------- | ------------------------------------- |
| **`unsupported_media_type`** | The body isn't sent as JSON | Send `Content-Type: application/json` |

## 429 Too Many Requests [#429-too-many-requests]

| Code                           | Meaning                                                                                               | Fix                                                                                      |
| ------------------------------ | ----------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| **`rate_limited`** | Too many requests for the key, or too many account connects, reconnects and disconnects for your user | Wait the number of seconds in `Retry-After`. See [Rate limits](/docs/limits#rate-limits) |

## 500 and above [#500-and-above]

| Status          | Code                                   | Meaning                                                                  | Fix                                                                                                                                                                                                |
| --------------- | -------------------------------------- | ------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 500             | **`internal_error`**       | Something broke on our side                                              | Retry with backoff. If it persists, contact support with the `request_id`                                                                                                                          |
| 502, 503 or 504 | **`close_failed`**         | Copies couldn't be closed while deleting a copier or removing a follower | Nothing was deleted. Every follower handled up to the failure is now paused, and all of them except the one that failed already had their copies closed. Retry the delete, or resume the followers |
| 502, 503 or 504 | **`disconnect_failed`**    | The account's terminal couldn't be stopped                               | The account was kept. Retry shortly                                                                                                                                                                |
| 502             | **`terminal_unreachable`** | The account's terminal couldn't be reached                               | Retry with backoff                                                                                                                                                                                 |
| 503             | **`terminal_unavailable`** | The terminal is still starting up or busy                                | Retry shortly                                                                                                                                                                                      |
| 503             | **`connect_unavailable`**  | New accounts can't be connected right now                                | Retry later                                                                                                                                                                                        |
| 504             | **`terminal_timeout`**     | The terminal didn't answer in time                                       | Retry shortly                                                                                                                                                                                      |

## Field-level codes [#field-level-codes]

A `validation_failed` error lists each problem in `errors`, with a `path` to the field and a `code`. Besides the generic codes `required`, `invalid`, `null`, `blank`, `max_length`, `invalid_choice`, `does_not_exist`, `incorrect_type`, `min_value`, `max_digits` and `max_decimal_places`, these say exactly what is wrong:

| Code                    | Field                     | Meaning                                                                                                                                                                         |
| ----------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `already_connected`     | `login`                   | You already connected this MetaTrader account                                                                                                                                   |
| `account_released`      | `login`                   | You connected it before and its terminal was released. [Reconnect](/docs/accounts#reconnect-after-a-lapse) it instead                                                           |
| `account_not_connected` | `master_id`, `account_id` | The account is still connecting or was released                                                                                                                                 |
| `account_is_master`     | `account_id`              | The account is a copier's master. An account can't be both a master and a follower                                                                                              |
| `account_is_follower`   | `master_id`               | The account follows another copier. An account can't be both a master and a follower                                                                                            |
| `already_following`     | `account_id`              | The account already copies this master, in this copier or another one                                                                                                           |
| `self_copy`             | `account_id`              | The copier's master can't follow itself                                                                                                                                         |
| `read_only_account`     | `account_id`              | The account was connected with its investor password and can't place trades                                                                                                     |
| `netting_account`       | `master_id`, `account_id` | MT5 netting accounts aren't supported, only hedging accounts                                                                                                                    |
| `automated_trading_off` | `account_id`              | The broker has switched off trading, or automated trading (Expert Advisors), on this account, so copies can't be placed. Ask the broker to allow it                             |
| `immutable`             | `master_id`, `account_id` | The field can't be changed after creation. Create a new copier or follower instead                                                                                              |
| `unknown_field`         | Any                       | The body has a field the resource doesn't have, often a typo. Read-only fields such as `id` or `status` are accepted and ignored, so you can send an object back as you read it |

A `close_positions` query parameter that isn't `true`, `false`, `1` or `0` fails with `validation_failed` too, with the `path` `/close_positions`.
