# 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 accepts up to 1000 tenants in a single request and reports per-row status.

## From the dashboard

1. Open [Tenants](https://app.truto.one/tenants) and click **Bulk import** in the top-right.
   ![Tenants list with Bulk import](/images/tenants/bulk-import.png)
2. Choose the format — **CSV** or **JSON**.
3. Choose the mode:
   - **Insert** (default) — create new tenants; existing ids are skipped.
   - **Upsert** — create new tenants and update `name` / `metadata` when the id already exists.
4. 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.
5. If you're using CSV, click **Download CSV template** for the expected header row.
6. Fix any validation errors (shown as a toast). **Import** stays disabled until the content is valid.
7. Click **Import** / **Upsert**. A toast reports how many tenants were created, updated, or skipped.
   ![Bulk import tenants modal](/images/tenants/bulk-import-modal.png)

### Allowed fields

Each row may only include:

| Field | Required | Rules |
| --- | --- | --- |
| `id` | Yes | URL-path-safe: letters, digits, and `. _ : @ + -`. 1–255 characters. Unique within the file. |
| `name` | No | Non-empty string, max 255 characters. Defaults to `id` on insert when omitted. |
| `metadata` | No | JSON **object** (not an array or primitive). Blank keys are rejected. |

Unknown columns (CSV) or unknown keys (JSON) are rejected. CSV data rows may not have more values than the header defines.

### CSV format

Headers required: `id`. Optional: `name`, `metadata`. Wrap `metadata` in double quotes — 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 '{
    "mode": "insert",
    "tenants": [
        { "id": "acme-corp", "name": "Acme Corp", "metadata": {"tier":"gold"} },
        { "id": "globex-inc", "name": "Globex Inc" },
        { "id": "initech" }
    ]
}'
```

`mode` is optional and defaults to `insert`. Set `"mode": "upsert"` to update existing rows.

### Insert response

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

Rows in `skipped` collided on `(id, environment_id)`. Insert uses `INSERT ... ON CONFLICT DO NOTHING`, so the request never fails partway through.

### Upsert response

```json
{
  "created": [{ "id": "initech", "name": "initech", "metadata": {}, "…": "…" }],
  "updated": [{ "id": "acme-corp", "name": "Acme Corp", "metadata": { "tier": "gold" }, "…": "…" }],
  "skipped": []
}
```

On upsert, omitted `name` / `metadata` keep the existing row's values. Newly created rows still default `name` to `id` and `metadata` to `{}` when omitted.

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

## From the CLI

```bash
# Insert (default) — skips existing ids
truto tenants create-bulk -b '[{"id":"acme"},{"id":"globex"}]'

# Upsert — updates name/metadata on existing ids
truto tenants create-bulk --mode upsert -b '[{"id":"acme","name":"Acme Corp"}]'

# 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

**Insert:** re-running the same import is safe — existing ids come back in `skipped`, and only new ids appear in `created`. Useful as a "sync my customer list" cron that only adds missing tenants.

**Upsert:** re-running applies the latest `name` / `metadata` from the file to matching ids and creates any missing ones.
