# Platform Admin

Cross-tenant platform administration: tenants, plans, licenses, custom domains, subscriptions, platform users, and job queues. Requires a platform-admin account, not a tenant membership.

Generated from `apps/api/openapi.json`. HTML version: https://docs.institflow.com/en/reference/platform

116 operations.

## DELETE /api/v1/platform/accounts/links

Remove a student/guardian institute link. Idempotent — already-removed is returned as-is.

- Operation id: `AccountLinksController_remove_v1`
- Permission: —
- Auth: public (no credential)
- `Idempotency-Key`: not required

### Request body

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `kind` | enum: "student" \| "guardian" | yes |  |
| `email` | string (email) | yes | 0–320 chars |

```json
{
  "kind": "student",
  "email": "ada.lovelace@example.com"
}
```

### Responses

**200** — The removed link.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `kind` | enum: "student" \| "guardian" | yes |  |
| `status` | enum: "invited" \| "active" \| "removed" \| "declined" | yes |  |
| `invitedAt` | string (date-time) \| null | yes |  |
| `activatedAt` | string (date-time) \| null | yes |  |
| `removedAt` | string (date-time) \| null | yes |  |
| `declinedAt` | string (date-time) \| null | yes |  |
| `inviteEmailSentAt` | string (date-time) \| null | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X DELETE "https://your-institute.example/api/v1/platform/accounts/links" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "kind": "student",
    "email": "ada.lovelace@example.com"
  }'
```

## GET /api/v1/platform/accounts/links

This install's own institute links, newest first — never another install's or the account's other institutes.

- Operation id: `AccountLinksController_list_v1`
- Permission: —
- Auth: public (no credential)
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `cursor` | query | string | no |  |
| `limit` | query | integer | no | 1–100 · default: 25 |

### Responses

**200** — A page of institute links.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `data` | array<object> | yes |  |
| `nextCursor` | string \| null | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X GET "https://your-institute.example/api/v1/platform/accounts/links" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## POST /api/v1/platform/accounts/links

Invite a student/guardian by email to this institute. Idempotent on the link — a repeat returns the existing link; the invitation email is sent again only if it was never delivered, or when `resend` is set and the last send is at least two minutes old.

- Operation id: `AccountLinksController_create_v1`
- Permission: —
- Auth: public (no credential)
- `Idempotency-Key`: not required

### Request body

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `kind` | enum: "student" \| "guardian" | yes |  |
| `email` | string (email) | no | 0–320 chars |
| `identifier` | object \| object | no |  |
| `resend` | boolean | no |  |
| `ref` | string (uuid) | no |  |

```json
{
  "kind": "student",
  "email": "ada.lovelace@example.com",
  "identifier": {
    "email": "ada.lovelace@example.com"
  },
  "resend": true,
  "ref": "42f48402-42f4-72f4-8f48-42f4840242f4"
}
```

### Responses

**201** — The invited (or already-invited) link.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `kind` | enum: "student" \| "guardian" | yes |  |
| `status` | enum: "invited" \| "active" \| "removed" \| "declined" | yes |  |
| `invitedAt` | string (date-time) \| null | yes |  |
| `activatedAt` | string (date-time) \| null | yes |  |
| `removedAt` | string (date-time) \| null | yes |  |
| `declinedAt` | string (date-time) \| null | yes |  |
| `inviteEmailSentAt` | string (date-time) \| null | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/platform/accounts/links" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "kind": "student",
    "email": "ada.lovelace@example.com",
    "identifier": {
      "email": "ada.lovelace@example.com"
    },
    "resend": true,
    "ref": "42f48402-42f4-72f4-8f48-42f4840242f4"
  }'
```

## POST /api/v1/platform/accounts/links/activate

The first successful Institflow-account sign-in into this institute (§2.5 step 3). Idempotent — activating an already-active link is a no-op.

- Operation id: `AccountLinksController_activate_v1`
- Permission: —
- Auth: public (no credential)
- `Idempotency-Key`: not required

### Request body

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `kind` | enum: "student" \| "guardian" | yes |  |
| `email` | string (email) | yes | 0–320 chars |
| `accountId` | string (uuid) | yes |  |
| `ensure` | boolean | no |  |

```json
{
  "kind": "student",
  "email": "ada.lovelace@example.com",
  "accountId": "fec33a71-fec3-7ec3-8c33-fec33a71fec3",
  "ensure": true
}
```

### Responses

**200** — The activated link.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `kind` | enum: "student" \| "guardian" | yes |  |
| `status` | enum: "invited" \| "active" \| "removed" \| "declined" | yes |  |
| `invitedAt` | string (date-time) \| null | yes |  |
| `activatedAt` | string (date-time) \| null | yes |  |
| `removedAt` | string (date-time) \| null | yes |  |
| `declinedAt` | string (date-time) \| null | yes |  |
| `inviteEmailSentAt` | string (date-time) \| null | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/platform/accounts/links/activate" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "kind": "student",
    "email": "ada.lovelace@example.com",
    "accountId": "fec33a71-fec3-7ec3-8c33-fec33a71fec3",
    "ensure": true
  }'
```

## POST /api/v1/platform/accounts/links/decision

Tell the account that asked to join through a join link the owner's answer (`approved` or `rejected`): it is mailed. The account must own the `email` given.

- Operation id: `AccountLinksController_decision_v1`
- Permission: —
- Auth: public (no credential)
- `Idempotency-Key`: not required

### Request body

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `kind` | enum: "student" \| "guardian" | yes |  |
| `email` | string (email) | yes | 0–320 chars |
| `accountId` | string (uuid) | yes |  |
| `decision` | enum: "approved" \| "rejected" | yes |  |

```json
{
  "kind": "student",
  "email": "ada.lovelace@example.com",
  "accountId": "fec33a71-fec3-7ec3-8c33-fec33a71fec3",
  "decision": "approved"
}
```

### Responses

**200** — The account was notified.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `ok` | enum: true | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/platform/accounts/links/decision" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "kind": "student",
    "email": "ada.lovelace@example.com",
    "accountId": "fec33a71-fec3-7ec3-8c33-fec33a71fec3",
    "decision": "approved"
  }'
```

## POST /api/v1/platform/accounts/links/responses

The answer to each of this install's own invitations (`ref` = the install's invitation id): `pending`, `accepted` or `declined`. A ref this install never made reads `pending`, like an open one.

- Operation id: `AccountLinksController_responses_v1`
- Permission: —
- Auth: public (no credential)
- `Idempotency-Key`: not required

### Request body

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `refs` | array<string (uuid)> | yes |  |

```json
{
  "refs": [
    "d7ec7de3-d7ec-77ec-8ec7-d7ec7de3d7ec"
  ]
}
```

### Responses

**200** — One entry per requested ref.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `data` | array<object> | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/platform/accounts/links/responses" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "refs": [
      "d7ec7de3-d7ec-77ec-8ec7-d7ec7de3d7ec"
    ]
  }'
```

## GET /api/v1/platform/admins

List platform admins: role, status, 2FA enrolment, last login.

- Operation id: `PlatformAdminsController_list_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `cursor` | query | string | no |  |
| `limit` | query | integer | no | 1–100 · default: 25 |

### Responses

**200** — A page of platform admins.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `data` | array<object> | yes |  |
| `nextCursor` | string \| null | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X GET "https://your-institute.example/api/v1/platform/admins" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## DELETE /api/v1/platform/admins/{userId}

Remove a platform admin (the user account itself is untouched). Owner only, cannot target yourself, cannot remove the last enabled owner.

- Operation id: `PlatformAdminsController_remove_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `userId` | path | string | yes |  |

### Responses

**200** — The platform admin was removed.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `ok` | enum: true | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X DELETE "https://your-institute.example/api/v1/platform/admins/11f967df-11f9-71f9-8f96-11f967df11f9" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## PATCH /api/v1/platform/admins/{userId}

Change a platform admin’s role. Owner only, cannot target yourself; requires a step-up MFA check within the last 10 minutes.

- Operation id: `PlatformAdminsController_updateRole_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `userId` | path | string | yes |  |

### Request body

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `role` | enum: "owner" \| "support" | yes |  |

```json
{
  "role": "owner"
}
```

### Responses

**200** — The role was changed.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `ok` | enum: true | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X PATCH "https://your-institute.example/api/v1/platform/admins/11f967df-11f9-71f9-8f96-11f967df11f9" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "role": "owner"
  }'
```

## POST /api/v1/platform/admins/{userId}/disable

Disable a platform admin (revokes every session). Owner only, cannot target yourself, cannot disable the last enabled owner.

- Operation id: `PlatformAdminsController_disable_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `userId` | path | string | yes |  |

### Responses

**200** — The platform admin was disabled.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `ok` | enum: true | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/platform/admins/11f967df-11f9-71f9-8f96-11f967df11f9/disable" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## POST /api/v1/platform/admins/{userId}/enable

Re-enable a disabled platform admin. Owner only; requires a step-up MFA check within the last 10 minutes.

- Operation id: `PlatformAdminsController_enable_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `userId` | path | string | yes |  |

### Responses

**200** — The platform admin was enabled.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `ok` | enum: true | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/platform/admins/11f967df-11f9-71f9-8f96-11f967df11f9/enable" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## POST /api/v1/platform/admins/{userId}/totp/reset

Reset a platform admin’s two-factor authentication for a lost device — they enrol again at next login. Owner only.

- Operation id: `PlatformAdminsController_resetTotp_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `userId` | path | string | yes |  |

### Responses

**200** — Two-factor authentication was reset.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `ok` | enum: true | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/platform/admins/11f967df-11f9-71f9-8f96-11f967df11f9/totp/reset" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## GET /api/v1/platform/admins/invitations

List every platform-admin invitation, pending or not.

- Operation id: `PlatformAdminsController_listInvitations_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `cursor` | query | string | no |  |
| `limit` | query | integer | no | 1–100 · default: 25 |

### Responses

**200** — A page of invitations.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `data` | array<object> | yes |  |
| `nextCursor` | string \| null | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X GET "https://your-institute.example/api/v1/platform/admins/invitations" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## POST /api/v1/platform/admins/invitations

Invite a new platform admin by email. Owner only; requires a step-up MFA check within the last 10 minutes.

- Operation id: `PlatformAdminsController_invite_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Request body

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `email` | string (email) | yes |  |
| `role` | enum: "owner" \| "support" | yes |  |

```json
{
  "email": "ada.lovelace@example.com",
  "role": "owner"
}
```

### Responses

**201** — The created invitation.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `email` | string (email) | yes |  |
| `role` | enum: "owner" \| "support" | yes |  |
| `invitedBy` | string (uuid) | yes |  |
| `expiresAt` | string (date-time) | yes |  |
| `acceptedAt` | string (date-time) \| null | yes |  |
| `revokedAt` | string (date-time) \| null | yes |  |
| `createdAt` | string (date-time) | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/platform/admins/invitations" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "email": "ada.lovelace@example.com",
    "role": "owner"
  }'
```

## POST /api/v1/platform/admins/invitations/{id}/revoke

Revoke a still-pending platform-admin invitation. Owner only; requires a step-up MFA check within the last 10 minutes.

- Operation id: `PlatformAdminsController_revokeInvitation_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes |  |

### Responses

**200** — The invitation was revoked.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `ok` | enum: true | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/platform/admins/invitations/37386ae0-3738-7738-8386-37386ae03738/revoke" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## GET /api/v1/platform/customer-requests

List institute requests (cursor-paginated, optional status filter).

- Operation id: `PlatformCustomerRequestsController_list_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `cursor` | query | string | no |  |
| `limit` | query | integer | no | 1–100 · default: 25 |
| `status` | query | enum: "pending" \| "approved" \| "refused" | no |  |

### Responses

**200** — A page of institute requests.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `data` | array<object> | yes |  |
| `nextCursor` | string \| null | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X GET "https://your-institute.example/api/v1/platform/customer-requests" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## GET /api/v1/platform/customer-requests/{id}

One institute request, with the requesting account’s email.

- Operation id: `PlatformCustomerRequestsController_getDetail_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes |  |

### Responses

**200** — The institute request.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `instituteName` | string | yes |  |
| `slug` | string | yes |  |
| `country` | string | yes |  |
| `sizeBand` | enum: "small" \| "medium" \| "large" \| "xlarge" | yes |  |
| `phone` | string \| null | yes |  |
| `status` | enum: "pending" \| "approved" \| "refused" | yes |  |
| `refusalReason` | string \| null | yes |  |
| `tenantId` | string (uuid) \| null | yes |  |
| `createdAt` | string (date-time) | yes |  |
| `updatedAt` | string (date-time) | yes |  |
| `accountId` | string (uuid) | yes |  |
| `requesterEmail` | string (email) | yes |  |
| `decidedAt` | string (date-time) \| null | yes |  |
| `decidedBy` | string (uuid) \| null | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X GET "https://your-institute.example/api/v1/platform/customer-requests/37386ae0-3738-7738-8386-37386ae03738" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## POST /api/v1/platform/customer-requests/{id}/approve

Approve an institute request: creates the tenant (the "Add a customer" path), sets its owner, mails the requester. Owner only; requires a step-up MFA check within the last 10 minutes.

- Operation id: `PlatformCustomerRequestsController_approve_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes |  |

### Request body

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `planCode` | string | yes | 1–50 chars |
| `trialDays` | integer | no | 0–365 |

```json
{
  "planCode": "string",
  "trialDays": 0
}
```

### Responses

**200** — The approved request, now linked to its tenant.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `instituteName` | string | yes |  |
| `slug` | string | yes |  |
| `country` | string | yes |  |
| `sizeBand` | enum: "small" \| "medium" \| "large" \| "xlarge" | yes |  |
| `phone` | string \| null | yes |  |
| `status` | enum: "pending" \| "approved" \| "refused" | yes |  |
| `refusalReason` | string \| null | yes |  |
| `tenantId` | string (uuid) \| null | yes |  |
| `createdAt` | string (date-time) | yes |  |
| `updatedAt` | string (date-time) | yes |  |
| `accountId` | string (uuid) | yes |  |
| `requesterEmail` | string (email) | yes |  |
| `decidedAt` | string (date-time) \| null | yes |  |
| `decidedBy` | string (uuid) \| null | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/platform/customer-requests/37386ae0-3738-7738-8386-37386ae03738/approve" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "planCode": "string",
    "trialDays": 0
  }'
```

## POST /api/v1/platform/customer-requests/{id}/refuse

Refuse an institute request with a reason, mailed to the requester. Owner only; requires a step-up MFA check within the last 10 minutes.

- Operation id: `PlatformCustomerRequestsController_refuse_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes |  |

### Request body

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `reason` | string | yes | 1–2000 chars |

```json
{
  "reason": "Requested by guardian."
}
```

### Responses

**200** — The refused request.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `instituteName` | string | yes |  |
| `slug` | string | yes |  |
| `country` | string | yes |  |
| `sizeBand` | enum: "small" \| "medium" \| "large" \| "xlarge" | yes |  |
| `phone` | string \| null | yes |  |
| `status` | enum: "pending" \| "approved" \| "refused" | yes |  |
| `refusalReason` | string \| null | yes |  |
| `tenantId` | string (uuid) \| null | yes |  |
| `createdAt` | string (date-time) | yes |  |
| `updatedAt` | string (date-time) | yes |  |
| `accountId` | string (uuid) | yes |  |
| `requesterEmail` | string (email) | yes |  |
| `decidedAt` | string (date-time) \| null | yes |  |
| `decidedBy` | string (uuid) \| null | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/platform/customer-requests/37386ae0-3738-7738-8386-37386ae03738/refuse" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "reason": "Requested by guardian."
  }'
```

