> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ndi.nace.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Tenants

> Partner keys that act for many customers, each its own tenant

A **partner** serves many customers of its own through NDI. Each customer is a
**tenant**: a separate NDI organization with its own workspaces, files, jobs,
domains, access labels, schemas and limits. The partner holds one API key for
all of them and names the tenant on every request.

## Acting for a tenant

Send these headers with the partner key on every `/v1` route:

| Header | Value |
| - | - |
| `X-API-Key` | The partner key. |
| `X-NDI-Tenant` | `ext:<external_ref>`, your own reference for the customer, or the tenant's `id`. |
| `X-NDI-External-User` | A UUID for the person the request acts for. Required. |

Everything the request reads or creates belongs to that tenant alone: another
tenant's resources answer `404` exactly as if they did not exist. Platform
domains and extraction schemas stay visible to every tenant.

| Case | Status | `error.code` |
| - | - | - |
| The tenant is unknown, another partner's, or not `ext:<ref>` or a UUID | 404 | `tenant_not_found` |
| The tenant is disabled | 403 | `tenant_disabled` |
| A partner key without `X-NDI-Tenant` on a tenant route | 400 | `tenant_required` |
| A partner key with `X-NDI-External-Account` instead of `X-NDI-Tenant` | 400 | `tenant_required` |
| `X-NDI-Tenant` together with `X-NDI-External-Account` | 422 | `invalid_request` |
| `X-NDI-Tenant` from a key that is not a partner's | 422 | `partner_required` |
| `X-NDI-Tenant` without `X-NDI-External-User` | 422 | `invalid_request` |

A malformed `X-NDI-Tenant` answers `404` like an unknown one, so no answer
tells you which tenants exist. A partner key acts for a tenant only through
`X-NDI-Tenant`: `X-NDI-External-Account` is for service keys that are not a
partner's. The legacy `/api/v1` routes, including `/api/v1/example` and
`/api/v1/health/load`, answer `403` to a partner key.

## Managing tenants

Call these with the partner key and **without** `X-NDI-Tenant`.

| Method | Path | |
| - | - | - |
| GET | `/v1/tenants` | Page through every tenant, oldest first. |
| PUT | `/v1/tenants/{external_ref}` | Create the tenant (`201`) or update it (`200`). Safe to repeat. |
| GET | `/v1/tenants/{external_ref}` | Read one tenant. |
| DELETE | `/v1/tenants/{external_ref}` | Disable the tenant. Its data is kept; `PUT` with `"disabled": false` re-enables it. With `?purge=true`, permanently delete the tenant's data instead (see [Purging a tenant](#purging-a-tenant)). |

`PUT` accepts `display_name` (up to 128 characters), `disabled`,
`data_retention`, `retention_tier` and `limits` (`rpm`, `max_concurrent_jobs`). Creating a tenant
past the partner's tenant cap answers `409` `tenant_limit_reached`; disabled
tenants count toward the cap.

* `rpm` is the tenant's requests per minute. A request over it answers `429`.
* `max_concurrent_jobs` is how many parse, split, classify, categorize,
  extract, ground and agent jobs the tenant may have running at once. More jobs
  wait in the queue instead of being refused. NDI's own per-tenant cap applies
  when it is lower, so this setting can lower a tenant's concurrency but not
  raise it. Ingestion isn't counted.
* A limit above the partner's own is lowered to it. The partner's own limit is
  the lower of its own value and its partner-wide cap (`partner.rpm`,
  `partner.concurrent_jobs`), when one is set.
* Lowering the partner's limits later doesn't rewrite existing tenants. The
  partner-wide caps bound all of its tenants together, and so each of them too.
* `data_retention` is how the tenant's document operations retain data, `short`
  or `zero_retention`. Omit it on create to inherit your own setting, or on
  update to leave the tenant's setting unchanged. The tenant is returned with
  the setting in force for it: on a deployment that has not enabled zero
  retention, that is `short`. Under `zero_retention`, results are never
  stored: a request must pass `wait_seconds` and read the result from the
  response, or it answers `422` `wait_required_for_zero_retention`. Setting
  `zero_retention` on a deployment that has not enabled it answers `422`
  `invalid_request`.
