# Bulk import

> Source: https://truto.one/docs/guides/tenants/bulk-import/

When you're migrating from another platform, seeding staging with a known list of customers, or onboarding a batch of new tenants at once, use the bulk-import endpoint. It creates up to 1000 tenants in a single request and reports per-row status (`created` vs `skipped` due to a pre-existing ID).

## From the dashboard

1. Open [Tenants](https://app.truto.one/tenants) and click **Bulk import** in the top-right.
2. Choose the format — **CSV** or **JSON**.
3. Either **click the upload area** (or drag-and-drop) a `.csv` / `.json` file (up to 5 MB), **or paste** the content directly into the editor below.
4. If you're using CSV, click **Download CSV template** for the expected header row.
5. Review the parse preview — the modal shows how many rows are ready to import and flags any parse errors.
6. Click **Import**. The modal closes and a toast reports the result — `Imported N tenants` (with `M skipped (already existed)` if any collided on ID).

### CSV format

Headers required: `id`. Optional: `name`, `metadata`. Wrap `metadata` in double quotes because it contains commas — the JSON is what's stored:

```csv
id,name,metadata
acme-corp,Acme Corp,"{""tier"":""gold"",""region"":""wnam""}"
globex-inc,Globex Inc,"{""tier"":""silver""}"
initech,Initech,
```

### JSON format

A top-level array of tenant objects:

```json
[
  { "id": "acme-corp", "name": "Acme Corp", "metadata": { "tier": "gold" } },
  { "id": "globex-inc", "name": "Globex Inc" },
  { "id": "initech" }
]
```

## From the API

```bash
curl --location 'https://api.truto.one/tenant/bulk' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer <your-api-token>' \
--data '{
    "tenants": [
        { "id": "acme-corp", "name": "Acme Corp", "metadata": {"tier":"gold"} },
        { "id": "globex-inc", "name": "Globex Inc" },
        { "id": "initech" }
    ]
}'
```

Response (`201 Created`):

```json
{
  "created": [
    { "id": "acme-corp", "environment_id": "…", "name": "Acme Corp", "metadata": {"tier":"gold"}, "created_at": "…", "updated_at": "…" },
    { "id": "globex-inc", "environment_id": "…", "name": "Globex Inc", "metadata": {}, "created_at": "…", "updated_at": "…" },
    { "id": "initech", "environment_id": "…", "name": "initech", "metadata": {}, "created_at": "…", "updated_at": "…" }
  ],
  "skipped": []
}
```

Rows in `skipped` collided on `(id, environment_id)` — a tenant with that ID already existed. The endpoint uses `INSERT ... ON CONFLICT DO NOTHING`, so the request never fails partway through: whichever rows can be inserted are, and the rest are reported back to you.

**Caps:** at most 1000 tenants per request. Split larger inputs client-side and call `POST /tenant/bulk` multiple times.

## From the CLI

```bash
# Inline
truto tenants create-bulk -b '[{"id":"acme"},{"id":"globex"}]'

# From an NDJSON file (one JSON object per line)
cat tenants.ndjson | truto tenants create-bulk --stdin

# From a JSON array file
truto tenants create-bulk -b "$(cat tenants.json)"
```

The CLI enforces the 1000-row cap client-side so you catch it before hitting the network. See [`truto tenants`](/docs/cli/admin-resources#tenants).

## Idempotency and re-runs

Because the endpoint skips existing IDs rather than failing, re-running the same import is safe: rerunning after adding a few new entries returns only those new entries in `created` and the rest in `skipped`. This makes it usable as a "sync my customer list" job on a cron.

If you need "upsert" semantics — update `name` / `metadata` on rows that already exist — do that separately with `PATCH /tenant/:id` after the bulk create returns its `skipped` list.