## GET /api/v1/platform/installs

Fleet-wide list of self-hosted installs: institute, version, last heartbeat, updater result, relay install state (cursor-paginated).

- Operation id: `PlatformInstallsController_list_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `cursor` | query | string | no |  |
| `limit` | query | integer | no | 1–100 · default: 25 |

### Responses

**200** — A page of installs across every tenant.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `data` | array<object> | yes |  |
| `nextCursor` | string \| null | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X GET "https://your-institute.example/api/v1/platform/installs" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## POST /api/v1/platform/installs/{id}/dns

Restore this install's DNS A record (docs/specs/phase12-one-step-install.md §2.5). Refused for a non-active customer, an own-domain install, or one with no stored address.

- Operation id: `PlatformInstallsController_restoreDns_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes |  |

### Responses

**200** — The install, unchanged, once its DNS record is restored.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `licenseId` | string (uuid) | yes |  |
| `installId` | string (uuid) | yes |  |
| `tenantId` | string (uuid) | yes |  |
| `tenantName` | string | yes |  |
| `tenantSlug` | string | yes |  |
| `appVersion` | string \| null | yes |  |
| `lastSeenAt` | string (date-time) \| null | yes |  |
| `firstSeenAt` | string (date-time) | yes |  |
| `updateChannel` | enum: "stable" \| "beta" \| null \| null | yes |  |
| `updaterLastCheckAt` | string (date-time) \| null | yes |  |
| `updaterLastResult` | enum: "up_to_date" \| "updated" \| "rolled_back" \| "failed" \| "skipped" \| null \| null | yes |  |
| `updaterLastReason` | enum: "none" \| "auto_update_off" \| "min_from_version" \| "locked" \| "migration_running" \| "manifest_unreachable" \| "bad_signature" \| "channel_mismatch" \| "digest_mismatch" \| "docker_error" \| "health_check_failed" \| "api_unreachable" \| "setup_pending" \| null \| null | yes |  |
| `backup` | object \| null | yes |  |
| `guardian` | object \| null | yes |  |
| `needsAdoption` | boolean | yes |  |
| `relayInstallStatus` | enum: "approved" \| "pending" \| "blocked" \| null \| null | yes |  |
| `address` | string \| null | yes |  |
| `serverIp` | string \| null | yes |  |
| `state` | enum: "provisioning" \| "running" \| "unreachable" \| null \| null | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/platform/installs/37386ae0-3738-7738-8386-37386ae03738/dns" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## GET /api/v1/platform/licenses/{licenseId}/installs

Installs seen on this license, with last-seen, version, tamper signals and pending commands.

- Operation id: `PlatformLicensesController_installs_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `licenseId` | path | string | yes |  |
| `cursor` | query | string | no |  |
| `limit` | query | integer | no | 1–100 · default: 25 |

### Responses

**200** — A page of installs.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `data` | array<object> | yes |  |
| `nextCursor` | string \| null | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X GET "https://your-institute.example/api/v1/platform/licenses/f5f084e9-f5f0-75f0-8f08-f5f084e9f5f0/installs" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## POST /api/v1/platform/licenses/{licenseId}/lock

Lock a self-hosted install: issues a signed lock command applied on its next heartbeat.

- Operation id: `PlatformLicensesController_lock_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `licenseId` | path | string | yes |  |

### Request body

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `reason` | string | yes | 1–500 chars |

```json
{
  "reason": "Requested by guardian."
}
```

### Responses

**201** — The signed lock command awaiting acknowledgement.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `licenseId` | string (uuid) | yes |  |
| `kind` | enum: "lock" \| "unlock" | yes |  |
| `reason` | string \| null | yes |  |
| `issuedAt` | string (date-time) | yes |  |
| `acknowledgedAt` | string (date-time) \| null | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/platform/licenses/f5f084e9-f5f0-75f0-8f08-f5f084e9f5f0/lock" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "reason": "Requested by guardian."
  }'
```

## POST /api/v1/platform/licenses/{licenseId}/unlock

Unlock a self-hosted install: issues a signed unlock command.

- Operation id: `PlatformLicensesController_unlock_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `licenseId` | path | string | yes |  |

### Responses

**201** — The signed unlock command awaiting acknowledgement.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `licenseId` | string (uuid) | yes |  |
| `kind` | enum: "lock" \| "unlock" | yes |  |
| `reason` | string \| null | yes |  |
| `issuedAt` | string (date-time) | yes |  |
| `acknowledgedAt` | string (date-time) \| null | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/platform/licenses/f5f084e9-f5f0-75f0-8f08-f5f084e9f5f0/unlock" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## POST /api/v1/platform/licenses/{licenseId}/unlock-token

Mint a one-time signed unlock token the customer pastes into their self-hosted admin when the license server is unreachable.

- Operation id: `PlatformLicensesController_unlockToken_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `licenseId` | path | string | yes |  |

### Responses

**201** — The one-time unlock token. Shown once — it is not retrievable again.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `token` | string | yes |  |
| `expiresAt` | string (date-time) | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/platform/licenses/f5f084e9-f5f0-75f0-8f08-f5f084e9f5f0/unlock-token" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## POST /api/v1/platform/licenses/heartbeat

Self-hosted install heartbeat, signed with the relay identity. Returns a signed envelope carrying server time, state, and any pending lock/unlock commands.

- Operation id: `LicenseHeartbeatController_heartbeat_v1`
- Permission: —
- Auth: public (no credential)
- `Idempotency-Key`: not required

### Request body

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `jti` | string | yes | 1–200 chars |
| `appVersion` | string | yes | 1–50 chars |
| `counter` | integer | yes | 0–9007199254740991 |
| `clientTime` | string (date-time) | yes |  |
| `maxObservedTime` | string (date-time) \| null | yes |  |
| `nonce` | string | yes | 1–100 chars |
| `appliedCommandIds` | array<string (uuid)> | no | default: [] |
| `updater` | object | no |  |
| `backup` | object | no |  |
| `guardian` | object | no |  |

```json
{
  "jti": "string",
  "appVersion": "string",
  "counter": 0,
  "clientTime": "2026-03-02T09:00:00.000Z",
  "maxObservedTime": "2026-03-02T09:00:00.000Z",
  "nonce": "string",
  "appliedCommandIds": [
    "3a42c45f-3a42-7a42-842c-3a42c45f3a42"
  ],
  "updater": {
    "lastCheckAt": "2026-03-02T09:00:00.000Z",
    "lastResult": "up_to_date",
    "reason": "none",
    "channel": "stable"
  },
  "backup": {
    "lastStatus": "success",
    "lastRunAt": "2026-03-02T09:00:00.000Z",
    "lastSuccessAt": "2026-03-02T09:00:00.000Z"
  },
  "guardian": {
    "version": 1,
    "updaterVersion": "string",
    "lastTickAt": "2026-03-02T09:00:00.000Z"
  }
}
```

### Responses

**200** — A signed heartbeat envelope.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `envelope` | string | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/platform/licenses/heartbeat" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "jti": "string",
    "appVersion": "string",
    "counter": 0,
    "clientTime": "2026-03-02T09:00:00.000Z",
    "maxObservedTime": "2026-03-02T09:00:00.000Z",
    "nonce": "string",
    "appliedCommandIds": [
      "3a42c45f-3a42-7a42-842c-3a42c45f3a42"
    ],
    "updater": {
      "lastCheckAt": "2026-03-02T09:00:00.000Z",
      "lastResult": "up_to_date",
      "reason": "none",
      "channel": "stable"
    },
    "backup": {
      "lastStatus": "success",
      "lastRunAt": "2026-03-02T09:00:00.000Z",
      "lastSuccessAt": "2026-03-02T09:00:00.000Z"
    },
    "guardian": {
      "version": 1,
      "updaterVersion": "string",
      "lastTickAt": "2026-03-02T09:00:00.000Z"
    }
  }'
```

## GET /api/v1/platform/licenses/releases/manifest

The signed release manifest for a channel. Verified against the license public key baked into every self-hosted image.

- Operation id: `ReleaseManifestController_manifest_v1`
- Permission: —
- Auth: public (no credential)
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `channel` | query | enum: "stable" \| "beta" | no | default: "stable" |

### Responses

**200** — The signed manifest (compact JWS) and, advisory only, its decoded payload.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `manifest` | string | yes |  |
| `payload` | object | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X GET "https://your-institute.example/api/v1/platform/licenses/releases/manifest" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## GET /api/v1/platform/licenses/releases/v2/manifest

The signed release manifest, version 2 (updater and backup images, desired-state sha256, repair mode), for a channel.

- Operation id: `ReleaseManifestController_manifestV2_v1`
- Permission: —
- Auth: public (no credential)
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `channel` | query | enum: "stable" \| "beta" | no | default: "stable" |

### Responses

**200** — The signed v2 manifest (compact JWS) and, advisory only, its decoded payload.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `manifest` | string | yes |  |
| `payload` | object | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X GET "https://your-institute.example/api/v1/platform/licenses/releases/v2/manifest" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## GET /api/v1/platform/licenses/releases/v2/manifest/{version}

The signed v2 manifest of one published version.

- Operation id: `ReleaseManifestController_manifestV2ForVersion_v1`
- Permission: —
- Auth: public (no credential)
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `version` | path | string | yes |  |

### Responses

**200** — The signed v2 manifest (compact JWS) of that version and, advisory only, its payload.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `manifest` | string | yes |  |
| `payload` | object | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X GET "https://your-institute.example/api/v1/platform/licenses/releases/v2/manifest/string" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## POST /api/v1/platform/login-discovery

Resolve an email to every host it can sign in on (platform admin / Institflow account / institute install) — identical response shape whether the email is known or not.

- Operation id: `LoginDiscoveryController_discover_v1`
- Permission: —
- Auth: public (no credential)
- `Idempotency-Key`: not required

### Request body

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `email` | string (email) | yes | 0–320 chars |

```json
{
  "email": "ada.lovelace@example.com"
}
```

### Responses

**200** — Zero or more login outcomes.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `outcomes` | array<unknown> | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/platform/login-discovery" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "email": "ada.lovelace@example.com"
  }'
```

## GET /api/v1/platform/me

The current platform admin’s profile, 2FA status and session count.

- Operation id: `PlatformAdminOwnProfileController_me_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Responses

**200** — The profile.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `userId` | string (uuid) | yes |  |
| `email` | string (email) | yes |  |
| `pendingEmail` | string (email) \| null | yes |  |
| `fullName` | string | yes |  |
| `locale` | enum: "ar" \| "en" \| "he" | yes |  |
| `role` | enum: "owner" \| "support" | yes |  |
| `totpEnrolled` | boolean | yes |  |
| `sessionCount` | integer | yes | -9007199254740991–9007199254740991 |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X GET "https://your-institute.example/api/v1/platform/me" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## POST /api/v1/platform/me/change-email

Request an email change — sends a confirmation link to the NEW address. Requires the current password and a step-up MFA check within the last 10 minutes.

- Operation id: `PlatformAdminOwnProfileController_changeEmail_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Request body

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `newEmail` | string (email) | yes |  |
| `currentPassword` | string | yes | ≥1 chars |

```json
{
  "newEmail": "ada.lovelace@example.com",
  "currentPassword": "string"
}
```

### Responses

**200** — A confirmation link was sent.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `ok` | enum: true | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/platform/me/change-email" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "newEmail": "ada.lovelace@example.com",
    "currentPassword": "string"
  }'
```

## POST /api/v1/platform/me/change-password

Change your own password. Revokes every OTHER session (this one stays signed in).

- Operation id: `PlatformAdminOwnProfileController_changePassword_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Request body

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `currentPassword` | string | yes | ≥1 chars |
| `newPassword` | string | yes | ≥10 chars |

```json
{
  "currentPassword": "string",
  "newPassword": "string"
}
```

### Responses

**200** — The password was changed.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `ok` | enum: true | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/platform/me/change-password" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "currentPassword": "string",
    "newPassword": "string"
  }'
```

## POST /api/v1/platform/me/confirm-email

Confirm a pending email change from the link sent to the new address.

- Operation id: `PlatformAdminOwnProfileController_confirmEmail_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Request body

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `token` | string | yes | ≥1 chars |

```json
{
  "token": "string"
}
```

### Responses

**200** — The email address was changed.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `ok` | enum: true | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/platform/me/confirm-email" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "token": "string"
  }'
```

## GET /api/v1/platform/me/sessions

Every active session for the current platform admin.

- Operation id: `PlatformAdminOwnProfileController_sessions_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Responses

**200** — The active sessions.

```json
[
  {
    "id": "37386ae0-3738-7738-8386-37386ae03738",
    "userAgent": "string",
    "ip": "string",
    "lastUsedAt": "2026-03-02T09:00:00.000Z",
    "createdAt": "2026-03-02T09:00:00.000Z",
    "mfaAt": "2026-03-02T09:00:00.000Z"
  }
]
```

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X GET "https://your-institute.example/api/v1/platform/me/sessions" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## DELETE /api/v1/platform/me/sessions/{id}

Revoke one of your own sessions.

- Operation id: `PlatformAdminOwnProfileController_revokeSession_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes |  |

### Responses

**200** — The session was revoked.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `ok` | enum: true | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X DELETE "https://your-institute.example/api/v1/platform/me/sessions/37386ae0-3738-7738-8386-37386ae03738" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## POST /api/v1/platform/me/totp/enable

Complete TOTP enrolment with a live code from the app. This *is* the login the mfa/enroll challenge stood in for: returns the real token pair, plus 10 recovery codes shown exactly once.

- Operation id: `PlatformAdminOwnProfileController_totpEnable_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Request body

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `code` | string | yes |  |
| `client` | enum: "web" \| "mobile" | no | default: "web" |

```json
{
  "code": "string",
  "client": "web"
}
```

### Responses

**201** — Tokens and the recovery codes.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `accessToken` | string | yes |  |
| `refreshToken` | string | no |  |
| `user` | object | yes |  |
| `memberships` | array<MembershipSummary> | yes |  |
| `activeTenantId` | string (uuid) \| null | yes |  |
| `recoveryCodes` | array<string> | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/platform/me/totp/enable" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "code": "string",
    "client": "web"
  }'
```

## POST /api/v1/platform/me/totp/regenerate-recovery-codes

Replace every recovery code. Requires a live TOTP code (re-authenticates in place of a step-up).

- Operation id: `PlatformAdminOwnProfileController_regenerateRecoveryCodes_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Request body

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `code` | string | yes |  |

```json
{
  "code": "string"
}
```

### Responses

**200** — The new recovery codes.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `recoveryCodes` | array<string> | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/platform/me/totp/regenerate-recovery-codes" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "code": "string"
  }'
```

## POST /api/v1/platform/me/totp/setup

Begin TOTP enrolment: returns the otpauth:// URI + secret (shown once). Refused once 2FA is already enabled.

- Operation id: `PlatformAdminOwnProfileController_totpSetup_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Responses

**200** — The pending secret.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `otpauthUri` | string | yes |  |
| `secretBase32` | string | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/platform/me/totp/setup" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## GET /api/v1/platform/plans

List every plan.

- Operation id: `PlansController_list_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Responses