* `retention_tier` is the tenant's account tier for job retention, `paid` or
  `free`. A paid tenant's job inputs, outputs and processing files are removed
  48 hours after the job completes, a free tenant's 7 days after; a failed or
  cancelled job counts from the failure or cancel. A tenant never given a tier
  is `paid`. Omit it on update to leave the tier unchanged. A change applies to
  jobs that finish afterwards. See [Data retention](/concepts/data-retention#jobs-and-artifacts).

| Case | Status | `error.code` |
| - | - | - |
| The key is not a partner's | 422 | `partner_required` |
| The partner was disabled while `PUT` ran (afterwards its key answers `401`) | 403 | `access_denied` |
| `X-NDI-Tenant` sent to a management route | 422 | `invalid_request` |
| No tenant with that `external_ref` (`GET`, `DELETE`) | 404 | `tenant_not_found` |
| A new tenant past the partner's tenant cap | 409 | `tenant_limit_reached` |

## Purging a tenant

`DELETE /v1/tenants/{external_ref}?purge=true` permanently deletes everything
the tenant holds. It cannot be undone. Without `purge=true`, `DELETE` only
disables the tenant and keeps its data.

Call it with the partner key and no `X-NDI-Tenant`, like the other management
routes.

The call answers `202` with a `tenant_purge` job. The tenant is disabled at
once and its running jobs are cancelled. Every file's bytes, every result,
cache entry and webhook payload of the tenant is then deleted, and its keys are
revoked. Only anonymized metadata is kept: the job records (ids, kinds,
statuses, timestamps, pages, units, bytes, credits, token counts per model and
error codes, with no job names, inputs, results or error messages), an
anonymized record for each deleted file and for each console user whose only
organization it was, the tenant's workspaces and keys as renamed rows with no
settings and no usable key hash, and its other records (memberships,
invitations, uploads, webhook endpoints and events, access labels) marked
deleted, with no names, addresses, URLs, secrets or payloads. See
[What we keep after deletion](/concepts/data-retention#what-we-keep-after-deletion).

Poll the job with `GET /v1/jobs/{job_id}` using the partner key and **without**
`X-NDI-Tenant`; a partner may read only the purge jobs it owns this way. When
the job has `succeeded`, its `result` reports what the verification pass found:

| Field | Meaning |
| - | - |
| `verified` | `true` only when no content of the tenant remains anywhere and every kept record is anonymized. |
| `stores` | One entry per store (`store`, `remaining`); `remaining` is `0` when that store is clean. |
| `billing_record_kept` | Always `true`: the purge keeps the anonymized metadata described above. |
| `tenant_id`, `external_ref` | The tenant that was purged. |

A purge that ends with `verified: false` is still a `succeeded` job: the
report says what is left, and repeating the `DELETE` runs the purge again. A
purge already queued or running is returned as it is. Once a purge has
verified clean, the `external_ref` no longer resolves: repeating the call
answers `404` `tenant_not_found`, and a later `PUT` creates a new, empty
tenant.

## Usage across tenants

With the partner key and no `X-NDI-Tenant`, `GET /v1/usage?group_by=tenant`
returns one bucket per tenant, keyed by its `external_ref`, over the same
`period_start`/`period_end` window as any usage report. Disabled tenants are
included, since their usage is still owed. Only your own tenants are
counted.

One tenant's usage is the ordinary `GET /v1/usage` with `X-NDI-Tenant`.
`group_by=tenant` with `X-NDI-Tenant`, a `workspace_id`, or a key that is not
a partner's answers `422` `invalid_request`. Any other grouping from the
partner key without `X-NDI-Tenant`, and any request with
`X-NDI-External-Account`, answers `400` `tenant_required`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.