# Bulk create or upsert tenants

> Source: https://truto.one/docs/api-reference/admin/tenants/create-bulk/

`POST /tenant/bulk`

Resource: **Tenants**

## Request body

- **`mode`** _(string)_
  Collision behaviour. `insert` skips existing ids; `upsert` updates `name`/`metadata` on existing ids.
  Allowed: `insert`, `upsert`
- **`tenants`** _(array<object>)_
  The list of tenants to create or upsert. Max 1000 per request — split larger inputs client-side. Only `id`, `name`, and `metadata` are allowed per item.
  - **`id`** _(string)_
    Tenant id. Free-form up to 255 characters, excluding control characters, leading or trailing whitespace, and the dot segments `.` and `..`.
  - **`name`** _(string)_
    Display name. Defaults to `id` when omitted on insert; kept unchanged when omitted on upsert of an existing row.
  - **`metadata`** _(object)_
    Free-form JSON object. Defaults to `{}` when omitted on insert; kept unchanged when omitted on upsert of an existing row.
- **`environment_id`** _(string)_
  Environment to create the tenants in. Required for session auth; ignored for API tokens. Applied to every row in the batch — the endpoint does not let you mix environments in one request.

## Response body

- **`created`** _(array<object>)_
  The tenants that were newly inserted.
  - **`id`** _(string)_
    The tenant identifier — caller-chosen and immutable once created. Composite primary key with `environment_id`, so the same value can exist independently in `development`, `staging`, and `production`. Free-form up to 255 characters: whatever you already use to identify a customer, including `/`, spaces and non-ASCII. The only values rejected are the ones no URL path segment can carry — control characters, leading or trailing whitespace, and the dot segments `.` and `..`.
  - **`environment_id`** _(string)_
    The environment this tenant belongs to.
  - **`name`** _(string)_
    Display name shown in the dashboard. Defaults to the tenant `id` on create if not supplied; can be edited later via `PATCH /tenant/{id}`.
  - **`metadata`** _(object)_
    Free-form JSON object stored verbatim. Truto does not interpret this field — use it for tier, region, billing plan, or any other customer attribute your app cares about.
  - **`created_at`** _(string)_
    The date and time when the tenant was created.
  - **`updated_at`** _(string)_
    The date and time when the tenant was last updated.
- **`updated`** _(array<object>)_
  Tenants that already existed and were updated. Only populated when `mode` is `upsert`; always empty for `insert`.
  - **`id`** _(string)_
    The tenant identifier — caller-chosen and immutable once created. Composite primary key with `environment_id`, so the same value can exist independently in `development`, `staging`, and `production`. Free-form up to 255 characters: whatever you already use to identify a customer, including `/`, spaces and non-ASCII. The only values rejected are the ones no URL path segment can carry — control characters, leading or trailing whitespace, and the dot segments `.` and `..`.
  - **`environment_id`** _(string)_
    The environment this tenant belongs to.
  - **`name`** _(string)_
    Display name shown in the dashboard. Defaults to the tenant `id` on create if not supplied; can be edited later via `PATCH /tenant/{id}`.
  - **`metadata`** _(object)_
    Free-form JSON object stored verbatim. Truto does not interpret this field — use it for tier, region, billing plan, or any other customer attribute your app cares about.
  - **`created_at`** _(string)_
    The date and time when the tenant was created.
  - **`updated_at`** _(string)_
    The date and time when the tenant was last updated.
- **`skipped`** _(array<object>)_
  Rows that were not inserted because a tenant with the same `(id, environment_id)` already existed. Only populated when `mode` is `insert` (the default), which uses `INSERT ... ON CONFLICT DO NOTHING`. Empty for `upsert`.
  - **`id`** _(string)_
    The tenant `id` that was skipped.
  - **`reason`** _(string)_
    Why the row was skipped.
    Allowed: `already_exists`

## Code examples

### curl

```bash
curl -X POST 'https://api.truto.one/tenant/bulk' \
  -H 'Authorization: Bearer <your_api_token>' \
  -H 'Content-Type: application/json' \
  -d '{
  "mode": "insert",
  "tenants": [],
  "environment_id": "8a2b104d-74a6-47f2-b93e-c6b611e82391"
}'
```

### JavaScript

```javascript
const body = {
  "mode": "insert",
  "tenants": [],
  "environment_id": "8a2b104d-74a6-47f2-b93e-c6b611e82391"
};

const response = await fetch('https://api.truto.one/tenant/bulk', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer <your_api_token>',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify(body),
});

const data = await response.json();
console.log(data);
```

### Python

```python
import requests

url = "https://api.truto.one/tenant/bulk"
headers = {
    "Authorization": "Bearer <your_api_token>",
    "Content-Type": "application/json",
}
params = {
}
payload = {
    "mode": "insert",
    "tenants": [],
    "environment_id": "8a2b104d-74a6-47f2-b93e-c6b611e82391"
}

response = requests.post(url, headers=headers, params=params, json=payload)
print(response.json())
```