**200** — Every plan.

```json
[
  {
    "id": "37386ae0-3738-7738-8386-37386ae03738",
    "code": "string",
    "name": "Algebra I",
    "priceMinor": 12000,
    "currency": "USD",
    "billingInterval": "month",
    "limits": {
      "maxStudents": -9007199254740991,
      "maxBranches": -9007199254740991,
      "maxVideoCourses": -9007199254740991,
      "maxLessonsPerCourse": -9007199254740991,
      "notificationQuota": {
        "whatsapp": -9007199254740991,
        "sms": -9007199254740991,
        "email": "ada.lovelace@example.com"
      }
    },
    "features": [
      "string"
    ],
    "isActive": true,
    "createdAt": "2026-03-02T09:00:00.000Z",
    "updatedAt": "2026-03-02T09:00:00.000Z"
  }
]
```

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X GET "https://your-institute.example/api/v1/platform/plans" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## POST /api/v1/platform/plans

Create a plan.

- Operation id: `PlansController_create_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Request body

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `code` | string | yes | 1–50 chars |
| `name` | string | yes | 1–200 chars |
| `priceMinor` | integer | yes | 0–9007199254740991 |
| `currency` | string | yes | 3–3 chars |
| `billingInterval` | enum: "month" \| "year" | yes |  |
| `limits` | object | yes |  |
| `features` | array<string> | yes |  |

```json
{
  "code": "string",
  "name": "Algebra I",
  "priceMinor": 12000,
  "currency": "USD",
  "billingInterval": "month",
  "limits": {
    "maxStudents": 0,
    "maxBranches": 0,
    "maxVideoCourses": -1,
    "maxLessonsPerCourse": -1,
    "notificationQuota": {
      "whatsapp": 0,
      "sms": 0,
      "email": "ada.lovelace@example.com"
    }
  },
  "features": [
    "string"
  ]
}
```

### Responses

**201** — The newly created plan.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `code` | string | yes |  |
| `name` | string | yes |  |
| `priceMinor` | integer | yes | -9007199254740991–9007199254740991 |
| `currency` | string | yes | 3–3 chars |
| `billingInterval` | enum: "month" \| "year" | yes |  |
| `limits` | PlanLimits | yes |  |
| `features` | array<string> | yes |  |
| `isActive` | boolean | yes |  |
| `createdAt` | string (date-time) | yes |  |
| `updatedAt` | string (date-time) | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/platform/plans" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "code": "string",
    "name": "Algebra I",
    "priceMinor": 12000,
    "currency": "USD",
    "billingInterval": "month",
    "limits": {
      "maxStudents": 0,
      "maxBranches": 0,
      "maxVideoCourses": -1,
      "maxLessonsPerCourse": -1,
      "notificationQuota": {
        "whatsapp": 0,
        "sms": 0,
        "email": "ada.lovelace@example.com"
      }
    },
    "features": [
      "string"
    ]
  }'
```

## PATCH /api/v1/platform/plans/{id}

Update a plan (use `isActive: false` to deactivate — plans are never deleted).

- Operation id: `PlansController_update_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes |  |

### Request body

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `name` | string | no | 1–200 chars |
| `priceMinor` | integer | no | 0–9007199254740991 |
| `currency` | string | no | 3–3 chars |
| `billingInterval` | enum: "month" \| "year" | no |  |
| `limits` | object | no |  |
| `features` | array<string> | no |  |
| `isActive` | boolean | no |  |

```json
{
  "name": "Algebra I",
  "priceMinor": 12000,
  "currency": "USD",
  "billingInterval": "month",
  "limits": {
    "maxStudents": 0,
    "maxBranches": 0,
    "maxVideoCourses": -1,
    "maxLessonsPerCourse": -1,
    "notificationQuota": {
      "whatsapp": 0,
      "sms": 0,
      "email": "ada.lovelace@example.com"
    }
  },
  "features": [
    "string"
  ],
  "isActive": true
}
```

### Responses

**200** — The updated plan.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `code` | string | yes |  |
| `name` | string | yes |  |
| `priceMinor` | integer | yes | -9007199254740991–9007199254740991 |
| `currency` | string | yes | 3–3 chars |
| `billingInterval` | enum: "month" \| "year" | yes |  |
| `limits` | PlanLimits | yes |  |
| `features` | array<string> | yes |  |
| `isActive` | boolean | yes |  |
| `createdAt` | string (date-time) | yes |  |
| `updatedAt` | string (date-time) | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X PATCH "https://your-institute.example/api/v1/platform/plans/37386ae0-3738-7738-8386-37386ae03738" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "name": "Algebra I",
    "priceMinor": 12000,
    "currency": "USD",
    "billingInterval": "month",
    "limits": {
      "maxStudents": 0,
      "maxBranches": 0,
      "maxVideoCourses": -1,
      "maxLessonsPerCourse": -1,
      "notificationQuota": {
        "whatsapp": 0,
        "sms": 0,
        "email": "ada.lovelace@example.com"
      }
    },
    "features": [
      "string"
    ],
    "isActive": true
  }'
```

## GET /api/v1/platform/queues

Job counts (waiting/active/failed/delayed) per BullMQ queue (docs/ARCHITECTURE.md §4.7).

- Operation id: `PlatformQueuesController_counts_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Responses

**200** — Counts per queue.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `maintenance` | QueueJobCounts | yes |  |
| `notifications` | QueueJobCounts | yes |  |
| `webhooks` | QueueJobCounts | yes |  |
| `reports` | QueueJobCounts | yes |  |
| `digest` | QueueJobCounts | yes |  |
| `billing` | QueueJobCounts | yes |  |
| `academics` | QueueJobCounts | yes |  |
| `reminders` | QueueJobCounts | yes |  |
| `relay-system-mail` | QueueJobCounts | yes |  |
| `account-links-sync` | QueueJobCounts | yes |  |
| `support-submissions` | QueueJobCounts | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X GET "https://your-institute.example/api/v1/platform/queues" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## GET /api/v1/platform/registry/token

Docker Registry v2 token endpoint. HTTP Basic credentials (a registry_credentials row, or the vendor release identity); scope/service per the Docker Registry v2 token spec.

- Operation id: `RegistryTokenController_token_v1`
- Permission: —
- Auth: public (no credential)
- `Idempotency-Key`: not required

### Responses

**200** — A signed bearer token.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `token` | string | yes |  |
| `access_token` | string | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X GET "https://your-institute.example/api/v1/platform/registry/token" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## GET /api/v1/platform/relay/account

This install's account snapshot: balance, overdraft, prices in force, install status.

- Operation id: `RelayMessagesController_account_v1`
- Permission: —
- Auth: public (no credential)
- `Idempotency-Key`: not required

### Responses

**200** — The account snapshot.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `balanceUnits` | integer | yes | -9007199254740991–9007199254740991 |
| `overdraftUnits` | integer | yes | -9007199254740991–9007199254740991 |
| `status` | enum: "active" \| "closed" | yes |  |
| `reputationHold` | boolean | yes |  |
| `relayEmailEnabled` | boolean | yes |  |
| `installStatus` | enum: "approved" \| "pending" \| "blocked" | yes |  |
| `prices` | array<object> | yes |  |
| `senderName` | object | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X GET "https://your-institute.example/api/v1/platform/relay/account" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## POST /api/v1/platform/relay/account/sender-name

Submit this install’s requested institute-mode display name for the owner’s one-time impersonation-check approval (§11.3).

- Operation id: `RelayMessagesController_requestSenderName_v1`
- Permission: —
- Auth: public (no credential)
- `Idempotency-Key`: not required

### Request body

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `displayName` | string | yes | 1–70 chars |

```json
{
  "displayName": "string"
}
```

### Responses

**200** — Confirmation — the request now shows as pending.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `ok` | enum: true | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/platform/relay/account/sender-name" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "displayName": "string"
  }'
```

## GET /api/v1/platform/relay/accounts

List relay accounts: balance, overdraft, 30-day spend, and the owning tenant’s name/slug.

- Operation id: `RelayAccountsController_list_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `cursor` | query | string | no |  |
| `limit` | query | integer | no | 1–100 · default: 25 |

### Responses

**200** — A page of relay accounts.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `data` | array<object> | yes |  |
| `nextCursor` | string \| null | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X GET "https://your-institute.example/api/v1/platform/relay/accounts" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## GET /api/v1/platform/relay/accounts/{tenantId}

A single tenant’s relay account, with its 30-day spend.

- Operation id: `RelayAccountsController_get_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `tenantId` | path | string | yes |  |

### Responses

**200** — The relay account.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `tenantId` | string (uuid) | yes |  |
| `balanceUnits` | integer | yes | -9007199254740991–9007199254740991 |
| `overdraftUnits` | integer | yes | -9007199254740991–9007199254740991 |
| `status` | enum: "active" \| "closed" | yes |  |
| `ratePerMinute` | integer \| null | yes | -9007199254740991–9007199254740991 |
| `ratePerDay` | integer \| null | yes | -9007199254740991–9007199254740991 |
| `reputationHold` | boolean | yes |  |
| `reputationHoldReason` | string \| null | yes |  |
| `senderDisplayName` | string \| null | yes |  |
| `senderNameApprovedAt` | string (date-time) \| null | yes |  |
| `senderNameRequestedAt` | string (date-time) \| null | yes |  |
| `createdAt` | string (date-time) | yes |  |
| `updatedAt` | string (date-time) | yes |  |
| `spend30dUnits` | integer | yes | -9007199254740991–9007199254740991 |
| `installCount` | integer | yes | -9007199254740991–9007199254740991 |
| `pendingInstallCount` | integer | yes | -9007199254740991–9007199254740991 |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X GET "https://your-institute.example/api/v1/platform/relay/accounts/4332af8b-4332-7332-832a-4332af8b4332" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## PATCH /api/v1/platform/relay/accounts/{tenantId}

Change a relay account’s overdraft limit, per-channel rate limits or reputation hold. Owner only; requires a step-up MFA check within the last 10 minutes.

- Operation id: `RelayAccountsController_update_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `tenantId` | path | string | yes |  |

### Request body

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `overdraftUnits` | integer | no | 0–9007199254740991 |
| `ratePerMinute` | integer \| null | no | ≤9007199254740991 |
| `ratePerDay` | integer \| null | no | ≤9007199254740991 |
| `reputationHold` | boolean | no |  |
| `reputationHoldReason` | string \| null | no | 1–500 chars |
| `senderNameApproved` | boolean | no |  |

```json
{
  "overdraftUnits": 0,
  "ratePerMinute": 1,
  "ratePerDay": 1,
  "reputationHold": true,
  "reputationHoldReason": "string",
  "senderNameApproved": true
}
```

### Responses

**200** — The updated relay account.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `tenantId` | string (uuid) | yes |  |
| `balanceUnits` | integer | yes | -9007199254740991–9007199254740991 |
| `overdraftUnits` | integer | yes | -9007199254740991–9007199254740991 |
| `status` | enum: "active" \| "closed" | yes |  |
| `ratePerMinute` | integer \| null | yes | -9007199254740991–9007199254740991 |
| `ratePerDay` | integer \| null | yes | -9007199254740991–9007199254740991 |
| `reputationHold` | boolean | yes |  |
| `reputationHoldReason` | string \| null | yes |  |
| `senderDisplayName` | string \| null | yes |  |
| `senderNameApprovedAt` | string (date-time) \| null | yes |  |
| `senderNameRequestedAt` | string (date-time) \| null | yes |  |
| `createdAt` | string (date-time) | yes |  |
| `updatedAt` | string (date-time) | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X PATCH "https://your-institute.example/api/v1/platform/relay/accounts/4332af8b-4332-7332-832a-4332af8b4332" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "overdraftUnits": 0,
    "ratePerMinute": 1,
    "ratePerDay": 1,
    "reputationHold": true,
    "reputationHoldReason": "string",
    "senderNameApproved": true
  }'
```

## POST /api/v1/platform/relay/accounts/{tenantId}/adjustments

Record a manual balance correction (either sign, reason mandatory). Owner only; requires a step-up MFA check within the last 10 minutes.

- Operation id: `RelayAccountsController_adjust_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `tenantId` | path | string | yes |  |

### Request body

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `amountUnits` | integer | yes | -9007199254740991–9007199254740991 |
| `reason` | string | yes | 1–500 chars |
| `note` | string | no | 0–2000 chars |

```json
{
  "amountUnits": -9007199254740991,
  "reason": "Requested by guardian.",
  "note": "string"
}
```

### Responses

**201** — The adjustment ledger entry.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `accountId` | string (uuid) | yes |  |
| `kind` | enum: "topup" \| "debit" \| "refund" \| "adjustment" | yes |  |
| `amountUnits` | integer | yes | -9007199254740991–9007199254740991 |
| `balanceAfterUnits` | integer | yes | -9007199254740991–9007199254740991 |
| `messageId` | string (uuid) \| null | yes |  |
| `priceId` | string (uuid) \| null | yes |  |
| `reason` | string \| null | yes |  |
| `note` | string \| null | yes |  |
| `actorUserId` | string (uuid) \| null | yes |  |
| `createdAt` | string (date-time) | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/platform/relay/accounts/4332af8b-4332-7332-832a-4332af8b4332/adjustments" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "amountUnits": -9007199254740991,
    "reason": "Requested by guardian.",
    "note": "string"
  }'
```

## GET /api/v1/platform/relay/accounts/{tenantId}/ledger

The tenant’s relay ledger, newest first.

- Operation id: `RelayAccountsController_ledger_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `tenantId` | path | string | yes |  |
| `cursor` | query | string | no |  |
| `limit` | query | integer | no | 1–100 · default: 25 |

### Responses

**200** — A page of ledger entries.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `data` | array<object> | yes |  |
| `nextCursor` | string \| null | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X GET "https://your-institute.example/api/v1/platform/relay/accounts/4332af8b-4332-7332-832a-4332af8b4332/ledger" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## POST /api/v1/platform/relay/accounts/{tenantId}/sender-name/approve

Approve this account’s pending sender-name request, after the owner’s own impersonation check. The body must carry the exact displayName the console rendered — 409 CONFLICT if the install changed it since (TOCTOU guard, H3). Owner only, audited. Requires a step-up MFA check within the last 10 minutes.

- Operation id: `RelayAccountsController_approveSenderName_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `tenantId` | path | string | yes |  |

### Request body

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `displayName` | string | yes | 1–70 chars |

```json
{
  "displayName": "string"
}
```

### Responses

**200** — The updated relay account.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `tenantId` | string (uuid) | yes |  |
| `balanceUnits` | integer | yes | -9007199254740991–9007199254740991 |
| `overdraftUnits` | integer | yes | -9007199254740991–9007199254740991 |
| `status` | enum: "active" \| "closed" | yes |  |
| `ratePerMinute` | integer \| null | yes | -9007199254740991–9007199254740991 |
| `ratePerDay` | integer \| null | yes | -9007199254740991–9007199254740991 |
| `reputationHold` | boolean | yes |  |
| `reputationHoldReason` | string \| null | yes |  |
| `senderDisplayName` | string \| null | yes |  |
| `senderNameApprovedAt` | string (date-time) \| null | yes |  |
| `senderNameRequestedAt` | string (date-time) \| null | yes |  |
| `createdAt` | string (date-time) | yes |  |
| `updatedAt` | string (date-time) | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/platform/relay/accounts/4332af8b-4332-7332-832a-4332af8b4332/sender-name/approve" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "displayName": "string"
  }'
