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

# Platform API reference for tenant provisioning

> Reference for the NduMed platform API: authenticate with X-Platform-Key, provision tenants, list tenants, read usage, and suspend, reactivate or archive a tenant.

The platform API lets operators automate hospital (tenant) provisioning and lifecycle. It is a JSON API under `/platform/v1`. Hospital staff screens have no public API.

## Authentication

Send the platform key in the `X-Platform-Key` header on every request. The key is set by the operator in the deployment's environment; there is no default, and with no key configured every call is refused.

A missing or wrong key returns `401` with an RFC 9457 `application/problem+json` body. Both cases return identical responses.

```bash theme={null}
curl -s https://ndumed.example.com/platform/v1/tenants \
  -H "X-Platform-Key: $PLATFORM_API_KEY"
```

## Provision a tenant

`POST /platform/v1/tenants`

| Field | Rules |
| - | - |
| `slug` | Matches `^[a-z][a-z0-9]{2,29}$`; becomes the subdomain |
| `name` | Up to 200 characters |
| `timezone` | A valid IANA zone, for example `Africa/Lagos` |
| `currency` | Three uppercase letters, for example `NGN` |
| `adminEmail` | Up to 160 characters |
| `adminFullName` | Up to 160 characters |

```bash theme={null}
curl -s -X POST https://ndumed.example.com/platform/v1/tenants \
  -H "X-Platform-Key: $PLATFORM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"slug":"stmary","name":"St Mary Hospital","timezone":"Africa/Lagos","currency":"NGN","adminEmail":"admin@example.com","adminFullName":"Ada Admin"}'
```

Response body:

```json theme={null}
{
  "slug": "stmary",
  "name": "St Mary Hospital",
  "status": "ACTIVE",
  "timezone": "Africa/Lagos",
  "currency": "NGN",
  "createdAt": "...",
  "created": true,
  "adminInvitation": { "acceptUrl": "https://stmary.../invite/...", "expiresAt": "..." }
}
```

| Status | Meaning |
| - | - |
| `201` | Tenant created |
| `200` | Tenant already existed: pending updates are applied and a **new** invitation is issued (the previous link stops working) |
| `409` | Another provisioning run is in progress, or the tenant is suspended or archived (reactivate it first) |

> [!NOTE]
>
> The API returns the invitation link in `acceptUrl`; deliver it to the administrator yourself. Only a hash of the token is stored, so a lost link cannot be recovered, only replaced by calling this endpoint again.

## List tenants

`GET /platform/v1/tenants` returns `{ "tenants": [ { slug, name, status, timezone, currency, locale, createdAt, archivedAt } ] }`, including archived tenants.

## Tenant usage

`GET /platform/v1/tenants/{slug}/usage` returns `{ slug, status, counts: { patients, encounters, admissions, activeUsers }, countedAt }`. Returns `400` for an invalid slug and `404` for an unknown one.

## Lifecycle

| Endpoint | Body |
| - | - |
| `POST /platform/v1/tenants/{slug}/suspend` | `{ "reason": "..." }` (optional, up to 200 characters) |
| `POST /platform/v1/tenants/{slug}/reactivate` | `{ "confirmSlug": "...", "reason": "..." }` |
| `POST /platform/v1/tenants/{slug}/archive` | `{ "confirmSlug": "...", "reason": "..." }` |

Response: `{ slug, status, previousStatus, archivedAt, changed }`.

* Archiving, and reactivating an archived tenant, need `confirmSlug` equal to the slug, otherwise `400`.
* Suspending an archived tenant returns `409`.
* Requesting the state a tenant is already in succeeds with `"changed": false`.


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