> For the complete documentation index, see [llms.txt](https://docs.metacopier.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.metacopier.io/rest-api/api/async-account-creation.md).

# Async Account Creation and Updates

The `POST /accounts` and `PUT /accounts/{accountId}` endpoints support **asynchronous processing** via a callback URL.

> See [Open API Spec](https://api.metacopier.io/rest/api/documentation/swagger-ui/index.html#/Account%20API/createAccount)

## How It Works

1. You send a `POST /accounts` or `PUT /accounts/{accountId}` request with a `callbackUrl` field in the body.
2. The API validates the request and returns **202 Accepted** immediately with a `requestId`.
3. The account is created or updated in the background.
4. Once finished (success or failure), the result is **POSTed to your callback URL**.

> **Without** `callbackUrl`, both endpoints behave exactly as before, synchronous and blocking until the work is done.

The `requestId` tells you which operation it belongs to: `acc_...` for a creation, `upd_...` for an update. Both are tracked by the same status endpoints.

***

## Timeouts

| Mode                                            | Recommended Read Timeout     | Why                                                                                                                                       |
| ----------------------------------------------- | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| **Sync** (without `callbackUrl`)                | **100 seconds**              | The synchronous endpoints complete within 100 seconds. If it takes longer, the request is likely stuck.                                   |
| **Async** (with `callbackUrl`)                  | **N/A**                      | The API returns `202 Accepted` instantly. The result arrives via callback, no timeout to worry about.                                     |
| **Polling** (`GET /accounts/async/{requestId}`) | **300 seconds** (if waiting) | Async processing includes internal retries and can take up to 300 seconds. Poll periodically until the status is `COMPLETED` or `FAILED`. |

> **Important**: The async flow internally retries broker connections and waits for system capacity, which is why it can take up to 300 seconds. This is handled entirely server-side, your client just needs to wait for the callback or poll.

***

## Quick Start

### 1. Create Account (Async)

```bash
curl -X POST https://api.metacopier.io/rest/api/v1/accounts \
  -H "X-API-KEY: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "type": { "id": 1 },
    "region": { "id": 1 },
    "loginServer": "ICMarketsSC-MT5-4",
    "loginAccountNumber": "12345678",
    "loginAccountPassword": "your_password",
    "callbackUrl": "https://your-server.com/webhook/account-created"
  }'
```

### Response - 202 Accepted

```json
{
  "requestId": "acc_a1b2c3d4e5f67890",
  "message": "Account creation request accepted. The result will be POSTed to the provided callback URL.",
  "callbackUrl": "https://your-server.com/webhook/account-created",
  "status": "QUEUED"
}
```

***

## Changing the Password of an Account

A password change is sent with `PUT /accounts/{accountId}`. By default the new password is stored right away and the connection to the broker is re-established in the background, so `accountInformation` (including `wrongCredentials` and `isInvestorPassword`) reflects the new password only after the reconnect has finished.

{% hint style="warning" %}
`PUT` replaces the account, it is not a partial update. Always send the complete account object. The safe pattern is: `GET /accounts/{accountId}`, change what you need, send the result back.
{% endhint %}

If you need the verdict inside the same call, add `validateCredentialsOnChange`.

```bash
curl -X PUT https://api.metacopier.io/rest/api/v1/accounts/550e8400-e29b-41d4-a716-446655440000 \
  -H "X-API-KEY: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "alias": "My Account",
    "type": { "id": 1 },
    "region": { "id": 1 },
    "loginServer": "ICMarketsSC-MT5-4",
    "loginAccountNumber": "12345678",
    "nativeMode": false,
    "tradingDisabled": false,
    "closeUnmanagedPositions": false,
    "loginAccountPassword": "your_new_password",
    "validateCredentialsOnChange": true
  }'
```

| Result                                   | Meaning                                                                                |
| ---------------------------------------- | -------------------------------------------------------------------------------------- |
| `200 OK` with the account                | The new password was verified against the broker and stored                            |
| `400 [WRONG_CREDENTIALS]`                | The new password is not valid. Nothing was changed, the previous password stays active |
| `503 [HIGH_DEMAND_PLEASE_RETRY_SHORTLY]` | The system is at capacity, retry in a few seconds                                      |

> The check opens a real connection to the broker, so the request takes several seconds. Use a read timeout of **100 seconds**, or use the async variant below.

> Without `validateCredentialsOnChange`, the update behaves exactly as before.

### Password Change (Async)

Add a `callbackUrl` to get the result delivered instead of waiting for it. This can be combined with `validateCredentialsOnChange`.

```bash
curl -X PUT https://api.metacopier.io/rest/api/v1/accounts/550e8400-e29b-41d4-a716-446655440000 \
  -H "X-API-KEY: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "alias": "My Account",
    "type": { "id": 1 },
    "region": { "id": 1 },
    "loginServer": "ICMarketsSC-MT5-4",
    "loginAccountNumber": "12345678",
    "nativeMode": false,
    "tradingDisabled": false,
    "closeUnmanagedPositions": false,
    "loginAccountPassword": "your_new_password",
    "validateCredentialsOnChange": true,
    "callbackUrl": "https://your-server.com/webhook/account-updated"
  }'
```

**Response - 202 Accepted**

```json
{
  "requestId": "upd_a1b2c3d4e5f67890",
  "message": "Account update request accepted. The result will be POSTed to the provided callback URL.",
  "callbackUrl": "https://your-server.com/webhook/account-updated",
  "status": "QUEUED"
}
```

The callback payload, the retry policy and the polling endpoints are identical to the creation flow described below. On a failed credential check you receive `success: false` with `"error": "[WRONG_CREDENTIALS]"`.

***

## Callback Payload

When the request completes, a **POST** request is sent to your `callbackUrl`.

### On Success

```json
{
  "requestId": "acc_a1b2c3d4e5f67890",
  "success": true,
  "account": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "type": { "id": 1, "name": "MT5" },
    "region": { "id": 1, "name": "New York" },
    "loginServer": "ICMarketsSC-MT5-4",
    "loginAccountNumber": "12345678",
    "accountInformation": {
      "connected": true
    }
  },
  "error": null
}
```

### On Failure

```json
{
  "requestId": "acc_a1b2c3d4e5f67890",
  "success": false,
  "account": null,
  "error": "[WRONG_CREDENTIALS]"
}
```

### Callback Headers

| Header         | Description                     |
| -------------- | ------------------------------- |
| `Content-Type` | `application/json`              |
| `X-Request-Id` | The `requestId` for correlation |

### Retry Policy

If your callback endpoint is unavailable, the request is retried:

* **3 attempts** total
* **Backoff**: 2 seconds after attempt 1, 4 seconds after attempt 2
* Each retry uses a **different IP address** (rotating proxy)

***

## Polling (Optional)

You can also poll for status instead of (or in addition to) using callbacks.

### Get Status of a Single Request

```bash
curl https://api.metacopier.io/rest/api/v1/accounts/async/acc_a1b2c3d4e5f67890 \
  -H "X-API-KEY: YOUR_API_KEY"
```

**Response:**

```json
{
  "requestId": "acc_a1b2c3d4e5f67890",
  "status": "COMPLETED",
  "account": { ... },
  "error": null
}
```

### Get All Async Requests

```bash
curl https://api.metacopier.io/rest/api/v1/accounts/async \
  -H "X-API-KEY: YOUR_API_KEY"
```

Returns an array of all tracked async requests for your project.

***

## Status Lifecycle

| Status       | Description                                       |
| ------------ | ------------------------------------------------- |
| `QUEUED`     | Request accepted, waiting to be processed         |
| `PROCESSING` | The account is currently being created or updated |
| `COMPLETED`  | Finished successfully (includes `account` field)  |
| `FAILED`     | Failed (includes `error` field)                   |

> Statuses are retained for **1 hour** after completion, then automatically removed.

***

## Processing Rules

* **Sequential per project**: Only 1 async request runs at a time per project, creations and updates share that slot. Additional requests are queued and processed in order.
* **Duplicate check (`failIfAccountExistsInProject: true`)**: if an account with the same `loginServer` and `loginAccountNumber` already exists in the project, the creation is rejected right away and is not queued. The `202` response already contains `"status": "FAILED"` and `"error": "[ACCOUNT_WITH_SAME_LOGIN_SERVER_AND_LOGIN_ACCOUNT_NUMBER_ALREADY_EXISTS]"`, and the error callback is sent immediately.
* **Same account submitted twice**: if the first request is still queued or processing, the second one waits for it. If the first one succeeds, the second fails with the duplicate error above (when `failIfAccountExistsInProject` is `true`). If the first one fails, the second is processed normally.

***

## Callback URL Requirements

| Rule         | Details                                             |
| ------------ | --------------------------------------------------- |
| **Protocol** | `https://` or `http://`                             |
| **Format**   | Must be a valid URL                                 |
| **Method**   | Your endpoint must accept `POST` requests           |
| **Response** | Return any `2xx` status code to acknowledge receipt |

If the URL is invalid, the API returns `400 Bad Request` with one of:

* `[CALLBACK_URL_REQUIRED]` - field is present but empty
* `[CALLBACK_URL_INVALID]` - malformed URL
* `[CALLBACK_URL_INVALID_PROTOCOL]` - protocol is not http/https

***

## Error Codes

All standard `POST /accounts` and `PUT /accounts/{accountId}` errors apply. These are returned synchronously (before the 202) if validation fails:

| Error                                            | HTTP Status | Description                                                                          |
| ------------------------------------------------ | ----------- | ------------------------------------------------------------------------------------ |
| `[CALLBACK_URL_INVALID]`                         | 400         | Malformed callback URL                                                               |
| `[CALLBACK_URL_INVALID_PROTOCOL]`                | 400         | Callback URL must use http or https                                                  |
| `[WRONG_CREDENTIALS]`                            | 400         | Only with `validateCredentialsOnChange`, the new password was rejected by the broker |
| `[HIGH_DEMAND_PLEASE_RETRY_SHORTLY]`             | 503         | Only with `validateCredentialsOnChange`, no capacity for the check, retry shortly    |
| `[REGION_NOT_FOUND]`                             | 404         | Invalid region ID                                                                    |
| `[ACCOUNT_TYPE_NOT_FOUND]`                       | 404         | Invalid account type ID                                                              |
| `[ACCOUNT_LEVEL_API_KEY_CANNOT_CREATE_ACCOUNTS]` | 403         | Must use a project-level API key                                                     |

Errors that occur **during** async processing are delivered via the callback payload and the polling endpoint (status = `FAILED`).

***

## Example: Node.js Callback Server

The same endpoint can serve both flows. Use the `requestId` prefix to tell them apart: `acc_` for a creation, `upd_` for an update.

```javascript
const express = require('express');
const app = express();
app.use(express.json());

app.post('/webhook/account', (req, res) => {
  const { requestId, success, account, error } = req.body;
  const action = requestId.startsWith('upd_') ? 'update' : 'creation';

  if (success) {
    console.log(`Account ${action} done: ${account.id} (request: ${requestId})`);
    // Store the account, update your database, etc.
  } else {
    console.error(`Account ${action} failed: ${error} (request: ${requestId})`);
    // error is "[WRONG_CREDENTIALS]" when a password check failed
  }

  res.status(200).send('OK');
});

app.listen(3000);
```