```

## POST /api/v1/platform/relay/accounts/{tenantId}/sender-name/reject

Reject this account’s pending sender-name request — clears the requested name so the institute goes back to "none". The body must carry the exact displayName the console rendered — 409 CONFLICT if the install changed it since (TOCTOU guard, H3). Owner only, audited. Requires a step-up MFA check within the last 10 minutes.

- Operation id: `RelayAccountsController_rejectSenderName_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `tenantId` | path | string | yes |  |

### Request body

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `displayName` | string | yes | 1–70 chars |

```json
{
  "displayName": "string"
}
```

### Responses

**200** — The updated relay account.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `tenantId` | string (uuid) | yes |  |
| `balanceUnits` | integer | yes | -9007199254740991–9007199254740991 |
| `overdraftUnits` | integer | yes | -9007199254740991–9007199254740991 |
| `status` | enum: "active" \| "closed" | yes |  |
| `ratePerMinute` | integer \| null | yes | -9007199254740991–9007199254740991 |
| `ratePerDay` | integer \| null | yes | -9007199254740991–9007199254740991 |
| `reputationHold` | boolean | yes |  |
| `reputationHoldReason` | string \| null | yes |  |
| `senderDisplayName` | string \| null | yes |  |
| `senderNameApprovedAt` | string (date-time) \| null | yes |  |
| `senderNameRequestedAt` | string (date-time) \| null | yes |  |
| `createdAt` | string (date-time) | yes |  |
| `updatedAt` | string (date-time) | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/platform/relay/accounts/4332af8b-4332-7332-832a-4332af8b4332/sender-name/reject" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "displayName": "string"
  }'
```

## GET /api/v1/platform/relay/accounts/{tenantId}/suppressions

The tenant’s suppressed recipients (hashed, never readable), newest first.

- Operation id: `RelayAccountsController_listSuppressions_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `tenantId` | path | string | yes |  |
| `cursor` | query | string | no |  |
| `limit` | query | integer | no | 1–100 · default: 25 |

### Responses

**200** — A page of suppressions.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `data` | array<object> | yes |  |
| `nextCursor` | string \| null | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X GET "https://your-institute.example/api/v1/platform/relay/accounts/4332af8b-4332-7332-832a-4332af8b4332/suppressions" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## DELETE /api/v1/platform/relay/accounts/{tenantId}/suppressions/{id}

Lift a suppression so the relay may send to this recipient again. Owner only, audited. Requires a step-up MFA check within the last 10 minutes.

- Operation id: `RelayAccountsController_deleteSuppression_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `tenantId` | path | string | yes |  |
| `id` | path | string | yes |  |

### Responses

**200** — Confirmation.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `ok` | enum: true | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X DELETE "https://your-institute.example/api/v1/platform/relay/accounts/4332af8b-4332-7332-832a-4332af8b4332/suppressions/37386ae0-3738-7738-8386-37386ae03738" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## POST /api/v1/platform/relay/accounts/{tenantId}/top-ups

Record a hand-received top-up (e.g. cash) to a tenant’s relay balance. Owner only; not idempotent by design — a repeated call records a second row (§11.4). Requires a step-up MFA check within the last 10 minutes.

- Operation id: `RelayAccountsController_topUp_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `tenantId` | path | string | yes |  |

### Request body

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `amountUnits` | integer | yes | 0–9007199254740991 |
| `reason` | string | yes | 1–500 chars |
| `note` | string | no | 0–2000 chars |

```json
{
  "amountUnits": 0,
  "reason": "Requested by guardian.",
  "note": "string"
}
```

### Responses

**201** — The top-up ledger entry.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `accountId` | string (uuid) | yes |  |
| `kind` | enum: "topup" \| "debit" \| "refund" \| "adjustment" | yes |  |
| `amountUnits` | integer | yes | -9007199254740991–9007199254740991 |
| `balanceAfterUnits` | integer | yes | -9007199254740991–9007199254740991 |
| `messageId` | string (uuid) \| null | yes |  |
| `priceId` | string (uuid) \| null | yes |  |
| `reason` | string \| null | yes |  |
| `note` | string \| null | yes |  |
| `actorUserId` | string (uuid) \| null | yes |  |
| `createdAt` | string (date-time) | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/platform/relay/accounts/4332af8b-4332-7332-832a-4332af8b4332/top-ups" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "amountUnits": 0,
    "reason": "Requested by guardian.",
    "note": "string"
  }'
```

## GET /api/v1/platform/relay/installs

List relay installs, newest first — every account, or one account with ?accountId= (K-41).

- Operation id: `RelayInstallsController_list_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `cursor` | query | string | no |  |
| `limit` | query | integer | no | 1–100 · default: 25 |
| `accountId` | query | string (uuid) | no |  |

### Responses

**200** — A page of installs.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `data` | array<object> | yes |  |
| `nextCursor` | string \| null | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X GET "https://your-institute.example/api/v1/platform/relay/installs" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## POST /api/v1/platform/relay/installs

Enrol this install with a licence-proven identity. The first install of an account is approved automatically; every further one is pending until the owner approves it.

- Operation id: `RelayInstallsController_enrol_v1`
- Permission: —
- Auth: public (no credential)
- `Idempotency-Key`: not required

### Request body

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `licenseKey` | string | yes | ≥1 chars |
| `installId` | string | yes | 1–200 chars |
| `publicKey` | string | yes | 1–4000 chars |
| `baseUrl` | string | yes |  |
| `label` | string \| null | no | 0–200 chars |

```json
{
  "licenseKey": "string",
  "installId": "2472867d-2472-7472-8728-2472867d2472",
  "publicKey": "string",
  "baseUrl": "string",
  "label": "string"
}
```

### Responses

**200** — The enrolled (or already-enrolled) install.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `accountId` | string (uuid) | yes |  |
| `installId` | string | yes |  |
| `status` | enum: "approved" \| "pending" \| "blocked" | yes |  |
| `label` | string \| null | yes |  |
| `baseUrl` | string | yes |  |
| `lastSeenAt` | string (date-time) \| null | yes |  |
| `keyReplacedAt` | string (date-time) \| null | yes |  |
| `updaterKeyBoundAt` | string (date-time) \| null | yes |  |
| `createdAt` | string (date-time) | yes |  |
| `updatedAt` | string (date-time) | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/platform/relay/installs" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "licenseKey": "string",
    "installId": "2472867d-2472-7472-8728-2472867d2472",
    "publicKey": "string",
    "baseUrl": "string",
    "label": "string"
  }'
```

## POST /api/v1/platform/relay/installs/{id}/approve

Approve a pending install so it may start spending. Owner only, audited.

- Operation id: `RelayInstallsController_approve_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes |  |

### Responses

**200** — The approved install.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `accountId` | string (uuid) | yes |  |
| `installId` | string | yes |  |
| `status` | enum: "approved" \| "pending" \| "blocked" | yes |  |
| `label` | string \| null | yes |  |
| `baseUrl` | string | yes |  |
| `lastSeenAt` | string (date-time) \| null | yes |  |
| `keyReplacedAt` | string (date-time) \| null | yes |  |
| `updaterKeyBoundAt` | string (date-time) \| null | yes |  |
| `createdAt` | string (date-time) | yes |  |
| `updatedAt` | string (date-time) | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/platform/relay/installs/37386ae0-3738-7738-8386-37386ae03738/approve" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## POST /api/v1/platform/relay/installs/{id}/block

Block an install — approved or pending — so it can never spend again. Owner only, audited.

- Operation id: `RelayInstallsController_block_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes |  |

### Responses

**200** — The blocked install.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `accountId` | string (uuid) | yes |  |
| `installId` | string | yes |  |
| `status` | enum: "approved" \| "pending" \| "blocked" | yes |  |
| `label` | string \| null | yes |  |
| `baseUrl` | string | yes |  |
| `lastSeenAt` | string (date-time) \| null | yes |  |
| `keyReplacedAt` | string (date-time) \| null | yes |  |
| `updaterKeyBoundAt` | string (date-time) \| null | yes |  |
| `createdAt` | string (date-time) | yes |  |
| `updatedAt` | string (date-time) | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/platform/relay/installs/37386ae0-3738-7738-8386-37386ae03738/block" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## POST /api/v1/platform/relay/installs/updater-key

Bind (or re-bind) the key this install’s updater generated, so the updater can sign fault reports while the API is down. Signed with the install’s own relay identity — never with the updater key itself.

- Operation id: `RelayInstallsController_bindUpdaterKey_v1`
- Permission: —
- Auth: public (no credential)
- `Idempotency-Key`: not required

### Request body

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `publicKey` | string | yes | 1–4000 chars |

```json
{
  "publicKey": "string"
}
```

### Responses

**200** — The install, with the time its updater key was bound.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `accountId` | string (uuid) | yes |  |
| `installId` | string | yes |  |
| `status` | enum: "approved" \| "pending" \| "blocked" | yes |  |
| `label` | string \| null | yes |  |
| `baseUrl` | string | yes |  |
| `lastSeenAt` | string (date-time) \| null | yes |  |
| `keyReplacedAt` | string (date-time) \| null | yes |  |
| `updaterKeyBoundAt` | string (date-time) \| null | yes |  |
| `createdAt` | string (date-time) | yes |  |
| `updatedAt` | string (date-time) | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/platform/relay/installs/updater-key" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "publicKey": "string"
  }'
```

## GET /api/v1/platform/relay/messages

This install's account's relay messages, newest first — or, with ?since=, only those updated since then (K-40, for status-mirroring polls).

- Operation id: `RelayMessagesController_list_v1`
- Permission: —
- Auth: public (no credential)
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `cursor` | query | string | no |  |
| `limit` | query | integer | no | 1–100 · default: 25 |
| `since` | query | string (date-time) | no |  |

### Responses

**200** — A page of relay messages.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `data` | array<object> | yes |  |
| `nextCursor` | string \| null | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X GET "https://your-institute.example/api/v1/platform/relay/messages" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## POST /api/v1/platform/relay/messages

Send a relay email — system (free, vendor-template only) or institute (paid, priced and debited). Idempotent on idempotencyKey.

- Operation id: `RelayMessagesController_send_v1`
- Permission: —
- Auth: public (no credential)
- `Idempotency-Key`: not required

### Request body

```json
"value"
```

### Responses

**200** — The relay message record.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `messageClass` | enum: "system" \| "institute" | yes |  |
| `templateId` | string \| null | yes |  |
| `channel` | enum: "email" \| "sms" \| "push" | yes |  |
| `senderMode` | enum: "platform" \| "institute" | yes |  |
| `status` | enum: "accepted" \| "sending" \| "sent" \| "delivered" \| "bounced" \| "complained" \| "failed" \| "suppressed" | yes |  |
| `costUnits` | integer | yes | -9007199254740991–9007199254740991 |
| `createdAt` | string (date-time) | yes |  |
| `updatedAt` | string (date-time) | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/platform/relay/messages" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '"value"'
```

## GET /api/v1/platform/relay/messages/{id}

A single relay message this install's account owns.

- Operation id: `RelayMessagesController_getById_v1`
- Permission: —
- Auth: public (no credential)
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes |  |

### Responses

**200** — The relay message record.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `messageClass` | enum: "system" \| "institute" | yes |  |
| `templateId` | string \| null | yes |  |
| `channel` | enum: "email" \| "sms" \| "push" | yes |  |
| `senderMode` | enum: "platform" \| "institute" | yes |  |
| `status` | enum: "accepted" \| "sending" \| "sent" \| "delivered" \| "bounced" \| "complained" \| "failed" \| "suppressed" | yes |  |
| `costUnits` | integer | yes | -9007199254740991–9007199254740991 |
| `createdAt` | string (date-time) | yes |  |
| `updatedAt` | string (date-time) | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X GET "https://your-institute.example/api/v1/platform/relay/messages/37386ae0-3738-7738-8386-37386ae03738" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## GET /api/v1/platform/relay/prices

The relay price list, newest (effective-from) first.

- Operation id: `RelayPricesController_list_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `cursor` | query | string | no |  |
| `limit` | query | integer | no | 1–100 · default: 25 |

### Responses

**200** — A page of relay prices.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `data` | array<object> | yes |  |
| `nextCursor` | string \| null | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X GET "https://your-institute.example/api/v1/platform/relay/prices" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## POST /api/v1/platform/relay/prices

Add a new price row, effective immediately or from a future instant. Owner only — prices are never updated or deleted, so a change is always a new row.

- Operation id: `RelayPricesController_create_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Request body

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `channel` | enum: "email" \| "sms" \| "push" | yes |  |
| `country` | string | yes | 2–2 chars |
| `senderMode` | enum: "platform" \| "institute" | yes |  |
| `unit` | enum: "email_message" \| "sms_segment" \| "push_message" | yes |  |
| `priceUnits` | integer | yes | 0–9007199254740991 |
| `providerCostUnits` | integer | yes | 0–9007199254740991 |
| `effectiveFrom` | string (date-time) | no |  |

```json
{
  "channel": "email",
  "country": "string",
  "senderMode": "platform",
  "unit": "email_message",
  "priceUnits": 0,
  "providerCostUnits": 0,
  "effectiveFrom": "2026-03-02T09:00:00.000Z"
}
```

### Responses

**201** — The newly added price row.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `channel` | enum: "email" \| "sms" \| "push" | yes |  |
| `country` | string | yes | 2–2 chars |
| `senderMode` | enum: "platform" \| "institute" | yes |  |
| `unit` | enum: "email_message" \| "sms_segment" \| "push_message" | yes |  |
| `priceUnits` | integer | yes | -9007199254740991–9007199254740991 |
| `providerCostUnits` | integer | yes | -9007199254740991–9007199254740991 |
| `effectiveFrom` | string (date-time) | yes |  |
| `createdByUserId` | string (uuid) | yes |  |
| `createdAt` | string (date-time) | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/platform/relay/prices" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "channel": "email",
    "country": "string",
    "senderMode": "platform",
    "unit": "email_message",
    "priceUnits": 0,
    "providerCostUnits": 0,
    "effectiveFrom": "2026-03-02T09:00:00.000Z"
  }'
```

## POST /api/v1/platform/relay/webhooks/resend

Resend's bounce/complaint/delivered webhook. Svix-signature verified, idempotent.

- Operation id: `RelayWebhooksController_resend_v1`
- Permission: —
- Auth: public (no credential)
- `Idempotency-Key`: not required

### Responses

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/platform/relay/webhooks/resend" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## GET /api/v1/platform/releases

Published releases, newest first.

- Operation id: `ReleasesController_list_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `cursor` | query | string | no |  |
| `limit` | query | integer | no | 1–100 · default: 25 |
| `channel` | query | enum: "stable" \| "beta" | no |  |

### Responses

**200** — A page of releases.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `data` | array<object> | yes |  |
| `nextCursor` | string \| null | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X GET "https://your-institute.example/api/v1/platform/releases" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## POST /api/v1/platform/releases

Publish a built release (images pinned by digest). Defaults to the beta channel — promotion to stable is a separate, manual step.

- Operation id: `ReleasesController_publish_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Request body

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `channel` | enum: "stable" \| "beta" | no | default: "beta" |
| `version` | string | yes |  |
| `images` | object | yes |  |
| `desiredState` | object | no |  |
| `minFromVersion` | string | no |  |
| `notes` | string \| null | no | 1–5000 chars · default: null |
| `releasedAt` | string (date-time) | no |  |

