# Bulk update users from CSV

Upload a CSV that changes many traders' profiles, roles, balances or custom fields in one background job.

- Endpoint: `POST https://api.returning.ai/v1/users/bulk-update`
- Section: Users and data / Bulk operations
- Authentication: `Authorization: Bearer <API_KEY>` (Community API key)
- Permission: `bulkUpdateUser` (Shown in the dashboard as "Bulk Update User")
- Retries: No Idempotency-Key; list jobs before retrying
- Guide: hand-written
- Verified: live, 2026-09-27
- Last updated: 26 Sep 2026
- Web page: https://docs.returning.ai/api-reference/bulk-operations/bulk-update-users-from-csv

## When to use this

- A nightly feed from your platform adds coins or XP, or writes custom field values, for many traders at once.
- You fill in broker customer IDs for traders who were created without one.
- Support needs to correct balances or roles for a list of traders, with a result for every row.

**Instead:** Use [Update User XP and Currency](https://docs.returning.ai/api-reference/users/update-user-xp-and-currency.md) instead to change one trader's balances straight away.

**Queued, not applied:** A `200` means the file is saved and the job is queued. It does not return a job ID; find the job by its `log_name`.

## Authentication

- Header: `Authorization: Bearer <API_KEY>`
- Permission: `bulkUpdateUser`

Uploading needs `bulkUpdateUser`. Reading jobs back needs `getBulkUpdate`, shown in the dashboard as Get Bulk Update History, so give your key both. Every row is limited to traders in the key's community. Keep the key on your server.

## Behaviour

The upload only queues a job. Jobs for one community run one at a time, oldest first. A job moves from `In Queue` to `In Progress`, then ends as `Completed` or `Failed`.

- `Failed` usually means the file was rejected as a whole, for example an unknown column, a number column without its `_op` column, or a missing identifier column. The API does not return the reason, so check the file against the rules below.
- `Completed` can still include failed rows. [Get bulk update job details](https://docs.returning.ai/api-reference/bulk-operations/get-bulk-update-job-details.md) lists the rows, a page at a time, with `status`, and failed rows carry `errors`, each with a `code` such as `USER_NOT_AVAILABLE`. Read details only once the job is `Completed`.
- XP and coin changes are saved to each trader's history, and XP changes can also update the trader's level and XP-based roles. New roles and badges can send the trader a role or badge email if your community has those emails switched on.

## Request

### Headers

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `Authorization` | `string` | Yes | Community API key with `bulkUpdateUser`. (`Bearer <API_KEY>`) |

### Body

Send `multipart/form-data` with the file in field `data`. The first line must be a header row, and columns are separated by commas. You can also send the rows as JSON, `{"data": [{"Email": "trader@example.com", "Total Coins": "500", "Total Coins_op": "add"}]}`, with the same column names.

```csv
Email,Total Coins,Total Coins_op,KYC status
trader@example.com,500,add,verified
```

- Rows are matched by `Email`, not case-sensitive. Only the first row for each trader is applied; repeats fail.
- Columns, spelled exactly like this: `Email`, `First Name`, `Last Name`, `Phone Number`, `Country`, `Language`, `Role`, `Tag`, `Badges`, `Total XP`, `Total Coins`, `Penalty(warned/suspended/banned)`, and each custom field by its display name. Any other column fails the job.
- `Total XP`, `Total Coins` and numerical custom fields need a matching `_op` column, such as `Total Coins_op`, with `add`, `subtract` or `overwrite`. Fill the operator cell on every row, even where the value is empty.
- An empty cell is skipped, so it never clears a value.
- `Role` replaces the trader's roles, apart from `@all`, with the comma-separated list in the cell. Add a `Role_op` column with `add` or `subtract` to change only the listed roles. `Tag` and `Badges` replace the current list.
- To set a broker customer ID, add its column by display name. It fills an empty identifier but cannot change one that is already set.
- There is no row limit; the file can be up to 30 MB.

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `data` | `file` | Yes | The CSV file, with a header row and one row per trader. The form field must be named `data`. (`.csv`, max 30 MB) |
| `log_name` | `string` | No | A name for this job. Make it unique; it is how you find the job in List bulk update jobs. |
| `identifier` | `string` | No | Omit to match rows by the `Email` column. To match by broker customer ID instead, send the key of your community's active identifier field and include a column with that name. (Your active identifier field's key) |
| `currency_cap` | `string` | No | `false` applies coin additions in full. Otherwise, if your community has a coin earning cap, `add` amounts in `Total Coins` are limited by it. (`true` or `false`) |

Send the body as `multipart/form-data`.

### Example request

```bash
curl --request POST \
  --url https://api.returning.ai/v1/users/bulk-update \
  --header 'Authorization: Bearer <API_KEY>' \
  --form 'data=@users.csv' \
  --form 'log_name=Coins top-up 2026-09-26'
```

## Response

A `200` returns only a confirmation that the job is queued. It has no job ID, so use a unique `log_name` and look the job up with [List bulk update jobs](https://docs.returning.ai/api-reference/bulk-operations/list-bulk-update-jobs.md).

### Response fields

| Field | Type | Presence | Description |
| --- | --- | --- | --- |
| `status` | `string` | always | Result of the request. (`success`) |
| `message` | `string` | always | Human-readable summary. Do not branch on it. |
| `data` | `object` | always | A short confirmation. |
| `data.message` | `string` | always | Human-readable confirmation that the job is queued. |

### Example response (200)

```json
{
  "status": "success",
  "message": "Bulk update user process is queued",
  "data": {
    "message": "Bulk update user process is queued. You can check in Api Log ( community settings )"
  }
}
```

## Errors

Errors from the API key check, such as 401 and 403, carry the code in `meta.code`. Other errors from this endpoint have no `code`, so branch on the HTTP status. Problems inside the file never cause an error here; they show up in the job's status and row details.

### Fix the request

| Status | Code | What to do |
| --- | --- | --- |
| 400 | - | No file arrived, the file is not a CSV, or JSON `data` is not a non-empty array. Send the file as multipart form field `data`, with a `.csv` name or a `text/csv` type. |
| 401 | `AUTHENTICATION_REQUIRED` | The key is missing, invalid or expired. Send `Authorization: Bearer <API_KEY>` with a current key. |
| 403 | `API_KEY_PERMISSION_DENIED` | The key is valid but lacks `bulkUpdateUser`. Add the permission in Settings > Integration > API Keys. |

### Fix the data

| Status | Code | What to do |
| --- | --- | --- |
| 404 | `COMMUNITY_NOT_FOUND` | The key's community no longer exists. Use a key from an active community. |

### Retry with backoff

| Status | Code | What to do |
| --- | --- | --- |
| 500 | - | A JSON `500` means the file was not saved and no job was created; retry with backoff. A `500` error page without JSON means the file is over 30 MB or was sent under a form field other than `data`; fix the upload instead. |
| 500 | `AUTHENTICATION_FAILED` | The key could not be checked. Retry the same request with backoff. |

**Retries:** This endpoint does not accept an `Idempotency-Key`. Each upload creates a new job, and running the same file twice applies `add` and `subtract` twice. After a timeout, list jobs and look for your `log_name` before you upload again. Over the [rate limit](https://docs.returning.ai/how-it-works.md#rate-limits), requests return `429`; wait for the window to reset, then retry.

## Next step

- [List bulk update jobs](https://docs.returning.ai/api-reference/bulk-operations/list-bulk-update-jobs.md): `GET /v1/users/bulk-update`. Find your job by matching `name` to your `log_name`, and save its `_id`.
- [Then poll Get bulk update job status until it finishes](https://docs.returning.ai/api-reference/bulk-operations/get-bulk-update-job-status.md): `GET /v1/users/bulk-update/{id}/status`.
