# Import through the API

> Bulk import into a group and its progress.

For a few subscribers at a time (a sign-up, a checkout) use [`POST /subscribers`](https://docs.uptools.io/echo/developer-guide/subscribers/#add-or-update-a-subscriber). For a list, use the bulk import: it runs in the background, exactly like a file import in the app, and shows up on the **History** tab under **Subscribers**.

## Start an import

`POST /groups/{id}/import-subscribers` imports 1 to 50,000 subscribers into one group. It needs Subscribers: manage.

| Key | Meaning |
|---|---|
| `subscribers` | The rows. Each row is an object with `email` (required) and `fields` (values by field key). No other keys. |

```bash
curl -X POST "$ECHO_API/groups/257e83a2-d55e-4a8c-b9a4-b3e939c151cc/import-subscribers" \
  -H "Authorization: Bearer $ECHO_TOKEN" -H "Content-Type: application/json" \
  -d '{"subscribers":[
        {"email":"bence.kovacs@example.com","fields":{"name":"Bence","city":"Győr"}},
        {"email":"reka.toth@example.org","fields":{"name":"Réka","rendelesek_szama":5}},
        {"email":"hibas-cim"}
      ]}'
```

```http
HTTP/1.1 202 Accepted
Content-Type: application/json
X-Ratelimit-Limit: 5
X-Ratelimit-Remaining: 4

{"data":{"import_id":"df745729-be25-495c-a5b1-8a070dbd18cb"}}
```

The rate limit headers of this answer show the import bucket (5 a minute), see [Errors and limits](https://docs.uptools.io/echo/developer-guide/errors-and-limits/#rate-limits).

Every field key used in any row must exist in the workspace; otherwise nothing is imported:

```json
{"error":{"code":"validation","message":"Please check the highlighted fields.","fields":{"fields.kedvenc_szin":"Unknown field."}}}
```

An unknown group is `404 not_found`; an empty list or more than 50,000 rows is `422` with `fields.subscribers`.

## Follow the import

`GET /subscribers/import/{import_id}` (Subscribers: view) shows the progress and the result. Poll it every few seconds until `status` is `done`, `failed` or `canceled`.

```bash
curl -H "Authorization: Bearer $ECHO_TOKEN" "$ECHO_API/subscribers/import/df745729-be25-495c-a5b1-8a070dbd18cb"
```

```json
{"data":{"id":"df745729-be25-495c-a5b1-8a070dbd18cb","kind":"csv","status":"done","filename":"api-import.csv","size_bytes":115,"encoding":"utf-8","delimiter":",","has_header":true,"preview":{"headers":[],"rows":[],"columns":0},"mapping":["email","city","name","rendelesek_szama"],"groups":[{"id":"257e83a2-d55e-4a8c-b9a4-b3e939c151cc","name":"Webshop vásárlók"}],"start_automations":false,"processed_rows":3,"added":2,"updated":0,"failed":1,"error":"","created_at":"2026-10-22T08:52:08.576581Z","started_at":"2026-10-22T08:52:08.759366Z","finished_at":"2026-10-22T08:52:08.810755Z","failed_rows":[{"row":3,"email":"hibas-cim","reason":"invalid_email","detail":""}]}}
```

| Key | Meaning |
|---|---|
| `status` | `queued` (waiting for an earlier import), `processing`, `done`, `failed` or `canceled`. |
| `processed_rows` | Rows handled so far. |
| `added`, `updated`, `failed` | New subscribers, updated subscribers, rows left out. |
| `error` | `parse_error` or `internal` when the whole import failed, otherwise empty. |
| `mapping` | The columns: `email`, then the field keys used (in alphabetical order). |
| `start_automations` | Always `false` for API imports. |
| `failed_rows` | The first 100 rows left out: `row` (the row's position in your `subscribers` array, from 1), `email` as sent (empty when missing), `reason` and `detail` (the field, when a value was refused). |

The API import is stored as a CSV named `api-import.csv`, so `kind`, `filename`, `encoding`, `delimiter` and `has_header` describe that file. `failed` counts every row left out, `failed_rows` lists the first 100. `reason` is one of `missing_email`, `invalid_email`, `duplicate`, `role_address`, `short_gmail`, `suppressed`, `invalid_number`, `invalid_date`, `value_too_long` or `invalid_text`. The import's result page in the app (**History** tab, **View**) shows the same rows.

## How rows are handled

- New addresses become `active` subscribers with the source `import`, in the group. Imported subscribers get no confirmation email, so only import people who agreed to receive your emails.
- Existing addresses are updated and added to the group; their status doesn't change, so an unsubscribed subscriber stays unsubscribed.
- An empty value doesn't erase a stored value.
- Rows fail for the same reasons as in the app: an invalid or missing address, a role address, an address on the suppression list, an address twice in the list, or a value that doesn't fit the field type (numbers for number fields, `YYYY-MM-DD` for dates).
- Imports don't start automation workflows.
- Imports run one at a time per workspace; a second one waits as `queued`.
- When the import finishes, the workspace's notification bell shows it; no email notification is sent for API imports.

## Limits

| Limit | Value |
|---|---|
| Rows per request | 1 to 50,000 |
| Request body | 10 MB (`413 too_large` above it) |
| Imports per token | 5 a minute, on top of the 120 requests a minute |