```json
{
  "channel": "beta",
  "version": "string",
  "images": {
    "api": {
      "ref": "string",
      "digest": "string"
    },
    "web": {
      "ref": "string",
      "digest": "string"
    },
    "backup": {
      "ref": "string",
      "digest": "string"
    },
    "updater": {
      "ref": "string",
      "digest": "string"
    }
  },
  "desiredState": {
    "sha256": "string",
    "updaterProtocol": 1
  },
  "minFromVersion": "string",
  "notes": null,
  "releasedAt": "2026-03-02T09:00:00.000Z"
}
```

### Responses

**201** — The published release.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `channel` | enum: "stable" \| "beta" | yes |  |
| `version` | string | yes |  |
| `images` | object | yes |  |
| `desiredStateSha256` | string \| null | yes |  |
| `repairMode` | enum: "repair" \| "report" | yes |  |
| `minFromVersion` | string | yes |  |
| `notes` | string \| null | yes |  |
| `releasedAt` | string (date-time) | yes |  |
| `promotedAt` | string (date-time) \| null | yes |  |
| `warnings` | array<enum: "v1_only_newer_than_v2"> | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/platform/releases" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "channel": "beta",
    "version": "string",
    "images": {
      "api": {
        "ref": "string",
        "digest": "string"
      },
      "web": {
        "ref": "string",
        "digest": "string"
      },
      "backup": {
        "ref": "string",
        "digest": "string"
      },
      "updater": {
        "ref": "string",
        "digest": "string"
      }
    },
    "desiredState": {
      "sha256": "string",
      "updaterProtocol": 1
    },
    "minFromVersion": "string",
    "notes": null,
    "releasedAt": "2026-03-02T09:00:00.000Z"
  }'
```

## POST /api/v1/platform/releases/{releaseId}/promote

Promote a beta release to the stable channel. Platform owner only — this is what reaches every unattended install. Requires a step-up MFA check within the last 10 minutes.

- Operation id: `ReleasesController_promote_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `releaseId` | path | string | yes |  |

### Responses

**200** — The promoted release.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `channel` | enum: "stable" \| "beta" | yes |  |
| `version` | string | yes |  |
| `images` | object | yes |  |
| `desiredStateSha256` | string \| null | yes |  |
| `repairMode` | enum: "repair" \| "report" | yes |  |
| `minFromVersion` | string | yes |  |
| `notes` | string \| null | yes |  |
| `releasedAt` | string (date-time) | yes |  |
| `promotedAt` | string (date-time) \| null | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/platform/releases/97e23b63-97e2-77e2-8e23-97e23b6397e2/promote" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## POST /api/v1/platform/releases/{releaseId}/repair-mode

Set a release's repair mode — the vendor kill switch. `report` makes every install on this release only report damage instead of repairing it. Platform owner only; refused (409) for a release without a v2 manifest. Requires a step-up MFA check within the last 10 minutes.

- Operation id: `ReleasesController_setRepairMode_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `releaseId` | path | string | yes |  |

### Request body

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `repairMode` | enum: "repair" \| "report" | yes |  |

```json
{
  "repairMode": "repair"
}
```

### Responses

**200** — The release with its new repair mode.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `channel` | enum: "stable" \| "beta" | yes |  |
| `version` | string | yes |  |
| `images` | object | yes |  |
| `desiredStateSha256` | string \| null | yes |  |
| `repairMode` | enum: "repair" \| "report" | yes |  |
| `minFromVersion` | string | yes |  |
| `notes` | string \| null | yes |  |
| `releasedAt` | string (date-time) | yes |  |
| `promotedAt` | string (date-time) \| null | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/platform/releases/97e23b63-97e2-77e2-8e23-97e23b6397e2/repair-mode" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "repairMode": "repair"
  }'
```

## POST /api/v1/platform/releases/ci

Publish a release to the beta channel from CI, authenticated by a GitHub Actions OIDC token (no stored secret, see docs/specs/phase11-vendor-console.md §2.5).

- Operation id: `ReleasesController_publishFromCi_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Request body

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `version` | string | yes |  |
| `images` | object | yes |  |
| `desiredState` | object | no |  |
| `minFromVersion` | string | no |  |
| `notes` | string \| null | no | 1–5000 chars · default: null |
| `releasedAt` | string (date-time) | no |  |

```json
{
  "version": "string",
  "images": {
    "api": {
      "ref": "string",
      "digest": "string"
    },
    "web": {
      "ref": "string",
      "digest": "string"
    },
    "backup": {
      "ref": "string",
      "digest": "string"
    },
    "updater": {
      "ref": "string",
      "digest": "string"
    }
  },
  "desiredState": {
    "sha256": "string",
    "updaterProtocol": 1
  },
  "minFromVersion": "string",
  "notes": null,
  "releasedAt": "2026-03-02T09:00:00.000Z"
}
```

### Responses

**201** — The published release.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `channel` | enum: "stable" \| "beta" | yes |  |
| `version` | string | yes |  |
| `images` | object | yes |  |
| `desiredStateSha256` | string \| null | yes |  |
| `repairMode` | enum: "repair" \| "report" | yes |  |
| `minFromVersion` | string | yes |  |
| `notes` | string \| null | yes |  |
| `releasedAt` | string (date-time) | yes |  |
| `promotedAt` | string (date-time) \| null | yes |  |
| `warnings` | array<enum: "v1_only_newer_than_v2"> | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/platform/releases/ci" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "version": "string",
    "images": {
      "api": {
        "ref": "string",
        "digest": "string"
      },
      "web": {
        "ref": "string",
        "digest": "string"
      },
      "backup": {
        "ref": "string",
        "digest": "string"
      },
      "updater": {
        "ref": "string",
        "digest": "string"
      }
    },
    "desiredState": {
      "sha256": "string",
      "updaterProtocol": 1
    },
    "minFromVersion": "string",
    "notes": null,
    "releasedAt": "2026-03-02T09:00:00.000Z"
  }'
```

## GET /api/v1/platform/settings

Platform-wide settings: the global auto-lock toggle and its delay.

- Operation id: `PlatformSettingsController_get_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Responses

**200** — The platform settings.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `autoLockEnabled` | boolean | yes |  |
| `autoLockAfterDays` | integer | yes | 0–9007199254740991 |
| `createdAt` | string (date-time) | yes |  |
| `updatedAt` | string (date-time) | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X GET "https://your-institute.example/api/v1/platform/settings" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## PATCH /api/v1/platform/settings

Update the global auto-lock policy. With auto-lock off (the default) an expired tenant is never locked automatically. Requires a step-up MFA check within the last 10 minutes.

- Operation id: `PlatformSettingsController_patch_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Request body

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `autoLockEnabled` | boolean | no |  |
| `autoLockAfterDays` | integer | no | 0–365 |

```json
{
  "autoLockEnabled": true,
  "autoLockAfterDays": 0
}
```

### Responses

**200** — The updated platform settings.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `autoLockEnabled` | boolean | yes |  |
| `autoLockAfterDays` | integer | yes | 0–9007199254740991 |
| `createdAt` | string (date-time) | yes |  |
| `updatedAt` | string (date-time) | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X PATCH "https://your-institute.example/api/v1/platform/settings" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "autoLockEnabled": true,
    "autoLockAfterDays": 0
  }'
```

## POST /api/v1/platform/support/faults

An automatic fault report (closed schema, no free text), signed by the install’s updater sub-key or by the install. A repeat of an open fault only raises its count.

- Operation id: `SupportIntakeController_submitFault_v1`
- Permission: —
- Auth: public (no credential)
- `Idempotency-Key`: not required

### Request body

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `code` | enum: "update_rolled_back" \| "update_stuck" \| "rollback_failed" \| "post_update_drift" \| "repair_applied" \| "file_unknown_modification" \| "env_missing_unrecoverable" \| "repair_loop" \| "backup_failed" \| "backup_missing" \| "backup_required_failed" \| "offsite_upload_failed" \| "offsite_quota_exceeded" \| "disk_low" \| "docker_daemon_down" \| "service_unhealthy" \| "registry_auth_failed" \| "registry_unreachable" \| "certificate_invalid" \| "guardian_restored" \| "guardian_tampered" \| "updater_tampered" \| "updater_crashloop" \| "pin_unverifiable" \| "manifest_bad_signature" \| "digest_mismatch" \| "desired_state_mismatch" \| "protocol_unsupported" \| "install_down" \| "repair_suppressed" \| "service_drift_unrecoverable" | yes |  |
| `severity` | enum: "info" \| "warning" \| "critical" | yes |  |
| `occurredAt` | string (date-time) | yes |  |
| `firstSeenAt` | string (date-time) | yes |  |
| `count` | integer | yes | 1–1000000 |
| `versions` | object | yes |  |
| `step` | enum: "manifest" \| "preflight" \| "pull" \| "backup" \| "snapshot" \| "apply" \| "verify" \| "commit" \| "rollback" \| "converge" \| "guardian" \| "report" \| "adoption" \| null \| null | yes |  |
| `subject` | object \| null | yes |  |
| `errorClass` | enum: "timeout" \| "permission_denied" \| "not_found" \| "auth_refused" \| "network_unreachable" \| "exit_nonzero" \| "health_timeout" \| "signature_invalid" \| "digest_mismatch" \| "out_of_space" \| "unknown" \| null \| null | yes |  |
| `exitCode` | integer \| null | yes | -255–255 |
| `metrics` | object \| null | yes |  |
| `backupSetId` | string \| null | yes |  |
| `manifestSha256` | string \| null | yes |  |

```json
{
  "code": "update_rolled_back",
  "severity": "info",
  "occurredAt": "2026-03-02T09:00:00.000Z",
  "firstSeenAt": "2026-03-02T09:00:00.000Z",
  "count": 1,
  "versions": {
    "app": "string",
    "updater": "string",
    "guardian": 0
  },
  "step": "manifest",
  "subject": {
    "service": "postgres",
    "path": "docker-compose.yml",
    "envKey": "POSTGRES_PASSWORD"
  },
  "errorClass": "timeout",
  "exitCode": -255,
  "metrics": {
    "diskFreeBytes": 0,
    "diskTotalBytes": 0,
    "setSizeBytes": 0,
    "durationSec": 0
  },
  "backupSetId": "c997a5a0-c997-7997-897a-c997a5a0c997",
  "manifestSha256": "string"
}
```

### Responses

**200** — The stored (or coalesced) report.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `status` | enum: "new" \| "acknowledged" \| "in_progress" \| "resolved" \| "declined" \| "duplicate" | yes |  |
| `created` | boolean | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/platform/support/faults" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "code": "update_rolled_back",
    "severity": "info",
    "occurredAt": "2026-03-02T09:00:00.000Z",
    "firstSeenAt": "2026-03-02T09:00:00.000Z",
    "count": 1,
    "versions": {
      "app": "string",
      "updater": "string",
      "guardian": 0
    },
    "step": "manifest",
    "subject": {
      "service": "postgres",
      "path": "docker-compose.yml",
      "envKey": "POSTGRES_PASSWORD"
    },
    "errorClass": "timeout",
    "exitCode": -255,
    "metrics": {
      "diskFreeBytes": 0,
      "diskTotalBytes": 0,
      "setSizeBytes": 0,
      "durationSec": 0
    },
    "backupSetId": "c997a5a0-c997-7997-897a-c997a5a0c997",
    "manifestSha256": "string"
  }'
```

## POST /api/v1/platform/support/faults/unverified

The open intake for a fault when the sender could not be verified. Stored as unverified, never changes any install, at most 16 KiB, 5 per hour per address and 500 a day in all.

- Operation id: `SupportIntakeController_submitUnverifiedFault_v1`
- Permission: —
- Auth: public (no credential)
- `Idempotency-Key`: not required

### Request body

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `code` | enum: "update_rolled_back" \| "update_stuck" \| "rollback_failed" \| "post_update_drift" \| "repair_applied" \| "file_unknown_modification" \| "env_missing_unrecoverable" \| "repair_loop" \| "backup_failed" \| "backup_missing" \| "backup_required_failed" \| "offsite_upload_failed" \| "offsite_quota_exceeded" \| "disk_low" \| "docker_daemon_down" \| "service_unhealthy" \| "registry_auth_failed" \| "registry_unreachable" \| "certificate_invalid" \| "guardian_restored" \| "guardian_tampered" \| "updater_tampered" \| "updater_crashloop" \| "pin_unverifiable" \| "manifest_bad_signature" \| "digest_mismatch" \| "desired_state_mismatch" \| "protocol_unsupported" \| "install_down" \| "repair_suppressed" \| "service_drift_unrecoverable" | yes |  |
| `severity` | enum: "info" \| "warning" \| "critical" | yes |  |
| `occurredAt` | string (date-time) | yes |  |
| `firstSeenAt` | string (date-time) | yes |  |
| `count` | integer | yes | 1–1000000 |
| `versions` | object | yes |  |
| `step` | enum: "manifest" \| "preflight" \| "pull" \| "backup" \| "snapshot" \| "apply" \| "verify" \| "commit" \| "rollback" \| "converge" \| "guardian" \| "report" \| "adoption" \| null \| null | yes |  |
| `subject` | object \| null | yes |  |
| `errorClass` | enum: "timeout" \| "permission_denied" \| "not_found" \| "auth_refused" \| "network_unreachable" \| "exit_nonzero" \| "health_timeout" \| "signature_invalid" \| "digest_mismatch" \| "out_of_space" \| "unknown" \| null \| null | yes |  |
| `exitCode` | integer \| null | yes | -255–255 |
| `metrics` | object \| null | yes |  |
| `backupSetId` | string \| null | yes |  |
| `manifestSha256` | string \| null | yes |  |
| `claimedInstallId` | string (uuid) | yes |  |

```json
{
  "code": "update_rolled_back",
  "severity": "info",
  "occurredAt": "2026-03-02T09:00:00.000Z",
  "firstSeenAt": "2026-03-02T09:00:00.000Z",
  "count": 1,
  "versions": {
    "app": "string",
    "updater": "string",
    "guardian": 0
  },
  "step": "manifest",
  "subject": {
    "service": "postgres",
    "path": "docker-compose.yml",
    "envKey": "POSTGRES_PASSWORD"
  },
  "errorClass": "timeout",
  "exitCode": -255,
  "metrics": {
    "diskFreeBytes": 0,
    "diskTotalBytes": 0,
    "setSizeBytes": 0,
    "durationSec": 0
  },
  "backupSetId": "c997a5a0-c997-7997-897a-c997a5a0c997",
  "manifestSha256": "string",
  "claimedInstallId": "6c98b290-6c98-7c98-898b-6c98b2906c98"
}
```

### Responses

**200** — The stored (or coalesced) unverified report.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `status` | enum: "new" \| "acknowledged" \| "in_progress" \| "resolved" \| "declined" \| "duplicate" | yes |  |
| `created` | boolean | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/platform/support/faults/unverified" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "code": "update_rolled_back",
    "severity": "info",
    "occurredAt": "2026-03-02T09:00:00.000Z",
    "firstSeenAt": "2026-03-02T09:00:00.000Z",
    "count": 1,
    "versions": {
      "app": "string",
      "updater": "string",
      "guardian": 0
    },
    "step": "manifest",
    "subject": {
      "service": "postgres",
      "path": "docker-compose.yml",
      "envKey": "POSTGRES_PASSWORD"
    },
    "errorClass": "timeout",
    "exitCode": -255,
    "metrics": {
      "diskFreeBytes": 0,
      "diskTotalBytes": 0,
      "setSizeBytes": 0,
      "durationSec": 0
    },
    "backupSetId": "c997a5a0-c997-7997-897a-c997a5a0c997",
    "manifestSha256": "string",
    "claimedInstallId": "6c98b290-6c98-7c98-898b-6c98b2906c98"
  }'
