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
- Open Tenants and click Bulk import in the top-right.

- Choose the format — CSV or JSON.
- Choose the mode:
- Insert (default) — create new tenants; existing ids are skipped.
- Upsert — create new tenants and update
name/metadatawhen the id already exists.
- Either click the upload area (or drag-and-drop) a
.csv/.jsonfile (up to 5 MB), or paste the content directly into the editor below. - If you're using CSV, click Download CSV template for the expected header row.
- Fix any validation errors (shown as a toast). Import stays disabled until the content is valid.
- Click Import / Upsert. A toast reports how many tenants were created, updated, or skipped.

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:
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:
[
{ "id": "acme-corp", "name": "Acme Corp", "metadata": { "tier": "gold" } },
{ "id": "globex-inc", "name": "Globex Inc" },
{ "id": "initech" }
]From the API
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
{
"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
{
"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
# 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.
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.