```

## GET /api/v1/platform/support/reports

List faults, problems and feature requests, newest first — filter by kind, status, verification, severity, code or customer.

- Operation id: `SupportReportsController_list_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `cursor` | query | string | no |  |
| `limit` | query | integer | no | 1–100 · default: 25 |
| `kind` | query | enum: "fault" \| "problem" \| "feature" | no |  |
| `status` | query | enum: "new" \| "acknowledged" \| "in_progress" \| "resolved" \| "declined" \| "duplicate" | no |  |
| `verification` | query | enum: "updater_signed" \| "install_signed" \| "unverified" | no |  |
| `verified` | query | enum: "true" \| "false" | no |  |
| `severity` | query | enum: "info" \| "warning" \| "critical" | no |  |
| `code` | query | enum: "update_rolled_back" \| "update_stuck" \| "rollback_failed" \| "post_update_drift" \| "repair_applied" \| "file_unknown_modification" \| "env_missing_unrecoverable" \| "repair_loop" \| "backup_failed" \| "backup_missing" \| "backup_required_failed" \| "offsite_upload_failed" \| "offsite_quota_exceeded" \| "disk_low" \| "docker_daemon_down" \| "service_unhealthy" \| "registry_auth_failed" \| "registry_unreachable" \| "certificate_invalid" \| "guardian_restored" \| "guardian_tampered" \| "updater_tampered" \| "updater_crashloop" \| "pin_unverifiable" \| "manifest_bad_signature" \| "digest_mismatch" \| "desired_state_mismatch" \| "protocol_unsupported" \| "install_down" \| "repair_suppressed" \| "service_drift_unrecoverable" | no |  |
| `tenantId` | query | string (uuid) | no |  |

### Responses

**200** — A page of reports.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `data` | array<object> | yes |  |
| `nextCursor` | string \| null | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X GET "https://your-institute.example/api/v1/platform/support/reports" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## GET /api/v1/platform/support/reports/{id}

One report with its structured fields, the owner’s text and the vendor’s replies.

- Operation id: `SupportReportsController_detail_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes |  |

### Responses

**200** — The report.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `kind` | enum: "fault" \| "problem" \| "feature" | yes |  |
| `source` | enum: "updater" \| "guardian" \| "install_ui" \| "unverified_intake" | yes |  |
| `verification` | enum: "updater_signed" \| "install_signed" \| "unverified" | yes |  |
| `status` | enum: "new" \| "acknowledged" \| "in_progress" \| "resolved" \| "declined" \| "duplicate" | yes |  |
| `severity` | enum: "info" \| "warning" \| "critical" \| null \| null | yes |  |
| `code` | enum: "update_rolled_back" \| "update_stuck" \| "rollback_failed" \| "post_update_drift" \| "repair_applied" \| "file_unknown_modification" \| "env_missing_unrecoverable" \| "repair_loop" \| "backup_failed" \| "backup_missing" \| "backup_required_failed" \| "offsite_upload_failed" \| "offsite_quota_exceeded" \| "disk_low" \| "docker_daemon_down" \| "service_unhealthy" \| "registry_auth_failed" \| "registry_unreachable" \| "certificate_invalid" \| "guardian_restored" \| "guardian_tampered" \| "updater_tampered" \| "updater_crashloop" \| "pin_unverifiable" \| "manifest_bad_signature" \| "digest_mismatch" \| "desired_state_mismatch" \| "protocol_unsupported" \| "install_down" \| "repair_suppressed" \| "service_drift_unrecoverable" \| null \| null | yes |  |
| `category` | enum: "login_access" \| "people" \| "attendance" \| "billing" \| "courses_content" \| "notifications" \| "updates_backups" \| "performance" \| "other" \| null \| null | yes |  |
| `title` | string \| null | yes |  |
| `tenant` | object \| null | yes |  |
| `installId` | string \| null | yes |  |
| `claimedInstallId` | string (uuid) \| null | yes |  |
| `serverIp` | string \| null | yes |  |
| `appVersion` | string \| null | yes |  |
| `details` | object \| null | yes |  |
| `occurrenceCount` | integer | yes | -9007199254740991–9007199254740991 |
| `firstSeenAt` | string (date-time) | yes |  |
| `lastSeenAt` | string (date-time) | yes |  |
| `duplicateOf` | string (uuid) \| null | yes |  |
| `resolvedAt` | string (date-time) \| null | yes |  |
| `createdAt` | string (date-time) | yes |  |
| `updatedAt` | string (date-time) | yes |  |
| `body` | string \| null | yes |  |
| `messages` | array<object> | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X GET "https://your-institute.example/api/v1/platform/support/reports/37386ae0-3738-7738-8386-37386ae03738" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## POST /api/v1/platform/support/reports/{id}/duplicate

Mark a report as a duplicate of an earlier one of the same kind. Audited.

- Operation id: `SupportReportsController_markDuplicate_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes |  |

### Request body

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `duplicateOf` | string (uuid) | yes |  |

```json
{
  "duplicateOf": "8e5c1847-8e5c-7e5c-85c1-8e5c18478e5c"
}
```

### Responses

**200** — The report, now a duplicate.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `kind` | enum: "fault" \| "problem" \| "feature" | yes |  |
| `source` | enum: "updater" \| "guardian" \| "install_ui" \| "unverified_intake" | yes |  |
| `verification` | enum: "updater_signed" \| "install_signed" \| "unverified" | yes |  |
| `status` | enum: "new" \| "acknowledged" \| "in_progress" \| "resolved" \| "declined" \| "duplicate" | yes |  |
| `severity` | enum: "info" \| "warning" \| "critical" \| null \| null | yes |  |
| `code` | enum: "update_rolled_back" \| "update_stuck" \| "rollback_failed" \| "post_update_drift" \| "repair_applied" \| "file_unknown_modification" \| "env_missing_unrecoverable" \| "repair_loop" \| "backup_failed" \| "backup_missing" \| "backup_required_failed" \| "offsite_upload_failed" \| "offsite_quota_exceeded" \| "disk_low" \| "docker_daemon_down" \| "service_unhealthy" \| "registry_auth_failed" \| "registry_unreachable" \| "certificate_invalid" \| "guardian_restored" \| "guardian_tampered" \| "updater_tampered" \| "updater_crashloop" \| "pin_unverifiable" \| "manifest_bad_signature" \| "digest_mismatch" \| "desired_state_mismatch" \| "protocol_unsupported" \| "install_down" \| "repair_suppressed" \| "service_drift_unrecoverable" \| null \| null | yes |  |
| `category` | enum: "login_access" \| "people" \| "attendance" \| "billing" \| "courses_content" \| "notifications" \| "updates_backups" \| "performance" \| "other" \| null \| null | yes |  |
| `title` | string \| null | yes |  |
| `tenant` | object \| null | yes |  |
| `installId` | string \| null | yes |  |
| `claimedInstallId` | string (uuid) \| null | yes |  |
| `serverIp` | string \| null | yes |  |
| `appVersion` | string \| null | yes |  |
| `details` | object \| null | yes |  |
| `occurrenceCount` | integer | yes | -9007199254740991–9007199254740991 |
| `firstSeenAt` | string (date-time) | yes |  |
| `lastSeenAt` | string (date-time) | yes |  |
| `duplicateOf` | string (uuid) \| null | yes |  |
| `resolvedAt` | string (date-time) \| null | yes |  |
| `createdAt` | string (date-time) | yes |  |
| `updatedAt` | string (date-time) | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/platform/support/reports/37386ae0-3738-7738-8386-37386ae03738/duplicate" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "duplicateOf": "8e5c1847-8e5c-7e5c-85c1-8e5c18478e5c"
  }'
```

## POST /api/v1/platform/support/reports/{id}/reply

Reply to a problem or a feature request; the institute’s owner sees the reply on their install. Audited.

- Operation id: `SupportReportsController_reply_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes |  |

### Request body

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `body` | string | yes | 1–4000 chars |

```json
{
  "body": "string"
}
```

### Responses

**201** — The report with the new reply.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `kind` | enum: "fault" \| "problem" \| "feature" | yes |  |
| `source` | enum: "updater" \| "guardian" \| "install_ui" \| "unverified_intake" | yes |  |
| `verification` | enum: "updater_signed" \| "install_signed" \| "unverified" | yes |  |
| `status` | enum: "new" \| "acknowledged" \| "in_progress" \| "resolved" \| "declined" \| "duplicate" | yes |  |
| `severity` | enum: "info" \| "warning" \| "critical" \| null \| null | yes |  |
| `code` | enum: "update_rolled_back" \| "update_stuck" \| "rollback_failed" \| "post_update_drift" \| "repair_applied" \| "file_unknown_modification" \| "env_missing_unrecoverable" \| "repair_loop" \| "backup_failed" \| "backup_missing" \| "backup_required_failed" \| "offsite_upload_failed" \| "offsite_quota_exceeded" \| "disk_low" \| "docker_daemon_down" \| "service_unhealthy" \| "registry_auth_failed" \| "registry_unreachable" \| "certificate_invalid" \| "guardian_restored" \| "guardian_tampered" \| "updater_tampered" \| "updater_crashloop" \| "pin_unverifiable" \| "manifest_bad_signature" \| "digest_mismatch" \| "desired_state_mismatch" \| "protocol_unsupported" \| "install_down" \| "repair_suppressed" \| "service_drift_unrecoverable" \| null \| null | yes |  |
| `category` | enum: "login_access" \| "people" \| "attendance" \| "billing" \| "courses_content" \| "notifications" \| "updates_backups" \| "performance" \| "other" \| null \| null | yes |  |
| `title` | string \| null | yes |  |
| `tenant` | object \| null | yes |  |
| `installId` | string \| null | yes |  |
| `claimedInstallId` | string (uuid) \| null | yes |  |
| `serverIp` | string \| null | yes |  |
| `appVersion` | string \| null | yes |  |
| `details` | object \| null | yes |  |
| `occurrenceCount` | integer | yes | -9007199254740991–9007199254740991 |
| `firstSeenAt` | string (date-time) | yes |  |
| `lastSeenAt` | string (date-time) | yes |  |
| `duplicateOf` | string (uuid) \| null | yes |  |
| `resolvedAt` | string (date-time) \| null | yes |  |
| `createdAt` | string (date-time) | yes |  |
| `updatedAt` | string (date-time) | yes |  |
| `body` | string \| null | yes |  |
| `messages` | array<object> | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/platform/support/reports/37386ae0-3738-7738-8386-37386ae03738/reply" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "body": "string"
  }'
```

## POST /api/v1/platform/support/reports/{id}/status

Acknowledge, start, resolve or decline a report. Audited.

- Operation id: `SupportReportsController_changeStatus_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes |  |

### Request body

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `status` | enum: "new" \| "acknowledged" \| "in_progress" \| "resolved" \| "declined" \| "duplicate" | yes |  |

```json
{
  "status": "new"
}
```

### Responses

**200** — The report with its new status.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `kind` | enum: "fault" \| "problem" \| "feature" | yes |  |
| `source` | enum: "updater" \| "guardian" \| "install_ui" \| "unverified_intake" | yes |  |
| `verification` | enum: "updater_signed" \| "install_signed" \| "unverified" | yes |  |
| `status` | enum: "new" \| "acknowledged" \| "in_progress" \| "resolved" \| "declined" \| "duplicate" | yes |  |
| `severity` | enum: "info" \| "warning" \| "critical" \| null \| null | yes |  |
| `code` | enum: "update_rolled_back" \| "update_stuck" \| "rollback_failed" \| "post_update_drift" \| "repair_applied" \| "file_unknown_modification" \| "env_missing_unrecoverable" \| "repair_loop" \| "backup_failed" \| "backup_missing" \| "backup_required_failed" \| "offsite_upload_failed" \| "offsite_quota_exceeded" \| "disk_low" \| "docker_daemon_down" \| "service_unhealthy" \| "registry_auth_failed" \| "registry_unreachable" \| "certificate_invalid" \| "guardian_restored" \| "guardian_tampered" \| "updater_tampered" \| "updater_crashloop" \| "pin_unverifiable" \| "manifest_bad_signature" \| "digest_mismatch" \| "desired_state_mismatch" \| "protocol_unsupported" \| "install_down" \| "repair_suppressed" \| "service_drift_unrecoverable" \| null \| null | yes |  |
| `category` | enum: "login_access" \| "people" \| "attendance" \| "billing" \| "courses_content" \| "notifications" \| "updates_backups" \| "performance" \| "other" \| null \| null | yes |  |
| `title` | string \| null | yes |  |
| `tenant` | object \| null | yes |  |
| `installId` | string \| null | yes |  |
| `claimedInstallId` | string (uuid) \| null | yes |  |
| `serverIp` | string \| null | yes |  |
| `appVersion` | string \| null | yes |  |
| `details` | object \| null | yes |  |
| `occurrenceCount` | integer | yes | -9007199254740991–9007199254740991 |
| `firstSeenAt` | string (date-time) | yes |  |
| `lastSeenAt` | string (date-time) | yes |  |
| `duplicateOf` | string (uuid) \| null | yes |  |
| `resolvedAt` | string (date-time) \| null | yes |  |
| `createdAt` | string (date-time) | yes |  |
| `updatedAt` | string (date-time) | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/platform/support/reports/37386ae0-3738-7738-8386-37386ae03738/status" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "status": "new"
  }'
```

## GET /api/v1/platform/support/reports/summary

What is new, split the way the console tabs are — the numbers behind the sidebar badge.

- Operation id: `SupportReportsController_summary_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Responses

**200** — Counts of new reports.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `newCriticalFaults` | integer | yes | -9007199254740991–9007199254740991 |
| `newFaults` | integer | yes | -9007199254740991–9007199254740991 |
| `newProblems` | integer | yes | -9007199254740991–9007199254740991 |
| `newFeatures` | integer | yes | -9007199254740991–9007199254740991 |
| `newUnverified` | integer | yes | -9007199254740991–9007199254740991 |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X GET "https://your-institute.example/api/v1/platform/support/reports/summary" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## GET /api/v1/platform/support/requests

This install’s customer’s own problem and feature requests with their status and the vendor’s replies, newest first; ?since= narrows to what changed. Never another customer’s.

- Operation id: `SupportIntakeController_listRequests_v1`
- Permission: —
- Auth: public (no credential)
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `cursor` | query | string | no |  |
| `limit` | query | integer | no | 1–100 · default: 25 |
| `since` | query | string (date-time) | no |  |

### Responses

**200** — A page of requests.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `data` | array<object> | yes |  |
| `nextCursor` | string \| null | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X GET "https://your-institute.example/api/v1/platform/support/requests" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## POST /api/v1/platform/support/requests

A problem report or a feature request an institute’s owner wrote, signed by the install. Idempotent on clientRef; carries no author name or email.

- Operation id: `SupportIntakeController_submitRequest_v1`
- Permission: —
- Auth: public (no credential)
- `Idempotency-Key`: not required

### Request body

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `clientRef` | string (uuid) | yes |  |
| `kind` | enum: "problem" \| "feature" | yes |  |
| `category` | enum: "login_access" \| "people" \| "attendance" \| "billing" \| "courses_content" \| "notifications" \| "updates_backups" \| "performance" \| "other" | yes |  |
| `title` | string | yes | 1–120 chars |
| `text` | string | yes | 1–4000 chars |

```json
{
  "clientRef": "97cc9cef-97cc-77cc-8cc9-97cc9cef97cc",
  "kind": "problem",
  "category": "login_access",
  "title": "Midterm Exam",
  "text": "string"
}
```

### Responses

**200** — The stored report.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `status` | enum: "new" \| "acknowledged" \| "in_progress" \| "resolved" \| "declined" \| "duplicate" | yes |  |
| `created` | boolean | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/platform/support/requests" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "clientRef": "97cc9cef-97cc-77cc-8cc9-97cc9cef97cc",
    "kind": "problem",
    "category": "login_access",
    "title": "Midterm Exam",
    "text": "string"
  }'
```

## GET /api/v1/platform/tenants

List tenants (cursor-paginated, optional name search and status filter).

- Operation id: `TenantsController_list_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `cursor` | query | string | no |  |
| `limit` | query | integer | no | 1–100 · default: 25 |
| `search` | query | string | no | 1–200 chars |
| `status` | query | enum: "active" \| "suspended" \| "archived" \| "deleted" | no |  |

### Responses

**200** — A page of tenants.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `data` | array<object> | yes |  |
| `nextCursor` | string \| null | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X GET "https://your-institute.example/api/v1/platform/tenants" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## POST /api/v1/platform/tenants

Create a tenant: seeds its default branch/roles, primary subdomain, trial subscription, and emails the owner an invitation.

- Operation id: `TenantsController_create_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Request body

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `slug` | string | yes | 3–40 chars |
| `name` | string | yes | 1–200 chars |
| `defaultLocale` | enum: "ar" \| "en" \| "he" | yes |  |
| `timezone` | string | no | 1–100 chars |
| `currency` | string | no | 3–3 chars |
| `deploymentMode` | enum: "cloud" \| "self_hosted" | no |  |
| `plan` | string | yes | 1–50 chars |
| `trialDays` | integer | yes | 0–365 |
| `ownerEmail` | string (email) | yes |  |

```json
{
  "slug": "string",
  "name": "Algebra I",
  "defaultLocale": "ar",
  "timezone": "string",
  "currency": "USD",
  "deploymentMode": "cloud",
  "plan": "string",
  "trialDays": 0,
  "ownerEmail": "ada.lovelace@example.com"
}
```

### Responses

**201** — The newly created tenant.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `tenant` | object | yes |  |
| `domain` | string | yes |  |
| `invitationId` | string (uuid) | yes |  |
| `portalOwner` | unknown | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/platform/tenants" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "slug": "string",
    "name": "Algebra I",
    "defaultLocale": "ar",
    "timezone": "string",
    "currency": "USD",
    "deploymentMode": "cloud",
    "plan": "string",
    "trialDays": 0,
    "ownerEmail": "ada.lovelace@example.com"
  }'
```

## GET /api/v1/platform/tenants/{id}

Tenant detail: subscription, domains, member/branch counts.

- Operation id: `TenantsController_getDetail_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes |  |

### Responses

**200** — The tenant, its subscription, domains, and counts.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `tenant` | object | yes |  |
| `subscription` | object \| null | yes |  |
| `domains` | array<object> | yes |  |
| `counts` | object | yes |  |
| `endedAt` | string (date-time) \| null | yes |  |
| `portalOwner` | unknown | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X GET "https://your-institute.example/api/v1/platform/tenants/37386ae0-3738-7738-8386-37386ae03738" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## PATCH /api/v1/platform/tenants/{id}

Update a tenant’s name, locale, timezone, currency, or per-tenant auto-lock override. Changing the auto-lock override is owner only.

- Operation id: `TenantsController_update_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes |  |

### Request body

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `name` | string | no | 1–200 chars |
| `defaultLocale` | enum: "ar" \| "en" \| "he" | no |  |
| `timezone` | string | no | 1–100 chars |
| `currency` | string | no | 3–3 chars |
| `autoLockOverride` | enum: "inherit" \| "on" \| "off" | no |  |

```json
{
  "name": "Algebra I",
  "defaultLocale": "ar",
  "timezone": "string",
  "currency": "USD",
  "autoLockOverride": "inherit"
}
```

### Responses

**200** — The updated tenant.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `slug` | string | yes |  |
| `name` | string | yes |  |
| `status` | enum: "active" \| "suspended" \| "archived" \| "deleted" | yes |  |
| `defaultLocale` | enum: "ar" \| "en" \| "he" | yes |  |
| `timezone` | string | yes |  |
| `currency` | string | yes | 3–3 chars |
| `deploymentMode` | enum: "cloud" \| "self_hosted" | yes |  |
| `autoLockOverride` | enum: "inherit" \| "on" \| "off" | yes |  |
| `lockedAt` | string (date-time) \| null | yes |  |
| `lockedReason` | string \| null | yes |  |
| `lockedByPlatformAdminId` | string (uuid) \| null | yes |  |
| `createdBy` | string (uuid) \| null | yes |  |
| `deletedAt` | string (date-time) \| null | yes |  |
| `deletedName` | string \| null | yes |  |
| `createdAt` | string (date-time) | yes |  |
| `updatedAt` | string (date-time) | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X PATCH "https://your-institute.example/api/v1/platform/tenants/37386ae0-3738-7738-8386-37386ae03738" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "name": "Algebra I",
    "defaultLocale": "ar",
    "timezone": "string",
    "currency": "USD",
    "autoLockOverride": "inherit"
  }'
```

## POST /api/v1/platform/tenants/{id}/delete

Delete an ended (archived) customer’s data: every row of the tenant in every tenant-scoped table, its files in object storage, its domains, and the users who belong to no other tenant. Kept: audit log, licences, subscriptions, install tokens, registry credentials, relay account and ledger, customer requests — and the tenant row as a tombstone (status `deleted`, slug renamed free). `confirmName` must equal the tenant’s name exactly. 409 unless the tenant is archived. Owner only; requires a step-up MFA check within the last 10 minutes.

- Operation id: `TenantsController_delete_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes |  |

### Request body

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `confirmName` | string | yes | 1–200 chars |

```json
{
  "confirmName": "string"
}
```

### Responses

**200** — The tombstone, what was removed, and the outcome of the storage cleanup.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `tenant` | object | yes |  |
| `deletedRows` | integer | yes | 0–9007199254740991 |
| `deletedUsers` | integer | yes | 0–9007199254740991 |
| `storageCleanup` | enum: "done" \| "failed" | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/platform/tenants/37386ae0-3738-7738-8386-37386ae03738/delete" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "confirmName": "string"
  }'
```

## POST /api/v1/platform/tenants/{id}/end

End a customer: revokes every licence and registry credential, closes the relay account, and archives the tenant. Nothing is deleted. Owner only; requires a step-up MFA check within the last 10 minutes; idempotent.

- Operation id: `TenantsController_end_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes |  |

### Responses

**200** — The tenant, now archived.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `slug` | string | yes |  |
| `name` | string | yes |  |
| `status` | enum: "active" \| "suspended" \| "archived" \| "deleted" | yes |  |
| `defaultLocale` | enum: "ar" \| "en" \| "he" | yes |  |
| `timezone` | string | yes |  |
| `currency` | string | yes | 3–3 chars |
| `deploymentMode` | enum: "cloud" \| "self_hosted" | yes |  |
| `autoLockOverride` | enum: "inherit" \| "on" \| "off" | yes |  |
| `lockedAt` | string (date-time) \| null | yes |  |
| `lockedReason` | string \| null | yes |  |
| `lockedByPlatformAdminId` | string (uuid) \| null | yes |  |
| `createdBy` | string (uuid) \| null | yes |  |
| `deletedAt` | string (date-time) \| null | yes |  |
| `deletedName` | string \| null | yes |  |
| `createdAt` | string (date-time) | yes |  |
| `updatedAt` | string (date-time) | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/platform/tenants/37386ae0-3738-7738-8386-37386ae03738/end" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## POST /api/v1/platform/tenants/{id}/impersonate

Issue a 10-minute access token for the tenant’s owner membership (audited). Owner only; requires a step-up MFA check within the last 10 minutes.

- Operation id: `TenantsController_impersonate_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes |  |

### Request body

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `reason` | string | yes | 1–500 chars |

```json
{
  "reason": "Requested by guardian."
}
```

### Responses

**200** — A short-lived access token scoped to the tenant’s owner.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `accessToken` | string | yes |  |
| `expiresInSec` | integer | yes | -9007199254740991–9007199254740991 |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/platform/tenants/37386ae0-3738-7738-8386-37386ae03738/impersonate" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "reason": "Requested by guardian."
  }'
```

## POST /api/v1/platform/tenants/{id}/link-portal-owner

Link or invite a portal owner for an institute that has none (docs/specs/phase17-unified-login.md §5.3, closes K-68). Owner only; requires a step-up MFA check within the last 10 minutes; refuses if the institute already has a portal owner, or if the email already owns a different institute.

- Operation id: `TenantsController_linkPortalOwner_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes |  |

### Request body

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `email` | string (email) | yes |  |

```json
{
  "email": "ada.lovelace@example.com"
}
```

### Responses

**200** — The resulting portal-owner status (linked or invited).

```json
"value"
```

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/platform/tenants/37386ae0-3738-7738-8386-37386ae03738/link-portal-owner" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "email": "ada.lovelace@example.com"
  }'
```

## POST /api/v1/platform/tenants/{id}/lock

Lock a tenant explicitly: its subscription becomes suspended and every member request gets 402 (except the allowlist). Expiry alone never does this.

- Operation id: `TenantsController_lock_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes |  |

### Request body

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `reason` | string | yes | 1–500 chars |

```json
{
  "reason": "Requested by guardian."
}
```

### Responses

**200** — The subscription, now locked.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `tenantId` | string (uuid) | yes |  |
| `planId` | string (uuid) | yes |  |
| `status` | enum: "trialing" \| "active" \| "expired" \| "suspended" \| "cancelled" | yes |  |
| `currentPeriodStart` | string (date-time) | yes |  |
| `currentPeriodEnd` | string (date-time) | yes |  |
| `cancelledAt` | string (date-time) \| null | yes |  |
| `createdAt` | string (date-time) | yes |  |
| `updatedAt` | string (date-time) | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/platform/tenants/37386ae0-3738-7738-8386-37386ae03738/lock" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "reason": "Requested by guardian."
  }'
```

## POST /api/v1/platform/tenants/{id}/reactivate

Reactivate a suspended tenant. Owner only.

- Operation id: `TenantsController_reactivate_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes |  |

### Responses

**200** — The tenant, now active.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `slug` | string | yes |  |
| `name` | string | yes |  |
| `status` | enum: "active" \| "suspended" \| "archived" \| "deleted" | yes |  |
| `defaultLocale` | enum: "ar" \| "en" \| "he" | yes |  |
| `timezone` | string | yes |  |
| `currency` | string | yes | 3–3 chars |
| `deploymentMode` | enum: "cloud" \| "self_hosted" | yes |  |
| `autoLockOverride` | enum: "inherit" \| "on" \| "off" | yes |  |
| `lockedAt` | string (date-time) \| null | yes |  |
| `lockedReason` | string \| null | yes |  |
| `lockedByPlatformAdminId` | string (uuid) \| null | yes |  |
| `createdBy` | string (uuid) \| null | yes |  |
| `deletedAt` | string (date-time) \| null | yes |  |
| `deletedName` | string \| null | yes |  |
| `createdAt` | string (date-time) | yes |  |
| `updatedAt` | string (date-time) | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/platform/tenants/37386ae0-3738-7738-8386-37386ae03738/reactivate" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## POST /api/v1/platform/tenants/{id}/suspend

Suspend a tenant. Every member request then gets 402 (except the allowlist). Owner only.

- Operation id: `TenantsController_suspend_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes |  |

### Responses

**200** — The tenant, now suspended.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `slug` | string | yes |  |
| `name` | string | yes |  |
| `status` | enum: "active" \| "suspended" \| "archived" \| "deleted" | yes |  |
| `defaultLocale` | enum: "ar" \| "en" \| "he" | yes |  |
| `timezone` | string | yes |  |
| `currency` | string | yes | 3–3 chars |
| `deploymentMode` | enum: "cloud" \| "self_hosted" | yes |  |
| `autoLockOverride` | enum: "inherit" \| "on" \| "off" | yes |  |
| `lockedAt` | string (date-time) \| null | yes |  |
| `lockedReason` | string \| null | yes |  |
| `lockedByPlatformAdminId` | string (uuid) \| null | yes |  |
| `createdBy` | string (uuid) \| null | yes |  |
| `deletedAt` | string (date-time) \| null | yes |  |
| `deletedName` | string \| null | yes |  |
| `createdAt` | string (date-time) | yes |  |
| `updatedAt` | string (date-time) | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/platform/tenants/37386ae0-3738-7738-8386-37386ae03738/suspend" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## POST /api/v1/platform/tenants/{id}/unarchive

Restore an ended (archived) customer: the tenant becomes active and its members can sign in again; a relay account that "end" closed is reopened. Licences, registry credentials and install commands stay revoked — re-issue them from the Customer tab (`reissueRequired`). 409 unless the tenant is archived. Owner only; requires a step-up MFA check within the last 10 minutes.

- Operation id: `TenantsController_unarchive_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes |  |

### Responses

**200** — The tenant, now active.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `tenant` | object | yes |  |
| `relayAccount` | enum: "reopened" \| "unchanged" \| "none" | yes |  |
| `reissueRequired` | array<enum: "licenses" \| "registry_credentials" \| "install_command"> | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/platform/tenants/37386ae0-3738-7738-8386-37386ae03738/unarchive" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## POST /api/v1/platform/tenants/{id}/unlock

Unlock a tenant: back to active, or expired if the paid period has run out.

- Operation id: `TenantsController_unlock_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes |  |

### Responses

**200** — The subscription, now unlocked.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `tenantId` | string (uuid) | yes |  |
| `planId` | string (uuid) | yes |  |
| `status` | enum: "trialing" \| "active" \| "expired" \| "suspended" \| "cancelled" | yes |  |
| `currentPeriodStart` | string (date-time) | yes |  |
| `currentPeriodEnd` | string (date-time) | yes |  |
| `cancelledAt` | string (date-time) \| null | yes |  |
| `createdAt` | string (date-time) | yes |  |
| `updatedAt` | string (date-time) | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/platform/tenants/37386ae0-3738-7738-8386-37386ae03738/unlock" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## GET /api/v1/platform/tenants/{tenantId}/bundle

Download the customer install bundle (docker-compose.yml, .env.example, Caddyfile, README-FIRST.md) as a tar.gz for a published version.

- Operation id: `TenantBundleController_download_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `tenantId` | path | string | yes |  |
| `version` | query | string | yes |  |

### Responses

**200** — The bundle, as a gzip-compressed tar stream.

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X GET "https://your-institute.example/api/v1/platform/tenants/4332af8b-4332-7332-832a-4332af8b4332/bundle?version=string" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## POST /api/v1/platform/tenants/{tenantId}/domains

Add a custom domain (CNAME). Returns a verification token to publish as a DNS TXT record.

- Operation id: `TenantDomainsController_add_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `tenantId` | path | string | yes |  |

### Request body

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `domain` | string | yes | 3–253 chars |

```json
{
  "domain": "string"
}
```

### Responses

**201** — The newly added (unverified) domain.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `tenantId` | string (uuid) | yes |  |
| `domain` | string | yes |  |
| `kind` | enum: "subdomain" \| "custom" | yes |  |
| `verificationToken` | string | yes |  |
| `verifiedAt` | string (date-time) \| null | yes |  |
| `isPrimary` | boolean | yes |  |
| `createdAt` | string (date-time) | yes |  |
| `updatedAt` | string (date-time) | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/platform/tenants/4332af8b-4332-7332-832a-4332af8b4332/domains" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "domain": "string"
  }'
```

## DELETE /api/v1/platform/tenants/{tenantId}/domains/{domainId}

Remove a custom domain (the primary domain cannot be removed).

- Operation id: `TenantDomainsController_remove_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `tenantId` | path | string | yes |  |
| `domainId` | path | string | yes |  |

### Responses

**200** — The domain was removed.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `ok` | enum: true | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X DELETE "https://your-institute.example/api/v1/platform/tenants/4332af8b-4332-7332-832a-4332af8b4332/domains/f5472324-f547-7547-8472-f5472324f547" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## POST /api/v1/platform/tenants/{tenantId}/domains/{domainId}/verify

Verify a custom domain by DNS TXT lookup at `_ims-verify.<domain>`.

- Operation id: `TenantDomainsController_verify_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `tenantId` | path | string | yes |  |
| `domainId` | path | string | yes |  |

### Responses

**200** — The now-verified domain.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `tenantId` | string (uuid) | yes |  |
| `domain` | string | yes |  |
| `kind` | enum: "subdomain" \| "custom" | yes |  |
| `verificationToken` | string | yes |  |
| `verifiedAt` | string (date-time) \| null | yes |  |
| `isPrimary` | boolean | yes |  |
| `createdAt` | string (date-time) | yes |  |
| `updatedAt` | string (date-time) | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/platform/tenants/4332af8b-4332-7332-832a-4332af8b4332/domains/f5472324-f547-7547-8472-f5472324f547/verify" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## GET /api/v1/platform/tenants/{tenantId}/install-tokens

List this tenant's install tokens (state derived: unused / used / expired / revoked).

- Operation id: `InstallTokensController_list_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `tenantId` | path | string | yes |  |

### Responses

**200** — The tenant's install tokens.

```json
[
  {
    "id": "37386ae0-3738-7738-8386-37386ae03738",
    "tenantId": "4332af8b-4332-7332-832a-4332af8b4332",
    "state": "unused",
    "expiresAt": "2026-03-02T09:00:00.000Z",
    "usedAt": "2026-03-02T09:00:00.000Z",
    "usedFromIp": "string",
    "revokedAt": "2026-03-02T09:00:00.000Z",
    "createdBy": "0b14e5a6-0b14-7b14-814e-0b14e5a60b14",
    "createdByAccountId": "34bcf5dc-34bc-74bc-8bcf-34bcf5dc34bc",
    "label": "string",
    "createdAt": "2026-03-02T09:00:00.000Z",
    "updatedAt": "2026-03-02T09:00:00.000Z"
  }
]
```

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X GET "https://your-institute.example/api/v1/platform/tenants/4332af8b-4332-7332-832a-4332af8b4332/install-tokens" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## POST /api/v1/platform/tenants/{tenantId}/install-tokens

Generate a one-time install command for this tenant. The raw token is returned exactly once. Owner only.

- Operation id: `InstallTokensController_create_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `tenantId` | path | string | yes |  |

### Request body

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `label` | string | no | 1–200 chars |

```json
{
  "label": "string"
}
```

### Responses

**201** — The new token and its ready-to-paste install command.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `token` | object | yes |  |
| `rawToken` | string | yes |  |
| `command` | string | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/platform/tenants/4332af8b-4332-7332-832a-4332af8b4332/install-tokens" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "label": "string"
  }'
```

## DELETE /api/v1/platform/tenants/{tenantId}/install-tokens/{tokenId}

Revoke an unused install token. A used or already-revoked token cannot be revoked again.

- Operation id: `InstallTokensController_revoke_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `tenantId` | path | string | yes |  |
| `tokenId` | path | string | yes |  |

### Responses

**200** — The revoked token.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `tenantId` | string (uuid) | yes |  |
| `state` | enum: "unused" \| "used" \| "expired" \| "revoked" | yes |  |
| `expiresAt` | string (date-time) | yes |  |
| `usedAt` | string (date-time) \| null | yes |  |
| `usedFromIp` | string \| null | yes |  |
| `revokedAt` | string (date-time) \| null | yes |  |
| `createdBy` | string (uuid) \| null | yes |  |
| `createdByAccountId` | string (uuid) \| null | yes |  |
| `label` | string \| null | yes |  |
| `createdAt` | string (date-time) | yes |  |
| `updatedAt` | string (date-time) | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X DELETE "https://your-institute.example/api/v1/platform/tenants/4332af8b-4332-7332-832a-4332af8b4332/install-tokens/3ab6031f-3ab6-7ab6-8b60-3ab6031f3ab6" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## GET /api/v1/platform/tenants/{tenantId}/licenses

List every license issued to this tenant.

- Operation id: `TenantLicensesController_list_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `tenantId` | path | string | yes |  |

### Responses

**200** — The tenant’s licenses.

```json
[
  {
    "id": "37386ae0-3738-7738-8386-37386ae03738",
    "tenantId": "4332af8b-4332-7332-832a-4332af8b4332",
    "jti": "string",
    "issuedAt": "2026-03-02T09:00:00.000Z",
    "expiresAt": "2026-03-02T09:00:00.000Z",
    "lastValidatedAt": "2026-03-02T09:00:00.000Z",
    "status": "valid",
    "metadata": {},
    "createdAt": "2026-03-02T09:00:00.000Z",
    "updatedAt": "2026-03-02T09:00:00.000Z"
  }
]
```

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X GET "https://your-institute.example/api/v1/platform/tenants/4332af8b-4332-7332-832a-4332af8b4332/licenses" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## POST /api/v1/platform/tenants/{tenantId}/licenses

Issue a signed license for this tenant. The key is returned exactly once. Owner only; requires a step-up MFA check within the last 10 minutes.

- Operation id: `TenantLicensesController_issue_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `tenantId` | path | string | yes |  |

### Request body

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `ttlDays` | integer | no | 0–3650 · default: 365 |

```json
{
  "ttlDays": 365
}
```

### Responses

**201** — The issued license (key returned once) and its record.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `license` | object | yes |  |
| `key` | string | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/platform/tenants/4332af8b-4332-7332-832a-4332af8b4332/licenses" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "ttlDays": 365
  }'
```

## POST /api/v1/platform/tenants/{tenantId}/licenses/{licenseId}/revoke

Revoke a license (key compromise / re-issue). Its heartbeats then answer 404, identically to an unknown license.

- Operation id: `TenantLicensesController_revoke_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `tenantId` | path | string | yes |  |
| `licenseId` | path | string | yes |  |

### Responses

**200** — The revoked license.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `tenantId` | string (uuid) | yes |  |
| `jti` | string | yes |  |
| `issuedAt` | string (date-time) | yes |  |
| `expiresAt` | string (date-time) | yes |  |
| `lastValidatedAt` | string (date-time) \| null | yes |  |
| `status` | enum: "valid" \| "expired" \| "locked" \| "revoked" | yes |  |
| `metadata` | object \| null | yes |  |
| `createdAt` | string (date-time) | yes |  |
| `updatedAt` | string (date-time) | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/platform/tenants/4332af8b-4332-7332-832a-4332af8b4332/licenses/f5f084e9-f5f0-75f0-8f08-f5f084e9f5f0/revoke" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## GET /api/v1/platform/tenants/{tenantId}/registry-credentials

List this tenant's registry credentials (usernames, timestamps — never a secret).

- Operation id: `TenantRegistryCredentialsController_list_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `tenantId` | path | string | yes |  |

### Responses

**200** — The tenant's registry credentials.

```json
[
  {
    "id": "37386ae0-3738-7738-8386-37386ae03738",
    "tenantId": "4332af8b-4332-7332-832a-4332af8b4332",
    "username": "string",
    "createdByUserId": "d630cadc-d630-7630-830c-d630cadcd630",
    "lastUsedAt": "2026-03-02T09:00:00.000Z",
    "revokedAt": "2026-03-02T09:00:00.000Z",
    "createdAt": "2026-03-02T09:00:00.000Z",
    "updatedAt": "2026-03-02T09:00:00.000Z"
  }
]
```

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X GET "https://your-institute.example/api/v1/platform/tenants/4332af8b-4332-7332-832a-4332af8b4332/registry-credentials" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## POST /api/v1/platform/tenants/{tenantId}/registry-credentials

Create a registry credential for this tenant. The secret is returned exactly once. Owner only.

- Operation id: `TenantRegistryCredentialsController_create_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `tenantId` | path | string | yes |  |

### Responses

**201** — The new credential and its one-time secret.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `credential` | object | yes |  |
| `secret` | string | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/platform/tenants/4332af8b-4332-7332-832a-4332af8b4332/registry-credentials" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## POST /api/v1/platform/tenants/{tenantId}/registry-credentials/{credentialId}/revoke

Revoke a registry credential. Its next token request answers 401.

- Operation id: `TenantRegistryCredentialsController_revoke_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `tenantId` | path | string | yes |  |
| `credentialId` | path | string | yes |  |

### Responses

**200** — The revoked credential.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `tenantId` | string (uuid) | yes |  |
| `username` | string | yes |  |
| `createdByUserId` | string (uuid) \| null | yes |  |
| `lastUsedAt` | string (date-time) \| null | yes |  |
| `revokedAt` | string (date-time) \| null | yes |  |
| `createdAt` | string (date-time) | yes |  |
| `updatedAt` | string (date-time) | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/platform/tenants/4332af8b-4332-7332-832a-4332af8b4332/registry-credentials/609aae45-609a-709a-89aa-609aae45609a/revoke" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## POST /api/v1/platform/tenants/{tenantId}/subscription

Set a tenant’s plan and/or billing period.

- Operation id: `TenantSubscriptionController_set_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `tenantId` | path | string | yes |  |

### Request body

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `planId` | string (uuid) | no |  |
| `currentPeriodStart` | string (date-time) | no |  |
| `currentPeriodEnd` | string (date-time) | no |  |

```json
{
  "planId": "744bf63f-744b-744b-84bf-744bf63f744b",
  "currentPeriodStart": "2026-03-02T09:00:00.000Z",
  "currentPeriodEnd": "2026-03-02T09:00:00.000Z"
}
```

### Responses

**200** — The updated subscription.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `tenantId` | string (uuid) | yes |  |
| `planId` | string (uuid) | yes |  |
| `status` | enum: "trialing" \| "active" \| "expired" \| "suspended" \| "cancelled" | yes |  |
| `currentPeriodStart` | string (date-time) | yes |  |
| `currentPeriodEnd` | string (date-time) | yes |  |
| `cancelledAt` | string (date-time) \| null | yes |  |
| `createdAt` | string (date-time) | yes |  |
| `updatedAt` | string (date-time) | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/platform/tenants/4332af8b-4332-7332-832a-4332af8b4332/subscription" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "planId": "744bf63f-744b-744b-84bf-744bf63f744b",
    "currentPeriodStart": "2026-03-02T09:00:00.000Z",
    "currentPeriodEnd": "2026-03-02T09:00:00.000Z"
  }'
```

## POST /api/v1/platform/tenants/{tenantId}/subscription/cancel

Cancel a tenant’s subscription.

- Operation id: `TenantSubscriptionController_cancel_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `tenantId` | path | string | yes |  |

### Responses

**200** — The cancelled subscription.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `tenantId` | string (uuid) | yes |  |
| `planId` | string (uuid) | yes |  |
| `status` | enum: "trialing" \| "active" \| "expired" \| "suspended" \| "cancelled" | yes |  |
| `currentPeriodStart` | string (date-time) | yes |  |
| `currentPeriodEnd` | string (date-time) | yes |  |
| `cancelledAt` | string (date-time) \| null | yes |  |
| `createdAt` | string (date-time) | yes |  |
| `updatedAt` | string (date-time) | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/platform/tenants/4332af8b-4332-7332-832a-4332af8b4332/subscription/cancel" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## POST /api/v1/platform/tenants/{tenantId}/subscription/renew

Extend the billing period and set status back to active.

- Operation id: `TenantSubscriptionController_renew_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `tenantId` | path | string | yes |  |

### Request body

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `currentPeriodStart` | string (date-time) | no |  |
| `currentPeriodEnd` | string (date-time) | yes |  |

```json
{
  "currentPeriodStart": "2026-03-02T09:00:00.000Z",
  "currentPeriodEnd": "2026-03-02T09:00:00.000Z"
}
```

### Responses

**200** — The renewed subscription.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `tenantId` | string (uuid) | yes |  |
| `planId` | string (uuid) | yes |  |
| `status` | enum: "trialing" \| "active" \| "expired" \| "suspended" \| "cancelled" | yes |  |
| `currentPeriodStart` | string (date-time) | yes |  |
| `currentPeriodEnd` | string (date-time) | yes |  |
| `cancelledAt` | string (date-time) \| null | yes |  |
| `createdAt` | string (date-time) | yes |  |
| `updatedAt` | string (date-time) | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/platform/tenants/4332af8b-4332-7332-832a-4332af8b4332/subscription/renew" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "currentPeriodStart": "2026-03-02T09:00:00.000Z",
    "currentPeriodEnd": "2026-03-02T09:00:00.000Z"
  }'
```

## GET /api/v1/platform/users

Search users globally (cursor-paginated).

- Operation id: `PlatformUsersController_list_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `cursor` | query | string | no |  |
| `limit` | query | integer | no | 1–100 · default: 25 |
| `search` | query | string | no | 1–200 chars |

### Responses

**200** — A page of users.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `data` | array<object> | yes |  |
| `nextCursor` | string \| null | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X GET "https://your-institute.example/api/v1/platform/users" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## POST /api/v1/platform/users/{id}/disable

Disable a user account and revoke every one of their sessions.

- Operation id: `PlatformUsersController_disable_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes |  |

### Responses

**200** — The user was disabled.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `ok` | enum: true | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/platform/users/37386ae0-3738-7738-8386-37386ae03738/disable" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```

## POST /api/v1/platform/users/{id}/enable

Re-enable a disabled user account.

- Operation id: `PlatformUsersController_enable_v1`
- Permission: —
- Auth: credential required
- `Idempotency-Key`: not required

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes |  |

### Responses

**200** — The user was enabled.

| Property | Type | Required | Constraints |
| --- | --- | --- | --- |
| `ok` | enum: true | yes |  |

Every other status answers with the shared `ErrorEnvelope` — see
https://docs.institflow.com/md/en/error-codes.md

### Example request

```bash
curl -X POST "https://your-institute.example/api/v1/platform/users/37386ae0-3738-7738-8386-37386ae03738/enable" \
  -H "Authorization: Bearer $INSTITFLOW_API_KEY"
```
