# Open Mercato API

Version: 0.6.7

Auto-generated OpenAPI definition for all enabled modules.

## Servers
- https://trainmaxr.pboj.smallhost.pl/api – Default environment

## GET `/ai_training_plans/library/exercises`

List the visible library exercises - global product-default, own club tier, own private entries

Requires features: ai_training_plans.library.view

**Tags:** AI training plans

**Requires authentication.**

**Features:** ai_training_plans.library.view

### Responses

**200** – Exercises

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "tier": "string",
      "content": {},
      "ownerUserId": null,
      "copiedFromId": null,
      "importId": null,
      "createdAt": "string"
    }
  ]
}
```

**400** – Invalid query

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/ai_training_plans/library/exercises" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/ai_training_plans/library/exercises`

Save an exercise - always an insert; a copy-edit sets copiedFromId and never overwrites the source

Requires features: ai_training_plans.library.manage

**Tags:** AI training plans

**Requires authentication.**

**Features:** ai_training_plans.library.manage

### Request Body

Content-Type: `application/json`

```json
{
  "content": {
    "label": "string"
  },
  "shareToClub": true
}
```

### Responses

**201** – The saved entry

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "tier": "string",
  "content": {},
  "ownerUserId": null,
  "copiedFromId": null,
  "importId": null,
  "createdAt": "string"
}
```

**404** – Copy source out of scope

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid content

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/ai_training_plans/library/exercises" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"content\": {
    \"label\": \"string\"
  },
  \"shareToClub\": true
}"
```

## GET `/ai_training_plans/library/imports`

List the club import ingestion records with their status

Requires features: ai_training_plans.library.view

**Tags:** AI training plans

**Requires authentication.**

**Features:** ai_training_plans.library.view

### Responses

**200** – Imports

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "sourceFormat": "string",
      "importStatus": "string",
      "error": null,
      "attachmentId": null,
      "createdAt": "string"
    }
  ]
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/ai_training_plans/library/imports" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/ai_training_plans/library/imports`

Upload one office file (multipart `file` + optional `shareToClub`); a parse failure leaves a failed record and no entry

Requires features: ai_training_plans.library.manage

**Tags:** AI training plans

**Requires authentication.**

**Features:** ai_training_plans.library.manage

### Responses

**201** – The ingestion record and the created entry

Content-Type: `application/json`

```json
{
  "import": {
    "id": "00000000-0000-4000-8000-000000000000",
    "sourceFormat": "string",
    "importStatus": "string",
    "error": null,
    "attachmentId": null,
    "createdAt": "string"
  }
}
```

**422** – Unsupported, empty, or unreadable file

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/ai_training_plans/library/imports" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/ai_training_plans/library/outline-templates`

List the visible outline templates across the three tiers

Requires features: ai_training_plans.library.view

**Tags:** AI training plans

**Requires authentication.**

**Features:** ai_training_plans.library.view

### Responses

**200** – Templates

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "tier": "string",
      "content": {},
      "ownerUserId": null,
      "copiedFromId": null,
      "createdAt": "string"
    }
  ]
}
```

**400** – Invalid query

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/ai_training_plans/library/outline-templates" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/ai_training_plans/library/outline-templates`

Save a template - always an insert; the curated base is never overwritten

Requires features: ai_training_plans.library.manage

**Tags:** AI training plans

**Requires authentication.**

**Features:** ai_training_plans.library.manage

### Request Body

Content-Type: `application/json`

```json
{
  "content": {
    "blocks": [
      {
        "label": "string"
      }
    ]
  },
  "shareToClub": true
}
```

### Responses

**201** – The saved template

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "tier": "string",
  "content": {},
  "ownerUserId": null,
  "copiedFromId": null,
  "createdAt": "string"
}
```

**404** – Copy source out of scope

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid content

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/ai_training_plans/library/outline-templates" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"content\": {
    \"blocks\": [
      {
        \"label\": \"string\"
      }
    ]
  },
  \"shareToClub\": true
}"
```

## DELETE `/ai_training_plans/plans`

Discard a generation (soft delete) - the input layer is untouched

Requires features: ai_training_plans.plan.manage

**Tags:** AI training plans

**Requires authentication.**

**Features:** ai_training_plans.plan.manage

### Request Body

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

### Responses

**200** – Discarded

Content-Type: `application/json`

```json
{
  "ok": true
}
```

**404** – Plan out of scope

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X DELETE "https://trainmaxr.pboj.smallhost.pl/api/ai_training_plans/plans" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/ai_training_plans/plans`

List the plans of the teams in the caller scope, filterable by team, status, and session

Requires features: ai_training_plans.plan.view

**Tags:** AI training plans

**Requires authentication.**

**Features:** ai_training_plans.plan.view

### Responses

**200** – Plans

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "teamOrgId": "00000000-0000-4000-8000-000000000000",
      "prompt": "string",
      "title": null,
      "status": "string",
      "outline": null,
      "generatedOutline": null,
      "sessionId": null,
      "outlineTemplateId": null,
      "generatedAt": null,
      "generationError": null,
      "createdAt": "string"
    }
  ]
}
```

**400** – Invalid query

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/ai_training_plans/plans" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## PUT `/ai_training_plans/plans`

Save outline/title edits - the generated outline, prompt, and provenance stay immutable

Requires features: ai_training_plans.plan.manage

**Tags:** AI training plans

**Requires authentication.**

**Features:** ai_training_plans.plan.manage

### Request Body

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

### Responses

**200** – The edited plan

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "teamOrgId": "00000000-0000-4000-8000-000000000000",
  "prompt": "string",
  "title": null,
  "status": "string",
  "outline": null,
  "generatedOutline": null,
  "sessionId": null,
  "outlineTemplateId": null,
  "generatedAt": null,
  "generationError": null,
  "createdAt": "string"
}
```

**404** – Plan out of scope

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**409** – The plan has no editable outline yet

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid patch

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X PUT "https://trainmaxr.pboj.smallhost.pl/api/ai_training_plans/plans" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## POST `/ai_training_plans/plans/generate`

Explicit on-demand generation grounded in the club game model, the academy corpus, picked library entries, and the derived coach history - never auto-applied

Requires features: ai_training_plans.plan.manage

**Tags:** AI training plans

**Requires authentication.**

**Features:** ai_training_plans.plan.manage

### Request Body

Content-Type: `application/json`

```json
{
  "teamOrgId": "00000000-0000-4000-8000-000000000000",
  "prompt": "string",
  "libraryExerciseIds": []
}
```

### Responses

**201** – The generated (or failed) plan

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "status": "string"
}
```

**404** – Team or session out of scope

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**409** – The referenced session is cancelled or not a training

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid prompt

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/ai_training_plans/plans/generate" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"teamOrgId\": \"00000000-0000-4000-8000-000000000000\",
  \"prompt\": \"string\",
  \"libraryExerciseIds\": []
}"
```

## GET `/ai_training_plans/settings/academy-model`

Read the active corpus status for the club

Requires features: clubs.settings.view

**Tags:** AI training plans

**Requires authentication.**

**Features:** clubs.settings.view

### Responses

**200** – The active corpus or null

Content-Type: `application/json`

```json
{
  "corpus": null
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/ai_training_plans/settings/academy-model" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## PUT `/ai_training_plans/settings/academy-model`

Upload/replace the academy document (multipart `file`); it primes every subsequent generation

Requires features: clubs.settings.manage

**Tags:** AI training plans

**Requires authentication.**

**Features:** clubs.settings.manage

### Responses

**200** – The new active corpus

Content-Type: `application/json`

```json
{
  "corpus": {
    "id": "00000000-0000-4000-8000-000000000000",
    "fileName": "string",
    "active": true,
    "textLength": 1,
    "uploadedAt": "string"
  }
}
```

**422** – Unsupported or unreadable file

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X PUT "https://trainmaxr.pboj.smallhost.pl/api/ai_training_plans/settings/academy-model" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/ai_training_plans/settings/game-model`

Read the tenant game-model blob merged over defaults

Requires features: clubs.settings.view

**Tags:** AI training plans

**Requires authentication.**

**Features:** clubs.settings.view

### Responses

**200** – The game model

Content-Type: `application/json`

```json
{
  "gameModel": {
    "playingModel": "",
    "sampleMethods": []
  }
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/ai_training_plans/settings/game-model" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## PUT `/ai_training_plans/settings/game-model`

Replace the playing model + sample methods (club admin)

Requires features: clubs.settings.manage

**Tags:** AI training plans

**Requires authentication.**

**Features:** clubs.settings.manage

### Request Body

Content-Type: `application/json`

```json
{
  "playingModel": "string",
  "sampleMethods": []
}
```

### Responses

**200** – The saved game model

Content-Type: `application/json`

```json
{
  "gameModel": {
    "playingModel": "",
    "sampleMethods": []
  }
}
```

**422** – Empty or invalid playing model

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X PUT "https://trainmaxr.pboj.smallhost.pl/api/ai_training_plans/settings/game-model" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"playingModel\": \"string\",
  \"sampleMethods\": []
}"
```

## DELETE `/attachments`

Delete attachment

Removes an uploaded attachment and deletes the stored asset.

Requires features: attachments.manage

**Tags:** Attachments

**Requires authentication.**

**Features:** attachments.manage

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| id | query | any | Required |

### Responses

**200** – Attachment deleted

Content-Type: `application/json`

```json
{
  "ok": true
}
```

**400** – Missing attachment identifier

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**404** – Attachment not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X DELETE "https://trainmaxr.pboj.smallhost.pl/api/attachments?id=00000000-0000-4000-8000-000000000000" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/attachments`

List attachments for a record

Returns uploaded attachments for the given entity record, ordered by newest first.

Requires features: attachments.view

**Tags:** Attachments

**Requires authentication.**

**Features:** attachments.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| entityId | query | any | Required. Entity identifier that owns the attachments |
| recordId | query | any | Required. Record identifier within the entity |
| page | query | any | Optional |
| pageSize | query | any | Optional |

### Responses

**200** – Attachments found for the record

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "string",
      "url": "string",
      "fileName": "string",
      "fileSize": 1,
      "createdAt": "string",
      "mimeType": null,
      "content": null
    }
  ]
}
```

**400** – Missing entity or record identifiers

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/attachments?entityId=string&recordId=string" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/attachments`

Upload attachment

Uploads a new attachment using multipart form-data and stores metadata for later retrieval.

Requires features: attachments.manage

**Tags:** Attachments

**Requires authentication.**

**Features:** attachments.manage

### Request Body

Content-Type: `multipart/form-data`

```text
entityId=string
recordId=string
file=string
```

### Responses

**200** – Attachment stored successfully

Content-Type: `application/json`

```json
{
  "ok": true,
  "item": {
    "id": "string",
    "url": "string",
    "fileName": "string",
    "fileSize": 1,
    "content": null
  }
}
```

**400** – Payload validation error

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/attachments" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: multipart/form-data" \
  -d "{
  \"entityId\": \"string\",
  \"recordId\": \"string\",
  \"file\": \"string\"
}"
```

## GET `/attachments/file/{id}`

Download or serve attachment file

Returns the raw file content for an attachment. Path parameter: {id} - Attachment UUID. Query parameter: ?download=1 - Force file download with Content-Disposition header. Access control is enforced based on partition settings.

**Tags:** Attachments

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| id | path | any | Required |

### Responses

**200** – File content with appropriate MIME type

Content-Type: `application/json`

**400** – Missing attachment ID

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**401** – Unauthorized - authentication required for private partitions

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**403** – Forbidden - insufficient permissions

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**404** – Attachment or file not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**500** – Partition misconfigured

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/attachments/file/:id" \
  -H "Accept: application/json"
```

## GET `/attachments/image/{id}/{slug}`

Serve image with optional resizing

Returns an image attachment with optional on-the-fly resizing and cropping. Resized images are cached for performance. Only works with image MIME types. Path parameter: {id} - Attachment UUID. Query parameters: ?width=N (1-4000 pixels), ?height=N (1-4000 pixels), ?cropType=cover|contain (resize behavior).

**Tags:** Attachments

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| id | path | any | Required |
| slug | path | any | Optional |

### Responses

**200** – Binary image content (Content-Type: image/jpeg, image/png, etc.)

Content-Type: `application/json`

**400** – Invalid parameters, missing ID, or non-image attachment

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**401** – Unauthorized - authentication required for private partitions

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**403** – Forbidden - insufficient permissions

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**404** – Image not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**500** – Partition misconfigured or image rendering failed

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/attachments/image/:id/:slug" \
  -H "Accept: application/json"
```

## GET `/attachments/library`

List attachments

Returns paginated list of attachments with optional filtering by search term, partition, and tags. Includes available tags and partitions.

Requires features: attachments.view

**Tags:** Attachments

**Requires authentication.**

**Features:** attachments.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| page | query | any | Optional. Page number for pagination |
| pageSize | query | any | Optional. Number of items per page (max 100) |
| search | query | any | Optional. Search by file name (case-insensitive) |
| partition | query | any | Optional. Filter by partition code |
| tags | query | any | Optional. Filter by tags (comma-separated) |
| sortField | query | any | Optional. Field to sort by |
| sortDir | query | any | Optional. Sort direction |

### Responses

**200** – Attachments list with pagination and metadata

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "fileName": "string",
      "fileSize": 1,
      "mimeType": "string",
      "partitionCode": "string",
      "partitionTitle": null,
      "url": null,
      "createdAt": "string",
      "tags": [
        "string"
      ],
      "assignments": [],
      "content": null
    }
  ],
  "total": 1,
  "page": 1,
  "pageSize": 1,
  "totalPages": 1,
  "availableTags": [
    "string"
  ],
  "partitions": [
    {
      "code": "string",
      "title": "string",
      "description": null,
      "isPublic": true
    }
  ]
}
```

**400** – Invalid query parameters

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/attachments/library?page=1&pageSize=25" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## DELETE `/attachments/library/{id}`

Delete attachment

Permanently deletes an attachment file from storage and database. Emits CRUD side effects.

Requires features: attachments.manage

**Tags:** Attachments

**Requires authentication.**

**Features:** attachments.manage

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| id | path | any | Required |

### Responses

**200** – Attachment deleted successfully

Content-Type: `application/json`

```json
{
  "ok": true
}
```

**400** – Invalid attachment ID

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**404** – Attachment not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X DELETE "https://trainmaxr.pboj.smallhost.pl/api/attachments/library/:id" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/attachments/library/{id}`

Get attachment details

Returns complete details of an attachment including metadata, tags, assignments, and custom fields.

Requires features: attachments.view

**Tags:** Attachments

**Requires authentication.**

**Features:** attachments.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| id | path | any | Required |

### Responses

**200** – Attachment details

Content-Type: `application/json`

```json
{
  "item": {
    "id": "00000000-0000-4000-8000-000000000000",
    "fileName": "string",
    "fileSize": 1,
    "mimeType": "string",
    "partitionCode": "string",
    "partitionTitle": null,
    "tags": [
      "string"
    ],
    "assignments": [],
    "content": null,
    "customFields": null
  }
}
```

**400** – Invalid attachment ID

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**404** – Attachment not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/attachments/library/:id" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## PATCH `/attachments/library/{id}`

Update attachment metadata

Updates attachment tags, assignments, and custom fields. Emits CRUD side effects for indexing and events.

Requires features: attachments.manage

**Tags:** Attachments

**Requires authentication.**

**Features:** attachments.manage

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| id | path | any | Required |

### Request Body

Content-Type: `application/json`

```json
{}
```

### Responses

**200** – Attachment updated successfully

Content-Type: `application/json`

```json
{
  "ok": true
}
```

**400** – Invalid payload or attachment ID

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**404** – Attachment not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**500** – Failed to save attributes

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X PATCH "https://trainmaxr.pboj.smallhost.pl/api/attachments/library/:id" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{}"
```

## DELETE `/attachments/partitions`

Delete partition

Deletes a partition. Default partitions cannot be deleted. Partitions with existing attachments cannot be deleted.

Requires features: attachments.manage

**Tags:** Attachments

**Requires authentication.**

**Features:** attachments.manage

### Responses

**200** – Partition deleted successfully

Content-Type: `application/json`

```json
{
  "ok": true
}
```

**400** – Invalid ID or default partition deletion attempt

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**404** – Partition not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**409** – Partition in use

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X DELETE "https://trainmaxr.pboj.smallhost.pl/api/attachments/partitions" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/attachments/partitions`

List all attachment partitions

Returns all configured attachment partitions with storage settings, OCR configuration, and access control settings.

Requires features: attachments.manage

**Tags:** Attachments

**Requires authentication.**

**Features:** attachments.manage

### Responses

**200** – List of partitions

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "code": "string",
      "title": "string",
      "description": null,
      "isPublic": true,
      "requiresOcr": true,
      "ocrModel": null,
      "configJson": null,
      "createdAt": null,
      "updatedAt": null,
      "envKey": "string"
    }
  ]
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/attachments/partitions" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/attachments/partitions`

Create new partition

Creates a new attachment partition with specified storage and OCR settings. Requires unique partition code.

Requires features: attachments.manage

**Tags:** Attachments

**Requires authentication.**

**Features:** attachments.manage

### Request Body

Content-Type: `application/json`

```json
{
  "code": "string",
  "title": "string",
  "description": null,
  "ocrModel": null,
  "storageDriver": "local",
  "configJson": null
}
```

### Responses

**201** – Partition created successfully

Content-Type: `application/json`

```json
{
  "item": {
    "id": "00000000-0000-4000-8000-000000000000",
    "code": "string",
    "title": "string",
    "description": null,
    "isPublic": true,
    "requiresOcr": true,
    "ocrModel": null,
    "configJson": null,
    "createdAt": null,
    "updatedAt": null,
    "envKey": "string"
  }
}
```

**400** – Invalid payload or partition code

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**409** – Partition code already exists

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/attachments/partitions" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"code\": \"string\",
  \"title\": \"string\",
  \"description\": null,
  \"ocrModel\": null,
  \"storageDriver\": \"local\",
  \"configJson\": null
}"
```

## PUT `/attachments/partitions`

Update partition

Updates an existing partition. Partition code cannot be changed. Title, description, OCR settings, and access control can be modified.

Requires features: attachments.manage

**Tags:** Attachments

**Requires authentication.**

**Features:** attachments.manage

### Request Body

Content-Type: `application/json`

```json
{
  "code": "string",
  "title": "string",
  "description": null,
  "ocrModel": null,
  "storageDriver": "local",
  "configJson": null,
  "id": "00000000-0000-4000-8000-000000000000"
}
```

### Responses

**200** – Partition updated successfully

Content-Type: `application/json`

```json
{
  "item": {
    "id": "00000000-0000-4000-8000-000000000000",
    "code": "string",
    "title": "string",
    "description": null,
    "isPublic": true,
    "requiresOcr": true,
    "ocrModel": null,
    "configJson": null,
    "createdAt": null,
    "updatedAt": null,
    "envKey": "string"
  }
}
```

**400** – Invalid payload or code change attempt

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**404** – Partition not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X PUT "https://trainmaxr.pboj.smallhost.pl/api/attachments/partitions" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"code\": \"string\",
  \"title\": \"string\",
  \"description\": null,
  \"ocrModel\": null,
  \"storageDriver\": \"local\",
  \"configJson\": null,
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## POST `/attachments/transfer`

Transfer attachments to different record

Transfers one or more attachments from one record to another within the same entity type. Updates attachment assignments and metadata to reflect the new record.

Requires features: attachments.manage

**Tags:** Attachments

**Requires authentication.**

**Features:** attachments.manage

### Request Body

Content-Type: `application/json`

```json
{
  "entityId": "string",
  "attachmentIds": [
    "00000000-0000-4000-8000-000000000000"
  ],
  "toRecordId": "string"
}
```

### Responses

**200** – Attachments transferred successfully

Content-Type: `application/json`

```json
{
  "ok": true,
  "updated": 1
}
```

**400** – Invalid payload

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**404** – Attachments not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**500** – Attachment model missing

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/attachments/transfer" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"entityId\": \"string\",
  \"attachmentIds\": [
    \"00000000-0000-4000-8000-000000000000\"
  ],
  \"toRecordId\": \"string\"
}"
```

## GET `/attendance_tracking`

List attendance records

The attendance history read, scoped to the caller's visible teams. A record FKs the persistent player, so a player's history survives roster changes and transfers; a guest's record carries the host team-org and never shows under the home-team filter. Filters: session, player, team, outcome, guest flag, confirmation.

Requires features: attendance_tracking.attendance.view

**Tags:** Attendance

**Requires authentication.**

**Features:** attendance_tracking.attendance.view

### Responses

**200** – Attendance records

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "sessionId": "00000000-0000-4000-8000-000000000000",
      "playerId": "00000000-0000-4000-8000-000000000000",
      "teamOrgId": "00000000-0000-4000-8000-000000000000",
      "status": "present",
      "absenceReason": null,
      "source": "staff",
      "isGuest": true,
      "confirmationStatus": "confirmed",
      "markedByUserId": null,
      "markedAt": "string",
      "confirmedByUserId": null,
      "confirmedAt": null
    }
  ]
}
```

**400** – Invalid query

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**404** – Team or session not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/attendance_tracking" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/attendance_tracking`

Record or correct one attendance mark

Upserts the single live record for (session, player): a roster player of the session team at session time, or a consented guest. Rejected when the session is cancelled (409), the player is off the sheet (422), or a guest consent is pending/revoked (409).

Requires features: attendance_tracking.attendance.manage

**Tags:** Attendance

**Requires authentication.**

**Features:** attendance_tracking.attendance.manage

### Request Body

Content-Type: `application/json`

```json
{
  "sessionId": "00000000-0000-4000-8000-000000000000",
  "playerId": "00000000-0000-4000-8000-000000000000",
  "status": "present"
}
```

### Responses

**200** – Upserted record id

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

**404** – Session not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**409** – Cancelled session or unconsented guest

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid input or off-sheet player

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/attendance_tracking" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"sessionId\": \"00000000-0000-4000-8000-000000000000\",
  \"playerId\": \"00000000-0000-4000-8000-000000000000\",
  \"status\": \"present\"
}"
```

## POST `/attendance_tracking/accept`

Confirm a pending record, optionally overriding the outcome

The phase-2 acceptance surface, shipped now so the record shape is complete: a pending portal self-report becomes final only through the coach, who may edit the outcome while accepting. A pending mark is never final silently.

Requires features: attendance_tracking.attendance.manage

**Tags:** Attendance

**Requires authentication.**

**Features:** attendance_tracking.attendance.manage

### Request Body

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

### Responses

**200** – Confirmed record id

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

**404** – Record or session not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**409** – Not pending or cancelled session

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid input

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/attendance_tracking/accept" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## POST `/attendance_tracking/bulk`

Record the whole sheet in one pass

The coach's primary write path (feature §5): upserts every mark on (session, player) in one transaction over the session team's roster at session time plus its consented guests. The whole submission is validated before any row is touched.

Requires features: attendance_tracking.attendance.manage

**Tags:** Attendance

**Requires authentication.**

**Features:** attendance_tracking.attendance.manage

### Request Body

Content-Type: `application/json`

```json
{
  "sessionId": "00000000-0000-4000-8000-000000000000",
  "marks": [
    {
      "playerId": "00000000-0000-4000-8000-000000000000",
      "status": "present"
    }
  ]
}
```

### Responses

**200** – Upserted record ids

Content-Type: `application/json`

```json
{
  "ids": [
    "00000000-0000-4000-8000-000000000000"
  ]
}
```

**404** – Session not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**409** – Cancelled session or unconsented guest

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid input, duplicate player, or off-sheet player

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/attendance_tracking/bulk" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"sessionId\": \"00000000-0000-4000-8000-000000000000\",
  \"marks\": [
    {
      \"playerId\": \"00000000-0000-4000-8000-000000000000\",
      \"status\": \"present\"
    }
  ]
}"
```

## DELETE `/attendance_tracking/guests`

Remove a guest from the event

Soft-deletes the per-event guest row; an already-recorded attendance mark stays as history.

Requires features: attendance_tracking.guest.manage

**Tags:** Attendance

**Requires authentication.**

**Features:** attendance_tracking.guest.manage

### Request Body

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

### Responses

**200** – Removed

Content-Type: `application/json`

```json
{
  "ok": true
}
```

**404** – Guest not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X DELETE "https://trainmaxr.pboj.smallhost.pl/api/attendance_tracking/guests" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/attendance_tracking/guests`

List an event's guests

Guests are resolved per event (feature §5a): every row here is one player from another in-club team on this one host event, with the inline consent decision.

Requires features: attendance_tracking.attendance.view

**Tags:** Attendance

**Requires authentication.**

**Features:** attendance_tracking.attendance.view

### Responses

**200** – Event guests

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "sessionId": "00000000-0000-4000-8000-000000000000",
      "hostTeamOrgId": "00000000-0000-4000-8000-000000000000",
      "guestPlayerId": "00000000-0000-4000-8000-000000000000",
      "homeTeamOrgId": "00000000-0000-4000-8000-000000000000",
      "addedByUserId": "string",
      "consentStatus": "not_required",
      "consentByUserId": null,
      "consentAt": null
    }
  ]
}
```

**400** – Invalid query

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**404** – Session not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/attendance_tracking/guests" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/attendance_tracking/guests`

Add an in-club guest to the event

The guest must hold a live membership on the named home team - another team in the SAME club - and not on the host roster. A club-admin or host-coordinator add lands not_required; a coach add lands pending until the guest's home-team coach grants consent.

Requires features: attendance_tracking.guest.manage

**Tags:** Attendance

**Requires authentication.**

**Features:** attendance_tracking.guest.manage

### Request Body

Content-Type: `application/json`

```json
{
  "sessionId": "00000000-0000-4000-8000-000000000000",
  "guestPlayerId": "00000000-0000-4000-8000-000000000000",
  "homeTeamOrgId": "00000000-0000-4000-8000-000000000000"
}
```

### Responses

**201** – Created guest id and its consent status

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "consentStatus": "not_required"
}
```

**404** – Session or player not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**409** – Cancelled session or duplicate guest

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid input or ineligible guest

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/attendance_tracking/guests" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"sessionId\": \"00000000-0000-4000-8000-000000000000\",
  \"guestPlayerId\": \"00000000-0000-4000-8000-000000000000\",
  \"homeTeamOrgId\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## DELETE `/attendance_tracking/guests/consent`

Decline or revoke the consent

Flips the consent to revoked and drops the guest from the host sheet; their mark stays refused.

Requires features: attendance_tracking.guest.consent

**Tags:** Attendance

**Requires authentication.**

**Features:** attendance_tracking.guest.consent

### Request Body

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

### Responses

**200** – Revoked

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "consentStatus": "not_required"
}
```

**404** – Guest not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**409** – No consent required

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X DELETE "https://trainmaxr.pboj.smallhost.pl/api/attendance_tracking/guests/consent" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## POST `/attendance_tracking/guests/consent`

Grant the consent

Only a coach/coordinator of the guest's HOME team (or the club admin) decides; the decision is per event and never reused. Granting makes the guest markable on the host sheet.

Requires features: attendance_tracking.guest.consent

**Tags:** Attendance

**Requires authentication.**

**Features:** attendance_tracking.guest.consent

### Request Body

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

### Responses

**200** – Granted

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "consentStatus": "not_required"
}
```

**404** – Guest not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**409** – No consent required

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/attendance_tracking/guests/consent" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/attendance_tracking/sheet`

Read the roster-and-guests mark sheet for one session

The derived sheet the coach fills, in one request: the session team's memberships active at session time UNION the event's unrevoked guests, joined to the decrypted player identity and each player's current record. A cancelled session returns the sheet read-only (no row markable); a pending-consent guest is listed but blocked.

Requires features: attendance_tracking.attendance.view

**Tags:** Attendance

**Requires authentication.**

**Features:** attendance_tracking.attendance.view

### Responses

**200** – The mark sheet

Content-Type: `application/json`

```json
{
  "session": {
    "id": "00000000-0000-4000-8000-000000000000",
    "teamOrgId": "00000000-0000-4000-8000-000000000000",
    "eventType": "string",
    "status": "string",
    "startsAt": "string"
  },
  "rows": [
    {
      "playerId": "00000000-0000-4000-8000-000000000000",
      "firstName": null,
      "lastName": null,
      "isGuest": true,
      "consentStatus": null,
      "markable": true,
      "record": null
    }
  ]
}
```

**400** – Invalid query

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**404** – Session not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/attendance_tracking/sheet" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/audit_logs/audit-logs/access`

Retrieve access logs

Fetches paginated access audit logs scoped to the authenticated user. Tenant administrators can optionally expand the search to other actors or organizations.

Requires features: audit_logs.view_self

**Tags:** Audit & Action Logs

**Requires authentication.**

**Features:** audit_logs.view_self

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| organizationId | query | any | Optional. Limit results to a specific organization |
| actorUserId | query | any | Optional. Filter by actor user id (tenant administrators only) |
| resourceKind | query | any | Optional. Restrict to a resource kind such as `order` or `product` |
| accessType | query | any | Optional. Access type filter, e.g. `read` or `export` |
| page | query | any | Optional. Page number (default 1) |
| pageSize | query | any | Optional. Page size (default 50) |
| limit | query | any | Optional. Explicit maximum number of records when paginating manually |
| before | query | any | Optional. Return logs created before this ISO-8601 timestamp |
| after | query | any | Optional. Return logs created after this ISO-8601 timestamp |

### Responses

**200** – Access logs returned successfully

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "string",
      "resourceKind": "string",
      "resourceId": "string",
      "accessType": "string",
      "actorUserId": null,
      "actorUserName": null,
      "tenantId": null,
      "tenantName": null,
      "organizationId": null,
      "organizationName": null,
      "fields": [
        "string"
      ],
      "context": null,
      "createdAt": "string"
    }
  ],
  "canViewTenant": true,
  "page": 1,
  "pageSize": 1,
  "total": 1,
  "totalPages": 1
}
```

**400** – Invalid filters supplied

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/audit_logs/audit-logs/access" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/audit_logs/audit-logs/actions`

Fetch action logs

Returns recent action audit log entries. Tenant administrators can widen the scope to other actors or organizations, and callers can optionally restrict results to undoable actions.

Requires features: audit_logs.view_self

**Tags:** Audit & Action Logs

**Requires authentication.**

**Features:** audit_logs.view_self

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| organizationId | query | any | Optional. Limit results to a specific organization |
| actorUserId | query | any | Optional. Filter logs created by specific actor IDs (tenant administrators only). Accepts a single UUID or a comma-separated UUID list. |
| resourceKind | query | any | Optional. Filter by resource kind (e.g., "order", "product") |
| resourceId | query | any | Optional. Filter by resource ID (UUID of the specific record) |
| actionType | query | any | Optional. Filter by action type (`create`, `edit`, `delete`, `assign`). Accepts a single value or a comma-separated list. |
| fieldName | query | any | Optional. Filter to entries where the given field changed. Accepts a single field name or a comma-separated list. |
| includeRelated | query | any | Optional. When `true`, also returns changes to child entities linked via parentResourceKind/parentResourceId |
| includeTotal | query | any | Optional. When `true`, the response includes the filtered total count. |
| undoableOnly | query | any | Optional. When `true`, only undoable actions are returned |
| limit | query | any | Optional. Maximum number of records to return (default 50, max 1000) |
| offset | query | any | Optional. Zero-based record offset for pagination (legacy — prefer page/pageSize) |
| page | query | any | Optional. Page number (default 1) |
| pageSize | query | any | Optional. Page size (default 50, max 200) |
| sortField | query | any | Optional. Sort field: `createdAt`, `user`, `action`, `field`, or `source`. |
| sortDir | query | any | Optional. Sort direction: `asc` or `desc`. |
| before | query | any | Optional. Return actions created before this ISO-8601 timestamp |
| after | query | any | Optional. Return actions created after this ISO-8601 timestamp |

### Responses

**200** – Action logs retrieved successfully

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "string",
      "commandId": "string",
      "actionLabel": null,
      "executionState": "done",
      "actorUserId": null,
      "actorUserName": null,
      "tenantId": null,
      "tenantName": null,
      "organizationId": null,
      "organizationName": null,
      "resourceKind": null,
      "resourceId": null,
      "parentResourceKind": null,
      "parentResourceId": null,
      "undoToken": null,
      "createdAt": "string",
      "updatedAt": "string",
      "snapshotBefore": null,
      "snapshotAfter": null,
      "changes": null,
      "context": null
    }
  ],
  "canViewTenant": true,
  "page": 1,
  "pageSize": 1,
  "total": 1,
  "totalPages": 1
}
```

**400** – Invalid filter values

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/audit_logs/audit-logs/actions?includeRelated=false&includeTotal=false&undoableOnly=false" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/audit_logs/audit-logs/actions/export`

Export action logs as CSV

Returns a CSV attachment containing filtered action audit log entries. Tenant administrators can widen the scope to other actors or organizations.

Requires features: audit_logs.view_self

**Tags:** Audit & Action Logs

**Requires authentication.**

**Features:** audit_logs.view_self

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| organizationId | query | any | Optional. Limit results to a specific organization |
| actorUserId | query | any | Optional. Filter logs created by specific actor IDs (tenant administrators only). Accepts a single UUID or a comma-separated UUID list. |
| resourceKind | query | any | Optional. Filter by resource kind (e.g., "order", "product") |
| resourceId | query | any | Optional. Filter by resource ID (UUID of the specific record) |
| actionType | query | any | Optional. Filter by action type (`create`, `edit`, `delete`, `assign`). Accepts a single value or a comma-separated list. |
| fieldName | query | any | Optional. Filter to entries where the given field changed. Accepts a single field name or a comma-separated list. |
| includeRelated | query | any | Optional. When `true`, also returns changes to child entities linked via parentResourceKind/parentResourceId |
| undoableOnly | query | any | Optional. When `true`, only undoable actions are returned |
| limit | query | any | Optional. Maximum number of records to export (default 1000, capped at 1000) |
| sortField | query | any | Optional. Sort field: `createdAt`, `user`, `action`, `field`, or `source`. |
| sortDir | query | any | Optional. Sort direction: `asc` or `desc`. |
| before | query | any | Optional. Return actions created before this ISO-8601 timestamp |
| after | query | any | Optional. Return actions created after this ISO-8601 timestamp |

### Responses

**200** – CSV export generated successfully

Content-Type: `application/json`

```json
{
  "file": "csv"
}
```

**400** – Invalid filter values

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/audit_logs/audit-logs/actions/export?includeRelated=false&undoableOnly=false" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/audit_logs/audit-logs/actions/redo`

Redo by action log id

Redoes the latest undone command owned by the caller. Requires the action to still be eligible for redo within tenant and organization scope.

Requires features: audit_logs.redo_self

**Tags:** Audit & Action Logs

**Requires authentication.**

**Features:** audit_logs.redo_self

### Request Body

Content-Type: `application/json`

```json
{
  "logId": "string"
}
```

### Responses

**200** – Redo executed successfully

Content-Type: `application/json`

```json
{
  "ok": true,
  "logId": null,
  "undoToken": null
}
```

**400** – Log not eligible for redo

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/audit_logs/audit-logs/actions/redo" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"logId\": \"string\"
}"
```

## POST `/audit_logs/audit-logs/actions/undo`

Undo action by token

Replays the undo handler registered for a command. The provided undo token must match the latest undoable log entry accessible to the caller.

Requires features: audit_logs.undo_self

**Tags:** Audit & Action Logs

**Requires authentication.**

**Features:** audit_logs.undo_self

### Request Body

Content-Type: `application/json`

```json
{
  "undoToken": "string"
}
```

### Responses

**200** – Undo applied successfully

Content-Type: `application/json`

```json
{
  "ok": true,
  "logId": "string"
}
```

**400** – Invalid or unavailable undo token

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/audit_logs/audit-logs/actions/undo" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"undoToken\": \"string\"
}"
```

## GET `/auth/admin/nav`

Resolve backend chrome bootstrap payload

Returns the backend chrome payload available to the authenticated administrator after applying scope, RBAC, role defaults, and personal sidebar preferences.

**Tags:** Authentication & Accounts

**Requires authentication.**

### Responses

**200** – Backend chrome payload

Content-Type: `application/json`

```json
{
  "brand": null,
  "groups": [
    {
      "name": "string",
      "items": [
        {
          "href": "string",
          "title": "string"
        }
      ]
    }
  ],
  "settingsSections": [
    {
      "id": "string",
      "label": "string",
      "items": [
        {
          "id": "string",
          "label": "string",
          "href": "string"
        }
      ]
    }
  ],
  "settingsPathPrefixes": [
    "string"
  ],
  "profileSections": [
    {
      "id": "string",
      "label": "string",
      "items": [
        {
          "id": "string",
          "label": "string",
          "href": "string"
        }
      ]
    }
  ],
  "profilePathPrefixes": [
    "string"
  ],
  "grantedFeatures": [
    "string"
  ],
  "roles": [
    "string"
  ]
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/auth/admin/nav" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/auth/autologin`

Auto sign-in using env-configured demo credentials

When OM_AUTOLOGIN_EMAIL / OM_AUTOLOGIN_PASSWORD are configured, signs the visitor in with those credentials and redirects into the app. Intended for single-tenant demo instances only. Falls back to the login page when disabled or misconfigured.

**Tags:** Authentication & Accounts

### Responses

**200** – Success response

Content-Type: `application/json`

**307** – Redirect into the app (or to /login on failure)

Content-Type: `text/html`

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/auth/autologin" \
  -H "Accept: application/json"
```

## POST `/auth/feature-check`

Check feature grants for the current user

Evaluates which of the requested features are available to the signed-in user within the active tenant / organization context.

**Tags:** Authentication & Accounts

**Requires authentication.**

### Request Body

Content-Type: `application/json`

```json
{
  "features": [
    "string"
  ]
}
```

### Responses

**200** – Evaluation result

Content-Type: `application/json`

```json
{
  "ok": true,
  "granted": [
    "string"
  ],
  "userId": "string"
}
```

**400** – Invalid request — features array missing, too large, or contains invalid entries

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/auth/feature-check" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"features\": [
    \"string\"
  ]
}"
```

## GET `/auth/features`

List declared feature flags

Returns all static features contributed by the enabled modules along with their module source.

Requires features: auth.acl.manage

**Tags:** Authentication & Accounts

**Requires authentication.**

**Features:** auth.acl.manage

### Responses

**200** – Aggregated feature catalog

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "string",
      "title": "string",
      "module": "string"
    }
  ],
  "modules": [
    {
      "id": "string",
      "title": "string"
    }
  ]
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/auth/features" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/auth/locale`

Set locale and redirect

Stores the selected locale in a cookie and redirects to a safe local path.

**Tags:** Authentication & Accounts

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| locale | query | any | Required |
| redirect | query | any | Optional |

### Responses

**200** – Success response

Content-Type: `application/json`

**302** – Locale cookie set and request redirected

Content-Type: `application/json`

**400** – Invalid locale

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/auth/locale?locale=en" \
  -H "Accept: application/json"
```

## POST `/auth/locale`

Set locale

Stores the selected locale in a cookie and returns a JSON success response.

**Tags:** Authentication & Accounts

### Request Body

Content-Type: `application/json`

```json
{
  "locale": "en"
}
```

### Responses

**200** – Locale cookie set

Content-Type: `application/json`

```json
{
  "ok": true
}
```

**400** – Invalid locale or malformed request body

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/auth/locale" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d "{
  \"locale\": \"en\"
}"
```

## POST `/auth/login`

Authenticate user credentials

Validates the submitted credentials and issues a bearer token cookie for subsequent API calls.

**Tags:** Authentication & Accounts

### Request Body

Content-Type: `application/x-www-form-urlencoded`

```text
email=user%40example.com&password=string
```

### Responses

**200** – Authentication succeeded

Content-Type: `application/json`

```json
{
  "ok": true,
  "token": "string",
  "redirect": null
}
```

**400** – Validation failed

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**401** – Invalid credentials

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**403** – User lacks required role

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**429** – Too many login attempts

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/auth/login" \
  -H "Accept: application/json" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "email=user%40example.com&password=string"
```

## POST `/auth/logout`

Invalidate session and redirect

Clears authentication cookies and redirects the browser to the login page.

**Tags:** Authentication & Accounts

**Requires authentication.**

### Responses

**201** – Success response

Content-Type: `application/json`

**302** – Redirect to login after successful logout

Content-Type: `text/html`

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/auth/logout" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/auth/profile`

Get current profile

Returns the email address for the signed-in user.

**Tags:** Authentication & Accounts

**Requires authentication.**

### Responses

**200** – Profile payload

Content-Type: `application/json`

```json
{
  "email": "user@example.com",
  "roles": [
    "string"
  ]
}
```

**404** – User not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/auth/profile" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## PUT `/auth/profile`

Update current profile

Updates the email address or password for the signed-in user.

**Tags:** Authentication & Accounts

**Requires authentication.**

### Request Body

Content-Type: `application/json`

```json
{}
```

### Responses

**200** – Profile updated

Content-Type: `application/json`

```json
{
  "ok": true,
  "email": "user@example.com"
}
```

**400** – Invalid payload

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X PUT "https://trainmaxr.pboj.smallhost.pl/api/auth/profile" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{}"
```

## POST `/auth/reset`

Send reset email

Requests a password reset email for the given account. The endpoint always returns `ok: true` to avoid leaking account existence.

**Tags:** Authentication & Accounts

### Request Body

Content-Type: `application/x-www-form-urlencoded`

```text
email=user%40example.com
```

### Responses

**200** – Reset email dispatched (or ignored for unknown accounts)

Content-Type: `application/json`

```json
{
  "ok": true
}
```

**400** – Invalid request origin

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**429** – Too many password reset requests

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**500** – Password reset email origin is not configured

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/auth/reset" \
  -H "Accept: application/json" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "email=user%40example.com"
```

## POST `/auth/reset/confirm`

Complete password reset

Validates the reset token and updates the user password.

**Tags:** Authentication & Accounts

### Request Body

Content-Type: `application/x-www-form-urlencoded`

```text
token=string&password=string
```

### Responses

**200** – Password reset succeeded

Content-Type: `application/json`

```json
{
  "ok": true,
  "redirect": "string"
}
```

**400** – Invalid token or payload

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**429** – Too many reset confirmation attempts

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/auth/reset/confirm" \
  -H "Accept: application/json" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "token=string&password=string"
```

## DELETE `/auth/roles`

Delete role

Deletes a role by identifier. Fails when users remain assigned.

Requires features: auth.roles.manage

**Tags:** Authentication & Accounts

**Requires authentication.**

**Features:** auth.roles.manage

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| id | query | any | Required. Role identifier |

### Responses

**200** – Role deleted

Content-Type: `application/json`

```json
{
  "ok": true
}
```

**400** – Role cannot be deleted

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**404** – Role not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X DELETE "https://trainmaxr.pboj.smallhost.pl/api/auth/roles?id=00000000-0000-4000-8000-000000000000" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/auth/roles`

List roles

Returns available roles within the current tenant. Super administrators receive visibility across tenants.

Requires features: auth.roles.list

**Tags:** Authentication & Accounts

**Requires authentication.**

**Features:** auth.roles.list

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| id | query | any | Optional |
| page | query | any | Optional |
| pageSize | query | any | Optional |
| search | query | any | Optional |
| tenantId | query | any | Optional |

### Responses

**200** – Role collection

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "name": "string",
      "usersCount": 1,
      "tenantId": null,
      "tenantName": null,
      "updatedAt": null
    }
  ],
  "total": 1,
  "totalPages": 1
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/auth/roles?page=1&pageSize=50" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/auth/roles`

Create role

Creates a new role anchored to the caller's tenant. Non-superadmins cannot target another tenant; supplying a foreign `tenantId` is rejected.

Requires features: auth.roles.manage

**Tags:** Authentication & Accounts

**Requires authentication.**

**Features:** auth.roles.manage

### Request Body

Content-Type: `application/json`

```json
{
  "name": "string"
}
```

### Responses

**201** – Role created

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

**400** – Invalid payload

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/auth/roles" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"name\": \"string\"
}"
```

## PUT `/auth/roles`

Update role

Updates mutable fields on an existing role.

Requires features: auth.roles.manage

**Tags:** Authentication & Accounts

**Requires authentication.**

**Features:** auth.roles.manage

### Request Body

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

### Responses

**200** – Role updated

Content-Type: `application/json`

```json
{
  "ok": true
}
```

**400** – Invalid payload

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**404** – Role not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X PUT "https://trainmaxr.pboj.smallhost.pl/api/auth/roles" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/auth/roles/acl`

Fetch role ACL

Returns the feature and organization assignments associated with a role within the current tenant.

Requires features: auth.acl.manage

**Tags:** Authentication & Accounts

**Requires authentication.**

**Features:** auth.acl.manage

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| roleId | query | any | Required |
| tenantId | query | any | Optional |

### Responses

**200** – Role ACL entry

Content-Type: `application/json`

```json
{
  "isSuperAdmin": true,
  "features": [
    "string"
  ],
  "organizations": null,
  "updatedAt": null
}
```

**400** – Invalid role id

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**404** – Role not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/auth/roles/acl?roleId=00000000-0000-4000-8000-000000000000" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## PUT `/auth/roles/acl`

Update role ACL

Replaces the feature list, super admin flag, and optional organization assignments for a role.

Requires features: auth.acl.manage

**Tags:** Authentication & Accounts

**Requires authentication.**

**Features:** auth.acl.manage

### Request Body

Content-Type: `application/json`

```json
{
  "roleId": "00000000-0000-4000-8000-000000000000",
  "organizations": null
}
```

### Responses

**200** – Role ACL updated

Content-Type: `application/json`

```json
{
  "ok": true,
  "sanitized": true
}
```

**400** – Invalid payload

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**404** – Role not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X PUT "https://trainmaxr.pboj.smallhost.pl/api/auth/roles/acl" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"roleId\": \"00000000-0000-4000-8000-000000000000\",
  \"organizations\": null
}"
```

## GET `/auth/session/refresh`

Refresh auth cookie from session token (browser)

Exchanges an existing `session_token` cookie for a fresh JWT auth cookie and redirects the browser.

**Tags:** Authentication & Accounts

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| redirect | query | any | Optional. Absolute or relative URL to redirect after refresh |

### Responses

**200** – Success response

Content-Type: `application/json`

**302** – Redirect to target location when session is valid

Content-Type: `text/html`

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/auth/session/refresh" \
  -H "Accept: application/json"
```

## POST `/auth/session/refresh`

Refresh access token (API/mobile)

Exchanges a refresh token for a new JWT access token. Pass the refresh token obtained from login in the request body.

**Tags:** Authentication & Accounts

### Request Body

Content-Type: `application/json`

```json
{
  "refreshToken": "string"
}
```

### Responses

**200** – New access token issued

Content-Type: `application/json`

```json
{
  "ok": true,
  "accessToken": "string",
  "expiresIn": 1
}
```

**400** – Missing refresh token

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**401** – Invalid or expired token

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**429** – Too many refresh attempts

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/auth/session/refresh" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d "{
  \"refreshToken\": \"string\"
}"
```

## DELETE `/auth/sidebar/preferences`

Delete a role sidebar variant

Removes the role variant for the current tenant + locale. Idempotent. Requires `auth.sidebar.manage`.

Requires features: auth.sidebar.manage

**Tags:** Authentication & Accounts

**Requires authentication.**

**Features:** auth.sidebar.manage

### Responses

**200** – Variant deleted (or never existed)

Content-Type: `application/json`

```json
{
  "ok": true,
  "scope": {
    "type": "user"
  }
}
```

**400** – Missing roleId query parameter

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**404** – Role not found in current tenant scope

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X DELETE "https://trainmaxr.pboj.smallhost.pl/api/auth/sidebar/preferences" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/auth/sidebar/preferences`

Get sidebar preferences

Returns sidebar customization for the current user (default) or the specified role (`?roleId=…`, requires `auth.sidebar.manage`).

**Tags:** Authentication & Accounts

**Requires authentication.**

### Responses

**200** – Current sidebar configuration

Content-Type: `application/json`

```json
{
  "locale": "string",
  "settings": {
    "version": 1,
    "groupOrder": [
      "string"
    ],
    "groupLabels": {
      "key": "string"
    },
    "itemLabels": {
      "key": "string"
    },
    "hiddenItems": [
      "string"
    ],
    "itemOrder": {
      "key": [
        "string"
      ]
    }
  },
  "canApplyToRoles": true,
  "roles": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "name": "string",
      "hasPreference": true
    }
  ],
  "scope": {
    "type": "user"
  },
  "updatedAt": null
}
```

**403** – Missing features for role-scope read

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**404** – Role not found in current tenant scope

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/auth/sidebar/preferences" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## PUT `/auth/sidebar/preferences`

Update sidebar preferences

Updates sidebar configuration. With `scope.type === "user"` (default) writes the calling user's personal preferences and may optionally apply the same settings to selected roles via `applyToRoles[]`. With `scope.type === "role"` writes the named role variant directly (requires `auth.sidebar.manage`); `applyToRoles[]` and `clearRoleIds[]` are rejected in this mode.

Requires features: auth.sidebar.manage

**Tags:** Authentication & Accounts

**Requires authentication.**

**Features:** auth.sidebar.manage

### Request Body

Content-Type: `application/json`

```json
{}
```

### Responses

**200** – Preferences saved

Content-Type: `application/json`

```json
{
  "locale": "string",
  "settings": {
    "version": 1,
    "groupOrder": [
      "string"
    ],
    "groupLabels": {
      "key": "string"
    },
    "itemLabels": {
      "key": "string"
    },
    "hiddenItems": [
      "string"
    ],
    "itemOrder": {
      "key": [
        "string"
      ]
    }
  },
  "canApplyToRoles": true,
  "roles": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "name": "string",
      "hasPreference": true
    }
  ],
  "scope": {
    "type": "user"
  },
  "updatedAt": null,
  "appliedRoles": [
    "00000000-0000-4000-8000-000000000000"
  ],
  "clearedRoles": [
    "00000000-0000-4000-8000-000000000000"
  ]
}
```

**400** – Invalid payload

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**404** – Role not found in current tenant scope

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X PUT "https://trainmaxr.pboj.smallhost.pl/api/auth/sidebar/preferences" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{}"
```

## GET `/auth/sidebar/variants`

List sidebar variants

Returns the named sidebar variants saved by the current user for the current tenant + locale.

**Tags:** Authentication & Accounts

**Requires authentication.**

### Responses

**200** – Variant list

Content-Type: `application/json`

```json
{
  "locale": "string",
  "variants": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "name": "string",
      "isActive": true,
      "settings": {
        "version": 1,
        "groupOrder": [
          "string"
        ],
        "groupLabels": {
          "key": "string"
        },
        "itemLabels": {
          "key": "string"
        },
        "hiddenItems": [
          "string"
        ],
        "itemOrder": {
          "key": [
            "string"
          ]
        }
      },
      "createdAt": "string",
      "updatedAt": null
    }
  ]
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/auth/sidebar/variants" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/auth/sidebar/variants`

Create a sidebar variant

Creates a new variant. If `name` is omitted or blank, an auto-name like "My preferences", "My preferences 2", … is assigned.

Requires features: auth.sidebar.manage

**Tags:** Authentication & Accounts

**Requires authentication.**

**Features:** auth.sidebar.manage

### Request Body

Content-Type: `application/json`

```json
{}
```

### Responses

**200** – Variant created

Content-Type: `application/json`

```json
{
  "locale": "string",
  "variant": {
    "id": "00000000-0000-4000-8000-000000000000",
    "name": "string",
    "isActive": true,
    "settings": {
      "version": 1,
      "groupOrder": [
        "string"
      ],
      "groupLabels": {
        "key": "string"
      },
      "itemLabels": {
        "key": "string"
      },
      "hiddenItems": [
        "string"
      ],
      "itemOrder": {
        "key": [
          "string"
        ]
      }
    },
    "createdAt": "string",
    "updatedAt": null
  }
}
```

**400** – Invalid payload

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/auth/sidebar/variants" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{}"
```

## DELETE `/auth/sidebar/variants/{id}`

Delete a sidebar variant

Soft-deletes the variant (sets deleted_at).

Requires features: auth.sidebar.manage

**Tags:** Authentication & Accounts

**Requires authentication.**

**Features:** auth.sidebar.manage

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| id | path | any | Required |

### Responses

**200** – Variant deleted

Content-Type: `application/json`

```json
{
  "ok": true
}
```

**404** – Variant not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X DELETE "https://trainmaxr.pboj.smallhost.pl/api/auth/sidebar/variants/:id" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/auth/sidebar/variants/{id}`

Get a sidebar variant

**Tags:** Authentication & Accounts

**Requires authentication.**

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| id | path | any | Required |

### Responses

**200** – Variant

Content-Type: `application/json`

```json
{
  "locale": "string",
  "variant": {
    "id": "00000000-0000-4000-8000-000000000000",
    "name": "string",
    "isActive": true,
    "settings": {
      "version": 1,
      "groupOrder": [
        "string"
      ],
      "groupLabels": {
        "key": "string"
      },
      "itemLabels": {
        "key": "string"
      },
      "hiddenItems": [
        "string"
      ],
      "itemOrder": {
        "key": [
          "string"
        ]
      }
    },
    "createdAt": "string",
    "updatedAt": null
  }
}
```

**404** – Variant not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/auth/sidebar/variants/:id" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## PUT `/auth/sidebar/variants/{id}`

Update a sidebar variant

Updates the variant's name, settings, and/or isActive flag. Setting `isActive: true` deactivates other variants in the same scope (only one active per user/tenant/locale).

Requires features: auth.sidebar.manage

**Tags:** Authentication & Accounts

**Requires authentication.**

**Features:** auth.sidebar.manage

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| id | path | any | Required |

### Request Body

Content-Type: `application/json`

```json
{}
```

### Responses

**200** – Variant updated

Content-Type: `application/json`

```json
{
  "locale": "string",
  "variant": {
    "id": "00000000-0000-4000-8000-000000000000",
    "name": "string",
    "isActive": true,
    "settings": {
      "version": 1,
      "groupOrder": [
        "string"
      ],
      "groupLabels": {
        "key": "string"
      },
      "itemLabels": {
        "key": "string"
      },
      "hiddenItems": [
        "string"
      ],
      "itemOrder": {
        "key": [
          "string"
        ]
      }
    },
    "createdAt": "string",
    "updatedAt": null
  }
}
```

**400** – Invalid payload

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**404** – Variant not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X PUT "https://trainmaxr.pboj.smallhost.pl/api/auth/sidebar/variants/:id" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{}"
```

## DELETE `/auth/users`

Delete user

Deletes a user by identifier. Undo support is provided via the command bus.

Requires features: auth.users.delete

**Tags:** Authentication & Accounts

**Requires authentication.**

**Features:** auth.users.delete

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| id | query | any | Required. User identifier |

### Responses

**200** – User deleted

Content-Type: `application/json`

```json
{
  "ok": true
}
```

**400** – User cannot be deleted

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**404** – User not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X DELETE "https://trainmaxr.pboj.smallhost.pl/api/auth/users?id=00000000-0000-4000-8000-000000000000" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/auth/users`

List users

Returns users for the effective selected tenant and organization scope. Search matches email, organization name, and role name. Super administrators may scope the response via the topbar context, organization filters, or role filters. Pass scopeToActiveOrganization=1 to restrict results to the caller's active organization (used by recipient/assignee pickers so suggestions stay within the org that owns the resulting record).

Requires features: auth.users.list

**Tags:** Authentication & Accounts

**Requires authentication.**

**Features:** auth.users.list

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| id | query | any | Optional |
| page | query | any | Optional |
| pageSize | query | any | Optional |
| search | query | any | Optional |
| name | query | any | Optional |
| organizationId | query | any | Optional |
| scopeToActiveOrganization | query | any | Optional |
| roleIds | query | any | Optional |

### Responses

**200** – User collection

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "email": "user@example.com",
      "name": null,
      "organizationId": null,
      "organizationName": null,
      "tenantId": null,
      "tenantName": null,
      "roles": [
        "string"
      ],
      "updatedAt": null
    }
  ],
  "total": 1,
  "totalPages": 1
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/auth/users?page=1&pageSize=50" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/auth/users`

Create user

Creates a new confirmed user within the specified organization, optional display name, and optional roles.

Requires features: auth.users.create

**Tags:** Authentication & Accounts

**Requires authentication.**

**Features:** auth.users.create

### Request Body

Content-Type: `application/json`

```json
{
  "email": "user@example.com",
  "name": null,
  "organizationId": "00000000-0000-4000-8000-000000000000"
}
```

### Responses

**201** – User created

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

**400** – Invalid payload or duplicate email

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/auth/users" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"email\": \"user@example.com\",
  \"name\": null,
  \"organizationId\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## PUT `/auth/users`

Update user

Updates profile fields including display name, organization assignment, credentials, or role memberships.

Requires features: auth.users.edit

**Tags:** Authentication & Accounts

**Requires authentication.**

**Features:** auth.users.edit

### Request Body

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "name": null
}
```

### Responses

**200** – User updated

Content-Type: `application/json`

```json
{
  "ok": true
}
```

**400** – Invalid payload

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**404** – User not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X PUT "https://trainmaxr.pboj.smallhost.pl/api/auth/users" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\",
  \"name\": null
}"
```

## GET `/auth/users/acl`

Fetch user ACL

Returns custom ACL overrides for a user within the current tenant, if any.

Requires features: auth.acl.manage

**Tags:** Authentication & Accounts

**Requires authentication.**

**Features:** auth.acl.manage

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| userId | query | any | Required |

### Responses

**200** – User ACL entry

Content-Type: `application/json`

```json
{
  "hasCustomAcl": true,
  "isSuperAdmin": true,
  "features": [
    "string"
  ],
  "organizations": null,
  "updatedAt": null
}
```

**400** – Invalid user id

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/auth/users/acl?userId=00000000-0000-4000-8000-000000000000" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## PUT `/auth/users/acl`

Update user ACL

Configures per-user ACL overrides, including super admin access, feature list, and organization scope.

Requires features: auth.acl.manage

**Tags:** Authentication & Accounts

**Requires authentication.**

**Features:** auth.acl.manage

### Request Body

Content-Type: `application/json`

```json
{
  "userId": "00000000-0000-4000-8000-000000000000",
  "organizations": null
}
```

### Responses

**200** – User ACL updated

Content-Type: `application/json`

```json
{
  "ok": true,
  "sanitized": true
}
```

**400** – Invalid payload

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X PUT "https://trainmaxr.pboj.smallhost.pl/api/auth/users/acl" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"userId\": \"00000000-0000-4000-8000-000000000000\",
  \"organizations\": null
}"
```

## GET `/auth/users/consents`

List user consents

Returns all consent records for a given user, with integrity verification status.

Requires features: auth.users.edit

**Tags:** Auth

**Requires authentication.**

**Features:** auth.users.edit

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| userId | query | any | Required |

### Responses

**200** – Consent list returned

Content-Type: `application/json`

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/auth/users/consents?userId=00000000-0000-4000-8000-000000000000" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/auth/users/resend-invite`

Resend invitation email

Resends the invitation email to a user who has not yet set up their password. Generates a new 48-hour setup token and invalidates prior tokens.

Requires features: auth.users.create

**Tags:** Authentication & Accounts

**Requires authentication.**

**Features:** auth.users.create

### Request Body

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

### Responses

**200** – Invite email sent

Content-Type: `application/json`

```json
{
  "ok": true
}
```

**400** – Invalid request origin

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**404** – User not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**409** – User already has a password

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Validation error

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**429** – Rate limit exceeded

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**500** – Invitation email origin is not configured

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/auth/users/resend-invite" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/calendar/club`

Read the whole club's fixtures across all teams

The view-only club calendar (PRD 5.3.2): a reduced, read-only fixture projection (who plays where and against whom) across every team in the caller tenant, regardless of the caller team scope. Never crosses the club boundary and offers no mutation.

Requires features: calendar.club_calendar.view

**Tags:** Calendar

**Requires authentication.**

**Features:** calendar.club_calendar.view

### Responses

**200** – Club fixtures

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "teamOrgId": "00000000-0000-4000-8000-000000000000",
      "teamName": null,
      "eventType": "string",
      "status": "string",
      "startsAt": "string",
      "opponentName": null,
      "homeAway": null,
      "locationName": null,
      "locationAddress": null
    }
  ]
}
```

**400** – Invalid query

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/calendar/club" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/calendar/context`

Resolve the caller's default calendar view

Derives the role default view from the caller staff assignments (coordinator opens on week, coach on month, the assignment-less club admin on week) and whether the week view is single-team (a coach). UI state only - it grants and restricts nothing.

Requires features: sessions.session.view

**Tags:** Calendar

**Requires authentication.**

**Features:** sessions.session.view

### Responses

**200** – Calendar context

Content-Type: `application/json`

```json
{
  "defaultView": "grid",
  "singleTeamWeek": true
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/calendar/context" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/calendar/export`

Export the caller's in-scope schedule as an iCalendar file

One-way app-to-external sync (PRD 5.3.4): a read-only ICS document of the caller in-scope fixtures - one team via teamOrgId or the whole in-scope set - carrying title, start, meeting time, duration, location, and note per event; a cancelled event is emitted as STATUS:CANCELLED. Nothing is ever read back.

Requires features: sessions.session.view

**Tags:** Calendar

**Requires authentication.**

**Features:** sessions.session.view

### Responses

**200** – text/calendar document

Content-Type: `application/json`

```json
"string"
```

**404** – Team out of scope

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid scope selection

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/calendar/export" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## DELETE `/clubs/facilities`

Soft-delete a facility

Soft-deletes a facility; historical references keep resolving.

Requires features: clubs.facility.manage

**Tags:** Clubs

**Requires authentication.**

**Features:** clubs.facility.manage

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| id | query | any | Required |

### Responses

**200** – Facility soft-deleted

Content-Type: `application/json`

```json
{
  "ok": true
}
```

**404** – Not found in the club

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X DELETE "https://trainmaxr.pboj.smallhost.pl/api/clubs/facilities?id=00000000-0000-4000-8000-000000000000" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/clubs/facilities`

List facilities

Lists the club's pitches and locker rooms (tenant-scoped, retired entries included unless filtered).

Requires features: clubs.facility.view

**Tags:** Clubs

**Requires authentication.**

**Features:** clubs.facility.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| id | query | any | Optional |
| page | query | any | Optional |
| pageSize | query | any | Optional |
| facilityType | query | any | Optional |
| isActive | query | any | Optional |

### Responses

**200** – Facilities

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "name": "string",
      "facilityType": "pitch",
      "isActive": true,
      "createdAt": "2025-01-01T00:00:00.000Z",
      "updatedAt": null
    }
  ],
  "total": 1
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/clubs/facilities?page=1&pageSize=50" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/clubs/facilities`

Add a facility

Records a pitch or locker room in the club-wide inventory.

Requires features: clubs.facility.manage

**Tags:** Clubs

**Requires authentication.**

**Features:** clubs.facility.manage

### Request Body

Content-Type: `application/json`

```json
{
  "name": "string",
  "facilityType": "pitch"
}
```

### Responses

**201** – Facility created

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "name": "string",
  "facilityType": "pitch",
  "isActive": true,
  "createdAt": "2025-01-01T00:00:00.000Z",
  "updatedAt": null
}
```

**400** – Invalid payload

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/clubs/facilities" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"name\": \"string\",
  \"facilityType\": \"pitch\"
}"
```

## PUT `/clubs/facilities`

Edit a facility

Renames, retypes, or retires/reactivates a facility (isActive toggle).

Requires features: clubs.facility.manage

**Tags:** Clubs

**Requires authentication.**

**Features:** clubs.facility.manage

### Request Body

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

### Responses

**200** – Facility updated

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "name": "string",
  "facilityType": "pitch",
  "isActive": true,
  "createdAt": "2025-01-01T00:00:00.000Z",
  "updatedAt": null
}
```

**400** – Invalid payload

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**404** – Not found in the club

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X PUT "https://trainmaxr.pboj.smallhost.pl/api/clubs/facilities" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## POST `/clubs/facilities/bulk-setup`

Quick-create facilities from counts

Tops the club inventory up toward the requested pitch / locker room totals, creating named active facilities (never removes any).

Requires features: clubs.facility.manage

**Tags:** Clubs

**Requires authentication.**

**Features:** clubs.facility.manage

### Request Body

Content-Type: `application/json`

```json
{
  "pitches": 1,
  "lockerRooms": 1
}
```

### Responses

**200** – Created facilities

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "name": "string",
      "facilityType": "pitch",
      "isActive": true,
      "createdAt": "2025-01-01T00:00:00.000Z",
      "updatedAt": null
    }
  ]
}
```

**400** – Invalid payload

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/clubs/facilities/bulk-setup" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"pitches\": 1,
  \"lockerRooms\": 1
}"
```

## GET `/clubs/operator-clubs`

List clubs

Operator view of provisioned clubs (tenants).

Requires features: clubs.operator.view

**Tags:** Clubs

**Requires authentication.**

**Features:** clubs.operator.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| page | query | any | Optional |
| pageSize | query | any | Optional |
| search | query | any | Optional |

### Responses

**200** – Clubs

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "name": "string",
      "isActive": true,
      "createdAt": null
    }
  ],
  "total": 1,
  "totalPages": 1
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/clubs/operator-clubs?page=1&pageSize=50" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## PATCH `/clubs/operator-clubs/{id}`

Suspend or restore a club

Toggles a club (tenant) active state. Superadmin-only (clubs.suspend).

Requires features: clubs.suspend

**Tags:** Clubs

**Requires authentication.**

**Features:** clubs.suspend

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| id | path | any | Required |

### Request Body

Content-Type: `application/json`

```json
{
  "isActive": true
}
```

### Responses

**200** – Updated club

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "name": "string",
  "isActive": true,
  "createdAt": null
}
```

**400** – Invalid payload

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**404** – Club not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X PATCH "https://trainmaxr.pboj.smallhost.pl/api/clubs/operator-clubs/:id" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"isActive\": true
}"
```

## POST `/clubs/provision`

Provision a club

Creates a tenant with its club-level Organization (org slug = club handle, keys the customer portal URL), seeds the club_admin role, default settings, and the player/guardian portal customer roles, and emails the first admin an invitation. Superadmin-only.

Requires features: clubs.provision

**Tags:** Clubs

**Requires authentication.**

**Features:** clubs.provision

### Request Body

Content-Type: `application/json`

```json
{
  "clubName": "string",
  "adminEmail": "user@example.com"
}
```

### Responses

**201** – Club provisioned

Content-Type: `application/json`

```json
{
  "tenantId": "00000000-0000-4000-8000-000000000000",
  "organizationId": "00000000-0000-4000-8000-000000000000",
  "organizationSlug": "string",
  "adminUserId": "00000000-0000-4000-8000-000000000000",
  "adminEmail": "user@example.com",
  "inviteEmailSent": true
}
```

**400** – Invalid payload or provisioning failure

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**409** – Club handle (org slug) already taken by another organization

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/clubs/provision" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"clubName\": \"string\",
  \"adminEmail\": \"user@example.com\"
}"
```

## GET `/clubs/sessions/{id}/match-tags`

Read the six-metric team counts

A COUNT over the shared timeline, zero-filled over the six core metrics plus any operator-added ones - the per-match source the 5.2.2 team-statistics view reads. Nothing opponent-side is included; a cancelled match stays readable but its counts enter no statistics.

Requires features: clubs.match_tagging.view

**Tags:** Match tagging

**Requires authentication.**

**Features:** clubs.match_tagging.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| id | path | any | Required |

### Responses

**200** – Zero-filled tag counts

Content-Type: `application/json`

```json
{
  "sessionId": "00000000-0000-4000-8000-000000000000",
  "teamOrgId": "00000000-0000-4000-8000-000000000000",
  "counts": {
    "key": 1
  }
}
```

**404** – Match not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Not a match

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/clubs/sessions/:id/match-tags" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## PUT `/clubs/sessions/{id}/match-tags`

Set one metric count directly

Reconciles the timeline to the target figure - no raw totals table exists, so missing taps are appended team-level and surplus taps are soft-deleted newest-first. A count below zero is unrepresentable.

Requires features: clubs.match_tagging.manage

**Tags:** Match tagging

**Requires authentication.**

**Features:** clubs.match_tagging.manage

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| id | path | any | Required |

### Request Body

Content-Type: `application/json`

```json
{
  "metric": "string",
  "count": 1
}
```

### Responses

**200** – The reconciled metric and count

Content-Type: `application/json`

```json
{
  "metric": "string",
  "count": 1
}
```

**404** – Match not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**409** – Cancelled match

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Not a match or unknown metric

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X PUT "https://trainmaxr.pboj.smallhost.pl/api/clubs/sessions/:id/match-tags" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"metric\": \"string\",
  \"count\": 1
}"
```

## DELETE `/clubs/sessions/{id}/match-timeline`

Undo a mistap

Soft-deletes one tapped row; the derived counts reflect the change on the next read.

Requires features: clubs.match_tagging.manage

**Tags:** Match tagging

**Requires authentication.**

**Features:** clubs.match_tagging.manage

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| id | path | any | Required |

### Request Body

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

### Responses

**200** – Removed event id

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

**404** – Event not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**409** – Cancelled match

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid input

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X DELETE "https://trainmaxr.pboj.smallhost.pl/api/clubs/sessions/:id/match-timeline" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/clubs/sessions/{id}/match-timeline`

Read the match timeline and the on-pitch list

The ordered shared timeline plus the derived on-pitch list (starting XI + subs) the home player picker draws on. A training session offers no tagging sheet.

Requires features: clubs.match_tagging.view

**Tags:** Match tagging

**Requires authentication.**

**Features:** clubs.match_tagging.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| id | path | any | Required |

### Responses

**200** – Timeline view

Content-Type: `application/json`

```json
{
  "session": {
    "id": "00000000-0000-4000-8000-000000000000",
    "teamOrgId": "00000000-0000-4000-8000-000000000000",
    "eventType": "string",
    "status": "string",
    "taggable": true
  },
  "events": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "sessionId": "00000000-0000-4000-8000-000000000000",
      "teamOrgId": "00000000-0000-4000-8000-000000000000",
      "minute": 1,
      "eventType": "string",
      "side": "home",
      "playerId": null,
      "recordedByUserId": null,
      "recordedAt": "string"
    }
  ],
  "onPitchPlayerIds": [
    "string"
  ]
}
```

**404** – Match not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Not a match

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/clubs/sessions/:id/match-timeline" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/clubs/sessions/{id}/match-timeline`

Record one live tap

The companion hot path - one small append per tap. A home per-player tap names an on-pitch player; an opponent tag-metric tap is live display only and persists nothing (id: null); a cancelled match takes no taps.

Requires features: clubs.match_tagging.manage

**Tags:** Match tagging

**Requires authentication.**

**Features:** clubs.match_tagging.manage

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| id | path | any | Required |

### Request Body

Content-Type: `application/json`

```json
{
  "minute": 1,
  "eventType": "string",
  "side": "home"
}
```

### Responses

**200** – Appended event id, or null for a display-only opponent tap

Content-Type: `application/json`

```json
{
  "id": null
}
```

**404** – Match not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**409** – Cancelled match

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Not a match, unknown event key, or invalid attribution

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/clubs/sessions/:id/match-timeline" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"minute\": 1,
  \"eventType\": \"string\",
  \"side\": \"home\"
}"
```

## GET `/clubs/settings`

Get club settings

Returns the settings for the authenticated user's club (tenant), merged over defaults.

Requires features: clubs.settings.view

**Tags:** Clubs

**Requires authentication.**

**Features:** clubs.settings.view

### Responses

**200** – Club settings

Content-Type: `application/json`

```json
{
  "logoUrl": "",
  "logotypeUrl": "",
  "handle": "",
  "handleMode": "random",
  "subdomainEnabled": true,
  "legalAge": 1,
  "seasonStart": null,
  "seasonEnd": null,
  "consentRequiredMinorSurvey": true,
  "reminderLeadDays": 1,
  "weeklyReportDay": "mon",
  "weeklyReportTime": "string",
  "healthCheckExpiryWarningDays": 1,
  "archivedRetentionYears": null
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/clubs/settings" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## PUT `/clubs/settings`

Update club settings

Patches the club settings for the authenticated user's club.

Requires features: clubs.settings.manage

**Tags:** Clubs

**Requires authentication.**

**Features:** clubs.settings.manage

### Request Body

Content-Type: `application/json`

```json
{
  "seasonStart": null,
  "seasonEnd": null,
  "archivedRetentionYears": null
}
```

### Responses

**200** – Updated club settings

Content-Type: `application/json`

```json
{
  "logoUrl": "",
  "logotypeUrl": "",
  "handle": "",
  "handleMode": "random",
  "subdomainEnabled": true,
  "legalAge": 1,
  "seasonStart": null,
  "seasonEnd": null,
  "consentRequiredMinorSurvey": true,
  "reminderLeadDays": 1,
  "weeklyReportDay": "mon",
  "weeklyReportTime": "string",
  "healthCheckExpiryWarningDays": 1,
  "archivedRetentionYears": null
}
```

**400** – Invalid payload

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X PUT "https://trainmaxr.pboj.smallhost.pl/api/clubs/settings" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"seasonStart\": null,
  \"seasonEnd\": null,
  \"archivedRetentionYears\": null
}"
```

## DELETE `/clubs/tag-metrics`

Remove an operator metric

Soft-deletes the catalogue row; already tagged rows keep resolving.

Requires features: clubs.tag_metrics.manage

**Tags:** Match tagging

**Requires authentication.**

**Features:** clubs.tag_metrics.manage

### Request Body

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

### Responses

**200** – Removed catalogue id

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

**404** – Metric not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid input

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X DELETE "https://trainmaxr.pboj.smallhost.pl/api/clubs/tag-metrics" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/clubs/tag-metrics`

List the effective tag metrics

The six core metrics (shots on/off/saved/blocked, tackles, fouls) plus every active operator-added metric - the set the companion offers to tap and the rollup zero-fills. Platform-level, shared by every club.

Requires features: clubs.match_tagging.view

**Tags:** Match tagging

**Requires authentication.**

**Features:** clubs.match_tagging.view

### Responses

**200** – Effective metrics

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": null,
      "metric": "string",
      "label": null,
      "core": true
    }
  ]
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/clubs/tag-metrics" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/clubs/tag-metrics`

Add an operator metric

App-operator capability only - club members cannot extend the set. The key must fit the shared timeline column and not collide with a shipped event type.

Requires features: clubs.tag_metrics.manage

**Tags:** Match tagging

**Requires authentication.**

**Features:** clubs.tag_metrics.manage

### Request Body

Content-Type: `application/json`

```json
{
  "metric": "string",
  "label": "string"
}
```

### Responses

**200** – Created catalogue id

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

**409** – Duplicate metric key

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid or shipped key

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/clubs/tag-metrics" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"metric\": \"string\",
  \"label\": \"string\"
}"
```

## DELETE `/competition_and_results`

Clear the match result

Soft-deletes the single live result so the match returns to "no result" and is excluded from any derived record until re-recorded.

Requires features: competition_and_results.result.manage

**Tags:** Competition and results

**Requires authentication.**

**Features:** competition_and_results.result.manage

### Request Body

Content-Type: `application/json`

```json
{
  "sessionId": "00000000-0000-4000-8000-000000000000"
}
```

### Responses

**200** – Cleared result id

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

**404** – Match or result not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**409** – Cancelled match

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid input or not a match

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X DELETE "https://trainmaxr.pboj.smallhost.pl/api/competition_and_results" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"sessionId\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/competition_and_results`

List match results

The results read, scoped to the caller's visible teams (a delegated match-coach additionally reads the one delegated match). Each item joins the opponent name and kickoff from the linked match, so a results list is one request. Filters: session, team, outcome.

Requires features: competition_and_results.result.view

**Tags:** Competition and results

**Requires authentication.**

**Features:** competition_and_results.result.view

### Responses

**200** – Match results

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "sessionId": "00000000-0000-4000-8000-000000000000",
      "teamOrgId": "00000000-0000-4000-8000-000000000000",
      "outcome": "win",
      "ownScore": null,
      "opponentScore": null,
      "note": null,
      "opponentName": null,
      "startsAt": null,
      "recordedByUserId": null,
      "recordedAt": "string"
    }
  ]
}
```

**400** – Invalid query

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**404** – Team or match not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/competition_and_results" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/competition_and_results`

Record or correct the match result

Upserts the single live result for the match: an outcome (win/loss/draw from the own team's perspective) with an optional own/opponent score, consistency-checked when both scores are present. Rejected when the session is not a match (422), is cancelled (409), or is out of the caller's per-team scope (404).

Requires features: competition_and_results.result.manage

**Tags:** Competition and results

**Requires authentication.**

**Features:** competition_and_results.result.manage

### Request Body

Content-Type: `application/json`

```json
{
  "sessionId": "00000000-0000-4000-8000-000000000000",
  "outcome": "win"
}
```

### Responses

**200** – Recorded result id

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

**404** – Match not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**409** – Cancelled match

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid input, inconsistent outcome, or not a match

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/competition_and_results" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"sessionId\": \"00000000-0000-4000-8000-000000000000\",
  \"outcome\": \"win\"
}"
```

## GET `/configs/cache`

Get cache statistics

Returns detailed cache statistics including total entries and breakdown by cache segments. Requires cache service to be available.

Requires features: configs.cache.view

**Tags:** Configs

**Requires authentication.**

**Features:** configs.cache.view

### Responses

**200** – Cache statistics

Content-Type: `application/json`

```json
{
  "generatedAt": "string",
  "totalKeys": 1,
  "segments": [
    {
      "segment": "string",
      "resource": null,
      "method": null,
      "path": null,
      "keyCount": 1,
      "keys": [
        "string"
      ]
    }
  ]
}
```

**500** – Failed to resolve cache stats

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**503** – Cache service unavailable

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/configs/cache" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/configs/cache`

Purge cache

Purges cache entries. Supports two actions: purgeAll (clears entire cache) or purgeSegment (clears specific segment). Returns updated cache statistics after purge.

Requires features: configs.cache.manage

**Tags:** Configs

**Requires authentication.**

**Features:** configs.cache.manage

### Request Body

Content-Type: `application/json`

```json
{
  "action": "purgeAll"
}
```

### Responses

**200** – Cache segment cleared successfully

Content-Type: `application/json`

```json
{
  "action": "purgeSegment",
  "segment": "string",
  "deleted": 1,
  "stats": {
    "generatedAt": "string",
    "totalKeys": 1,
    "segments": [
      {
        "segment": "string",
        "resource": null,
        "method": null,
        "path": null,
        "keyCount": 1,
        "keys": [
          "string"
        ]
      }
    ]
  }
}
```

**400** – Invalid request - missing segment identifier for purgeSegment action

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**500** – Failed to purge cache

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**503** – Cache service unavailable

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/configs/cache" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"action\": \"purgeAll\"
}"
```

## DELETE `/configs/module-telemetry`

Clear module telemetry data

Development-only endpoint that clears in-memory module telemetry and local process telemetry files.

Requires features: configs.manage

**Tags:** Configs

**Requires authentication.**

**Features:** configs.manage

### Responses

**200** – Module telemetry cleared

Content-Type: `application/json`

```json
{
  "cleared": true
}
```

### Example

```bash
curl -X DELETE "https://trainmaxr.pboj.smallhost.pl/api/configs/module-telemetry" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/configs/module-telemetry`

Get module resource usage telemetry

Returns in-process module resource attribution for API routes, event subscribers, and queue workers.

Requires features: configs.system_status.view

**Tags:** Configs

**Requires authentication.**

**Features:** configs.system_status.view

### Responses

**200** – Module resource usage report

Content-Type: `application/json`

```json
{
  "generatedAt": "string",
  "startedAt": "string",
  "enabled": true,
  "bucketIntervalMs": 1,
  "totals": {
    "modules": 1,
    "operations": 1,
    "calls": 1,
    "errors": 1,
    "totalDurationMs": 1,
    "totalCpuMs": 1,
    "positiveHeapDeltaBytes": 1,
    "positiveRssDeltaBytes": 1
  },
  "thresholds": {
    "p95DurationMs": 1,
    "cpuMs": 1,
    "positiveHeapDeltaBytes": 1,
    "positiveRssDeltaBytes": 1,
    "errors": 1
  },
  "modules": [
    {
      "moduleId": "string",
      "calls": 1,
      "errors": 1,
      "totalDurationMs": 1,
      "p95DurationMs": 1,
      "totalCpuMs": 1,
      "positiveHeapDeltaBytes": 1,
      "positiveRssDeltaBytes": 1,
      "surfaces": [
        {
          "surface": "api",
          "calls": 1,
          "errors": 1,
          "totalDurationMs": 1,
          "p95DurationMs": 1,
          "totalCpuMs": 1,
          "positiveHeapDeltaBytes": 1,
          "positiveRssDeltaBytes": 1
        }
      ],
      "topOperations": [
        {
          "moduleId": "string",
          "surface": "api",
          "operation": "string",
          "resourceId": null,
          "calls": 1,
          "errors": 1,
          "totalDurationMs": 1,
          "maxDurationMs": 1,
          "p95DurationMs": 1,
          "totalCpuUserMs": 1,
          "totalCpuSystemMs": 1,
          "maxCpuMs": 1,
          "totalHeapDeltaBytes": 1,
          "positiveHeapDeltaBytes": 1,
          "maxHeapDeltaBytes": 1,
          "totalRssDeltaBytes": 1,
          "positiveRssDeltaBytes": 1,
          "maxRssDeltaBytes": 1,
          "firstSeenAt": "string",
          "lastSeenAt": "string"
        }
      ],
      "candidateReasons": [
        "string"
      ]
    }
  ],
  "candidates": [
    {
      "moduleId": "string",
      "calls": 1,
      "errors": 1,
      "totalDurationMs": 1,
      "p95DurationMs": 1,
      "totalCpuMs": 1,
      "positiveHeapDeltaBytes": 1,
      "positiveRssDeltaBytes": 1,
      "surfaces": [
        {
          "surface": "api",
          "calls": 1,
          "errors": 1,
          "totalDurationMs": 1,
          "p95DurationMs": 1,
          "totalCpuMs": 1,
          "positiveHeapDeltaBytes": 1,
          "positiveRssDeltaBytes": 1
        }
      ],
      "topOperations": [
        {
          "moduleId": "string",
          "surface": "api",
          "operation": "string",
          "resourceId": null,
          "calls": 1,
          "errors": 1,
          "totalDurationMs": 1,
          "maxDurationMs": 1,
          "p95DurationMs": 1,
          "totalCpuUserMs": 1,
          "totalCpuSystemMs": 1,
          "maxCpuMs": 1,
          "totalHeapDeltaBytes": 1,
          "positiveHeapDeltaBytes": 1,
          "maxHeapDeltaBytes": 1,
          "totalRssDeltaBytes": 1,
          "positiveRssDeltaBytes": 1,
          "maxRssDeltaBytes": 1,
          "firstSeenAt": "string",
          "lastSeenAt": "string"
        }
      ],
      "candidateReasons": [
        "string"
      ]
    }
  ],
  "buckets": [
    {
      "bucketStart": "string",
      "bucketEnd": "string",
      "bucketIntervalMs": 1,
      "stage": "startup",
      "partial": true,
      "totals": {
        "modules": 1,
        "calls": 1,
        "errors": 1,
        "totalDurationMs": 1,
        "totalCpuMs": 1,
        "positiveHeapDeltaBytes": 1,
        "positiveRssDeltaBytes": 1
      },
      "modules": [
        {
          "moduleId": "string",
          "calls": 1,
          "errors": 1,
          "totalDurationMs": 1,
          "p95DurationMs": 1,
          "totalCpuMs": 1,
          "positiveHeapDeltaBytes": 1,
          "positiveRssDeltaBytes": 1,
          "surfaces": [
            {
              "surface": "api",
              "calls": 1,
              "errors": 1,
              "totalDurationMs": 1,
              "p95DurationMs": 1,
              "totalCpuMs": 1,
              "positiveHeapDeltaBytes": 1,
              "positiveRssDeltaBytes": 1
            }
          ],
          "topOperations": [
            {
              "moduleId": "string",
              "surface": "api",
              "operation": "string",
              "resourceId": null,
              "calls": 1,
              "errors": 1,
              "totalDurationMs": 1,
              "maxDurationMs": 1,
              "p95DurationMs": 1,
              "totalCpuUserMs": 1,
              "totalCpuSystemMs": 1,
              "maxCpuMs": 1,
              "totalHeapDeltaBytes": 1,
              "positiveHeapDeltaBytes": 1,
              "maxHeapDeltaBytes": 1,
              "totalRssDeltaBytes": 1,
              "positiveRssDeltaBytes": 1,
              "maxRssDeltaBytes": 1,
              "firstSeenAt": "string",
              "lastSeenAt": "string"
            }
          ],
          "candidateReasons": [
            "string"
          ]
        }
      ]
    }
  ]
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/configs/module-telemetry" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/configs/system-status`

Get system health status

Returns comprehensive system health information including environment details, version, resource usage, and service connectivity status.

Requires features: configs.system_status.view

**Tags:** Configs

**Requires authentication.**

**Features:** configs.system_status.view

### Responses

**200** – System status snapshot

Content-Type: `application/json`

```json
{
  "generatedAt": "string",
  "runtimeMode": "development",
  "categories": [
    {
      "key": "profiling",
      "labelKey": "string",
      "descriptionKey": null,
      "items": [
        {
          "key": "string",
          "category": "profiling",
          "kind": "boolean",
          "labelKey": "string",
          "descriptionKey": "string",
          "docUrl": null,
          "defaultValue": null,
          "state": "enabled",
          "value": null,
          "normalizedValue": null
        }
      ]
    }
  ]
}
```

**500** – Failed to load system status

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/configs/system-status" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/configs/system-status`

Clear system cache

Purges the entire cache for the current tenant. Useful for troubleshooting or forcing fresh data loading.

Requires features: configs.manage

**Tags:** Configs

**Requires authentication.**

**Features:** configs.manage

### Responses

**200** – Cache cleared successfully

Content-Type: `application/json`

```json
{
  "cleared": true
}
```

**500** – Failed to purge cache

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**503** – Cache service unavailable

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/configs/system-status" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/configs/upgrade-actions`

List pending upgrade actions

Returns a list of pending upgrade actions for the current version. These are one-time setup tasks that need to be executed after upgrading to a new version. Requires organization and tenant context.

Requires features: configs.manage

**Tags:** Configs

**Requires authentication.**

**Features:** configs.manage

### Responses

**200** – List of pending upgrade actions

Content-Type: `application/json`

```json
{
  "version": "string",
  "actions": [
    {
      "id": "string",
      "version": "string",
      "message": "string",
      "ctaLabel": "string",
      "successMessage": "string",
      "loadingLabel": "string"
    }
  ]
}
```

**400** – Missing organization or tenant context

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**500** – Failed to load upgrade actions

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/configs/upgrade-actions" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/configs/upgrade-actions`

Execute upgrade action

Executes a specific upgrade action by ID. Typically used for one-time setup tasks like seeding example data after version upgrade. Returns execution status and localized success message.

Requires features: configs.manage

**Tags:** Configs

**Requires authentication.**

**Features:** configs.manage

### Request Body

Content-Type: `application/json`

```json
{
  "actionId": "string"
}
```

### Responses

**200** – Upgrade action executed successfully

Content-Type: `application/json`

```json
{
  "status": "string",
  "message": "string",
  "version": "string"
}
```

**400** – Invalid request body or missing context

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**500** – Failed to execute upgrade action

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/configs/upgrade-actions" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"actionId\": \"string\"
}"
```

## GET `/cross_club_transfers`

List the caller club's outgoing and incoming agreements

Natively tenant-scoped: each club sees only its own side of the agreements it is party to. Listing lazily resolves lapsed proposals to expired and lapsed open windows to cancelled.

Requires features: cross_club_transfers.transfer.manage

**Tags:** Cross-club transfers

**Requires authentication.**

**Features:** cross_club_transfers.transfer.manage

### Responses

**200** – Agreements of the caller side

Content-Type: `application/json`

```json
{
  "items": [
    {
      "agreementRef": "00000000-0000-4000-8000-000000000000",
      "direction": "string",
      "status": "string",
      "counterpartyTenantId": "00000000-0000-4000-8000-000000000000",
      "sourcePlayerId": "00000000-0000-4000-8000-000000000000",
      "destinationPlayerId": null,
      "destinationTeamOrgId": "00000000-0000-4000-8000-000000000000",
      "summary": {
        "displayName": "string"
      },
      "proposedBy": "00000000-0000-4000-8000-000000000000",
      "expiresAt": "string",
      "openedAt": null,
      "completedAt": null,
      "cancelledAt": null
    }
  ]
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/cross_club_transfers" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/cross_club_transfers`

Propose a cross-club transfer of one player

Creates the mirrored agreement pair (outgoing in the source club, incoming in the destination). Carries only the source-authored summary - no encrypted personal data leaves the source club. Source club admin only.

Requires features: cross_club_transfers.transfer.manage

**Tags:** Cross-club transfers

**Requires authentication.**

**Features:** cross_club_transfers.transfer.manage

### Request Body

Content-Type: `application/json`

```json
{
  "sourcePlayerId": "00000000-0000-4000-8000-000000000000",
  "destinationTenantId": "00000000-0000-4000-8000-000000000000",
  "destinationTeamOrgId": "00000000-0000-4000-8000-000000000000",
  "summary": {
    "displayName": "string"
  }
}
```

### Responses

**201** – Created proposal

Content-Type: `application/json`

```json
{
  "agreementRef": "00000000-0000-4000-8000-000000000000",
  "expiresAt": "string"
}
```

**404** – Player not found in the source club

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**409** – An active agreement already exists for the player

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid input, own club, or unknown destination

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/cross_club_transfers" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"sourcePlayerId\": \"00000000-0000-4000-8000-000000000000\",
  \"destinationTenantId\": \"00000000-0000-4000-8000-000000000000\",
  \"destinationTeamOrgId\": \"00000000-0000-4000-8000-000000000000\",
  \"summary\": {
    \"displayName\": \"string\"
  }
}"
```

## POST `/cross_club_transfers/accept`

Accept a proposed transfer (destination club admin)

Both clubs have now consented: the transition window opens, a single-record read grant is issued, and a transitioning destination membership appears.

Requires features: cross_club_transfers.transfer.manage

**Tags:** Cross-club transfers

**Requires authentication.**

**Features:** cross_club_transfers.transfer.manage

### Request Body

Content-Type: `application/json`

```json
{
  "agreementRef": "00000000-0000-4000-8000-000000000000"
}
```

### Responses

**200** – Window opened

Content-Type: `application/json`

```json
{
  "ok": true
}
```

**404** – Not a party to this agreement

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**409** – Not proposed anymore (terminal, open, or expired)

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid input

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/cross_club_transfers/accept" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"agreementRef\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## POST `/cross_club_transfers/cancel`

Cancel an open transition window (either club admin)

Revokes the destination's scoped access and ends its transitioning membership, leaving the player wholly in the source club exactly as before the proposal.

Requires features: cross_club_transfers.transfer.manage

**Tags:** Cross-club transfers

**Requires authentication.**

**Features:** cross_club_transfers.transfer.manage

### Request Body

Content-Type: `application/json`

```json
{
  "agreementRef": "00000000-0000-4000-8000-000000000000"
}
```

### Responses

**200** – Cancelled

Content-Type: `application/json`

```json
{
  "ok": true
}
```

**404** – Not a party to this agreement

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**409** – Only an open window can be cancelled

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid input

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/cross_club_transfers/cancel" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"agreementRef\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## POST `/cross_club_transfers/complete`

Confirm completion on the caller's side

Completion is two-sided: each club admin confirms their own side. Once both have confirmed, the handover runs - the record is re-protected under the destination club, the source membership ends, the source profile is archived, and the scoped grant is revoked.

Requires features: cross_club_transfers.transfer.manage

**Tags:** Cross-club transfers

**Requires authentication.**

**Features:** cross_club_transfers.transfer.manage

### Request Body

Content-Type: `application/json`

```json
{
  "agreementRef": "00000000-0000-4000-8000-000000000000"
}
```

### Responses

**200** – Confirmation recorded; completed=true once both sides confirmed

Content-Type: `application/json`

```json
{
  "completed": true,
  "destinationPlayerId": null
}
```

**404** – Not a party, or the source record vanished

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**409** – Window not open or lapsed

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid input

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/cross_club_transfers/complete" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"agreementRef\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## POST `/cross_club_transfers/decline`

Decline a proposed transfer (destination club admin)

Ends the agreement on both sides; nothing about the player ever crossed the boundary.

Requires features: cross_club_transfers.transfer.manage

**Tags:** Cross-club transfers

**Requires authentication.**

**Features:** cross_club_transfers.transfer.manage

### Request Body

Content-Type: `application/json`

```json
{
  "agreementRef": "00000000-0000-4000-8000-000000000000"
}
```

### Responses

**200** – Declined

Content-Type: `application/json`

```json
{
  "ok": true
}
```

**404** – Not a party to this agreement

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**409** – Not proposed anymore

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid input

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/cross_club_transfers/decline" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"agreementRef\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/cross_club_transfers/record`

Read the transferring player's record during the open window

Destination club admin only, via the single-record time-boxed grant. Each read is individually audited with both clubs. Read-only; the record becomes writable in the destination only after completion re-protects it there.

Requires features: cross_club_transfers.transfer.manage

**Tags:** Cross-club transfers

**Requires authentication.**

**Features:** cross_club_transfers.transfer.manage

### Responses

**200** – The one granted record plus provenance

Content-Type: `application/json`

```json
{
  "playerId": "00000000-0000-4000-8000-000000000000",
  "firstName": null,
  "lastName": null,
  "dateOfBirth": null,
  "preferredPositions": null,
  "dominantFoot": null,
  "notes": null,
  "status": "string",
  "customerUserId": null,
  "provenance": {
    "sourceTenantId": "00000000-0000-4000-8000-000000000000",
    "agreementRef": "00000000-0000-4000-8000-000000000000"
  }
}
```

**404** – No visible record (not open, revoked, or not a party)

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**409** – The open window lapsed and was cancelled

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Missing or malformed agreement ref

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/cross_club_transfers/record" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/cross_club_transfers/withdraw`

Withdraw an un-accepted proposal (source club admin)

Only valid before acceptance - an open window resolves by completion or cancellation instead.

Requires features: cross_club_transfers.transfer.manage

**Tags:** Cross-club transfers

**Requires authentication.**

**Features:** cross_club_transfers.transfer.manage

### Request Body

Content-Type: `application/json`

```json
{
  "agreementRef": "00000000-0000-4000-8000-000000000000"
}
```

### Responses

**200** – Withdrawn

Content-Type: `application/json`

```json
{
  "ok": true
}
```

**404** – Not a party to this agreement

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**409** – Already accepted or terminal

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid input

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/cross_club_transfers/withdraw" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"agreementRef\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## DELETE `/customer_accounts/admin/domain-mappings`

Remove a custom domain

Removes the domain mapping identified by ?id=. Cache and Traefik routing drain within TTL.

Requires features: customer_accounts.domain.manage

**Tags:** CustomerAccounts

**Requires authentication.**

**Features:** customer_accounts.domain.manage

### Responses

**200** – OK

Content-Type: `application/json`

```json
{
  "ok": true
}
```

**400** – Bad request

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

### Example

```bash
curl -X DELETE "https://trainmaxr.pboj.smallhost.pl/api/customer_accounts/admin/domain-mappings" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/customer_accounts/admin/domain-mappings`

List domain mappings

Returns all custom-domain mappings for the current tenant, optionally filtered by organization.

Requires features: customer_accounts.domain.manage

**Tags:** CustomerAccounts

**Requires authentication.**

**Features:** customer_accounts.domain.manage

### Responses

**200** – OK

Content-Type: `application/json`

```json
{
  "ok": true,
  "domainMappings": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "hostname": "string",
      "organizationId": "00000000-0000-4000-8000-000000000000",
      "tenantId": "00000000-0000-4000-8000-000000000000",
      "provider": "traefik",
      "status": "pending",
      "verifiedAt": null,
      "lastDnsCheckAt": null,
      "dnsFailureReason": null,
      "tlsFailureReason": null,
      "tlsRetryCount": 1,
      "cnameTarget": null,
      "aRecordTarget": null,
      "createdAt": "string",
      "updatedAt": null
    }
  ],
  "config": {
    "cnameTarget": null,
    "aRecordTarget": null
  }
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/customer_accounts/admin/domain-mappings" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/customer_accounts/admin/domain-mappings`

Register a custom domain

Registers a new custom domain mapping for an organization. Verifies via DNS asynchronously.

Requires features: customer_accounts.domain.manage

**Tags:** CustomerAccounts

**Requires authentication.**

**Features:** customer_accounts.domain.manage

### Request Body

Content-Type: `application/json`

```json
{
  "hostname": "string",
  "organizationId": "00000000-0000-4000-8000-000000000000"
}
```

### Responses

**201** – Created

Content-Type: `application/json`

```json
{
  "ok": true,
  "domainMapping": {
    "id": "00000000-0000-4000-8000-000000000000",
    "hostname": "string",
    "organizationId": "00000000-0000-4000-8000-000000000000",
    "tenantId": "00000000-0000-4000-8000-000000000000",
    "provider": "traefik",
    "status": "pending",
    "verifiedAt": null,
    "lastDnsCheckAt": null,
    "dnsFailureReason": null,
    "tlsFailureReason": null,
    "tlsRetryCount": 1,
    "cnameTarget": null,
    "aRecordTarget": null,
    "createdAt": "string",
    "updatedAt": null
  }
}
```

**400** – Validation error

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**409** – Conflict

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/customer_accounts/admin/domain-mappings" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"hostname\": \"string\",
  \"organizationId\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## POST `/customer_accounts/admin/domain-mappings/{id}/health-check`

Trigger TLS health check

Runs an HTTPS probe to verify Traefik has provisioned a certificate. On success: transitions the mapping to active.

Requires features: customer_accounts.domain.manage

**Tags:** CustomerAccounts

**Requires authentication.**

**Features:** customer_accounts.domain.manage

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| id | path | any | Required |

### Responses

**200** – OK

Content-Type: `application/json`

```json
{
  "ok": true,
  "domainMapping": {
    "id": "00000000-0000-4000-8000-000000000000",
    "hostname": "string",
    "status": "string",
    "tlsRetryCount": 1,
    "tlsFailureReason": null
  }
}
```

**404** – Not found

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/customer_accounts/admin/domain-mappings/:id/health-check" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/customer_accounts/admin/domain-mappings/{id}/verify`

Trigger DNS verification

Runs DNS verification (CNAME → A → reverse-resolve fallback) for the domain mapping. Returns diagnostics on failure.

Requires features: customer_accounts.domain.manage

**Tags:** CustomerAccounts

**Requires authentication.**

**Features:** customer_accounts.domain.manage

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| id | path | any | Required |

### Responses

**200** – OK — see status

Content-Type: `application/json`

```json
{
  "ok": true,
  "domainMapping": {
    "id": "00000000-0000-4000-8000-000000000000",
    "hostname": "string",
    "status": "string",
    "verifiedAt": null,
    "lastDnsCheckAt": null,
    "dnsFailureReason": null
  },
  "diagnostics": null
}
```

**404** – Not found

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/customer_accounts/admin/domain-mappings/:id/verify" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/customer_accounts/admin/roles`

List customer roles (admin)

Returns all customer roles for the tenant.

**Tags:** Customer Accounts Admin

### Responses

**200** – Role list

Content-Type: `application/json`

```json
{
  "ok": true,
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "name": "string",
      "slug": "string",
      "description": null,
      "isDefault": true,
      "isSystem": true,
      "customerAssignable": true,
      "createdAt": "string",
      "updatedAt": null
    }
  ],
  "total": 1,
  "totalPages": 1,
  "page": 1
}
```

**401** – Not authenticated

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**403** – Insufficient permissions

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/customer_accounts/admin/roles" \
  -H "Accept: application/json"
```

## POST `/customer_accounts/admin/roles`

Create customer role (admin)

Creates a new customer role with an empty ACL.

**Tags:** Customer Accounts Admin

### Request Body

Content-Type: `application/json`

```json
{
  "name": "string",
  "slug": "string"
}
```

### Responses

**201** – Role created

Content-Type: `application/json`

```json
{
  "ok": true,
  "role": {
    "id": "00000000-0000-4000-8000-000000000000",
    "name": "string",
    "slug": "string",
    "description": null,
    "isDefault": true,
    "isSystem": true,
    "customerAssignable": true,
    "createdAt": "string"
  }
}
```

**400** – Validation failed

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**401** – Not authenticated

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**403** – Insufficient permissions

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**409** – Slug already exists

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/customer_accounts/admin/roles" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d "{
  \"name\": \"string\",
  \"slug\": \"string\"
}"
```

## DELETE `/customer_accounts/admin/roles/{id}`

Delete customer role (admin)

Soft deletes a customer role and its ACL. The default role (auto-assigned to new portal users) and roles with assigned users cannot be deleted.

**Tags:** Customer Accounts Admin

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| id | path | any | Required |

### Responses

**200** – Role deleted

Content-Type: `application/json`

```json
{
  "ok": true
}
```

**400** – Default role or has assigned users

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**401** – Not authenticated

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**403** – Insufficient permissions

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**404** – Role not found

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

### Example

```bash
curl -X DELETE "https://trainmaxr.pboj.smallhost.pl/api/customer_accounts/admin/roles/00000000-0000-4000-8000-000000000000" \
  -H "Accept: application/json"
```

## GET `/customer_accounts/admin/roles/{id}`

Get customer role detail (admin)

Returns full customer role details including ACL features.

**Tags:** Customer Accounts Admin

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| id | path | any | Required |

### Responses

**200** – Role detail with ACL

Content-Type: `application/json`

```json
{
  "ok": true,
  "role": {
    "id": "00000000-0000-4000-8000-000000000000",
    "name": "string",
    "slug": "string",
    "description": null,
    "isDefault": true,
    "isSystem": true,
    "customerAssignable": true,
    "createdAt": "string",
    "updatedAt": null,
    "acl": {
      "features": [
        "string"
      ],
      "isPortalAdmin": true
    }
  }
}
```

**401** – Not authenticated

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**403** – Insufficient permissions

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**404** – Role not found

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/customer_accounts/admin/roles/00000000-0000-4000-8000-000000000000" \
  -H "Accept: application/json"
```

## PUT `/customer_accounts/admin/roles/{id}`

Update customer role (admin)

Updates a customer role. System roles are protected from name changes.

**Tags:** Customer Accounts Admin

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| id | path | any | Required |

### Request Body

Content-Type: `application/json`

```json
{}
```

### Responses

**200** – Role updated

Content-Type: `application/json`

```json
{
  "ok": true
}
```

**400** – Validation failed or system role restriction

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**401** – Not authenticated

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**403** – Insufficient permissions

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**404** – Role not found

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

### Example

```bash
curl -X PUT "https://trainmaxr.pboj.smallhost.pl/api/customer_accounts/admin/roles/00000000-0000-4000-8000-000000000000" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d "{}"
```

## PUT `/customer_accounts/admin/roles/{id}/acl`

Update customer role ACL (admin)

Updates the ACL (features and portal admin flag) for a customer role. Invalidates RBAC cache after update.

**Tags:** Customer Accounts Admin

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| id | path | any | Required |

### Request Body

Content-Type: `application/json`

```json
{
  "features": [
    "string"
  ]
}
```

### Responses

**200** – ACL updated

Content-Type: `application/json`

```json
{
  "ok": true,
  "updatedAt": "string"
}
```

**400** – Validation failed

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**401** – Not authenticated

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**403** – Insufficient permissions

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**404** – Role not found

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**409** – Stale ACL write (optimistic-lock conflict)

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

### Example

```bash
curl -X PUT "https://trainmaxr.pboj.smallhost.pl/api/customer_accounts/admin/roles/00000000-0000-4000-8000-000000000000/acl" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d "{
  \"features\": [
    \"string\"
  ]
}"
```

## GET `/customer_accounts/admin/users`

List customer users (admin)

Returns a paginated list of customer users with roles. Supports filtering by status, company, role, and search.

**Tags:** Customer Accounts Admin

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| page | query | any | Optional |
| pageSize | query | any | Optional |
| status | query | any | Optional |
| customerEntityId | query | any | Optional |
| roleId | query | any | Optional |
| search | query | any | Optional |

### Responses

**200** – Paginated user list

Content-Type: `application/json`

```json
{
  "ok": true,
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "email": "string",
      "displayName": "string",
      "emailVerified": true,
      "isActive": true,
      "lockedUntil": null,
      "lastLoginAt": null,
      "customerEntityId": null,
      "personEntityId": null,
      "createdAt": "string",
      "updatedAt": null,
      "roles": [
        {
          "id": "00000000-0000-4000-8000-000000000000",
          "name": "string",
          "slug": "string"
        }
      ]
    }
  ],
  "total": 1,
  "totalPages": 1,
  "page": 1
}
```

**401** – Not authenticated

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**403** – Insufficient permissions

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/customer_accounts/admin/users" \
  -H "Accept: application/json"
```

## POST `/customer_accounts/admin/users`

Create customer user (admin)

Creates a new customer user directly. Staff-initiated, bypasses signup flow.

**Tags:** Customer Accounts Admin

### Request Body

Content-Type: `application/json`

```json
{
  "email": "user@example.com",
  "password": "string",
  "displayName": "string"
}
```

### Responses

**201** – User created

Content-Type: `application/json`

```json
{
  "ok": true,
  "user": {
    "id": "00000000-0000-4000-8000-000000000000",
    "email": "string",
    "displayName": "string"
  }
}
```

**400** – Validation failed

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**401** – Not authenticated

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**403** – Insufficient permissions

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**409** – Email already exists

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/customer_accounts/admin/users" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d "{
  \"email\": \"user@example.com\",
  \"password\": \"string\",
  \"displayName\": \"string\"
}"
```

## POST `/customer_accounts/admin/users-invite`

Invite customer user (admin)

Creates a staff-initiated invitation for a new customer user. The invitedByUserId is set from the staff auth context.

**Tags:** Customer Accounts Admin

### Request Body

Content-Type: `application/json`

```json
{
  "email": "user@example.com",
  "roleIds": [
    "00000000-0000-4000-8000-000000000000"
  ]
}
```

### Responses

**201** – Invitation created

Content-Type: `application/json`

```json
{
  "ok": true,
  "invitation": {
    "id": "00000000-0000-4000-8000-000000000000",
    "email": "string",
    "expiresAt": "string"
  }
}
```

**400** – Validation failed

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**401** – Not authenticated

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**403** – Insufficient permissions

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**429** – Too many invitation requests

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/customer_accounts/admin/users-invite" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d "{
  \"email\": \"user@example.com\",
  \"roleIds\": [
    \"00000000-0000-4000-8000-000000000000\"
  ]
}"
```

## DELETE `/customer_accounts/admin/users/{id}`

Delete customer user (admin)

Soft deletes a customer user and revokes all their active sessions.

**Tags:** Customer Accounts Admin

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| id | path | any | Required |

### Responses

**200** – User deleted

Content-Type: `application/json`

```json
{
  "ok": true
}
```

**401** – Not authenticated

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**403** – Insufficient permissions

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**404** – User not found

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

### Example

```bash
curl -X DELETE "https://trainmaxr.pboj.smallhost.pl/api/customer_accounts/admin/users/00000000-0000-4000-8000-000000000000" \
  -H "Accept: application/json"
```

## GET `/customer_accounts/admin/users/{id}`

Get customer user detail (admin)

Returns full customer user details including CRM links, roles, and active session count.

**Tags:** Customer Accounts Admin

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| id | path | any | Required |

### Responses

**200** – User detail

Content-Type: `application/json`

```json
{
  "ok": true,
  "user": {
    "id": "00000000-0000-4000-8000-000000000000",
    "email": "string",
    "displayName": "string",
    "emailVerified": true,
    "isActive": true,
    "lockedUntil": null,
    "lastLoginAt": null,
    "failedLoginAttempts": 1,
    "customerEntityId": null,
    "personEntityId": null,
    "createdAt": "string",
    "updatedAt": null,
    "roles": [
      {
        "id": "00000000-0000-4000-8000-000000000000",
        "name": "string",
        "slug": "string"
      }
    ],
    "activeSessionCount": 1
  }
}
```

**401** – Not authenticated

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**403** – Insufficient permissions

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**404** – User not found

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/customer_accounts/admin/users/00000000-0000-4000-8000-000000000000" \
  -H "Accept: application/json"
```

## PUT `/customer_accounts/admin/users/{id}`

Update customer user (admin)

Updates a customer user. Staff can update status, lock, CRM links, and roles. Role assignment bypasses customer_assignable check.

**Tags:** Customer Accounts Admin

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| id | path | any | Required |

### Request Body

Content-Type: `application/json`

```json
{
  "lockedUntil": null,
  "personEntityId": null,
  "customerEntityId": null
}
```

### Responses

**200** – User updated

Content-Type: `application/json`

```json
{
  "ok": true
}
```

**400** – Validation failed or role not found

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**401** – Not authenticated

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**403** – Insufficient permissions

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**404** – User not found

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

### Example

```bash
curl -X PUT "https://trainmaxr.pboj.smallhost.pl/api/customer_accounts/admin/users/00000000-0000-4000-8000-000000000000" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d "{
  \"lockedUntil\": null,
  \"personEntityId\": null,
  \"customerEntityId\": null
}"
```

## POST `/customer_accounts/admin/users/{id}/reset-password`

Reset customer user password (admin)

Allows staff to set a new password for a customer user.

**Tags:** Customer Accounts Admin

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| id | path | any | Required |

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – Password reset

Content-Type: `application/json`

```json
{
  "ok": true
}
```

**400** – Validation failed

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**401** – Not authenticated

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**403** – Insufficient permissions

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**404** – User not found

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/customer_accounts/admin/users/00000000-0000-4000-8000-000000000000/reset-password" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d "{
  \"newPassword\": \"string\"
}"
```

## POST `/customer_accounts/admin/users/{id}/send-reset-link`

Send password reset link for customer user (admin)

Creates a password reset token for a customer user and returns a reset link URL. The admin must prepend the appropriate portal domain/slug to the relative URL.

**Tags:** Customer Accounts Admin

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| id | path | any | Required |

### Responses

**200** – Reset link generated

Content-Type: `application/json`

```json
{
  "ok": true,
  "resetLink": "string"
}
```

**401** – Not authenticated

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**403** – Insufficient permissions

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**404** – User not found

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/customer_accounts/admin/users/00000000-0000-4000-8000-000000000000/send-reset-link" \
  -H "Accept: application/json"
```

## POST `/customer_accounts/admin/users/{id}/verify-email`

Verify customer user email (admin)

Allows staff to manually mark a customer user email as verified.

**Tags:** Customer Accounts Admin

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| id | path | any | Required |

### Responses

**200** – Email verified

Content-Type: `application/json`

```json
{
  "ok": true
}
```

**401** – Not authenticated

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**403** – Insufficient permissions

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**404** – User not found

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/customer_accounts/admin/users/00000000-0000-4000-8000-000000000000/verify-email" \
  -H "Accept: application/json"
```

## GET `/customer_accounts/domain-check`

Verify a hostname is allowed for TLS provisioning

Called by Traefik before issuing a Let's Encrypt certificate. Requires the X-Domain-Check-Secret header to match DOMAIN_CHECK_SECRET.

**Tags:** CustomerAccounts

### Responses

**200** – Hostname allowed

Content-Type: `application/json`

```json
{
  "ok": true
}
```

**403** – Forbidden

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**404** – Hostname not registered or not yet verified

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**503** – Server misconfiguration

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/customer_accounts/domain-check" \
  -H "Accept: application/json"
```

## GET `/customer_accounts/domain-resolve`

Single-host resolve for the Node middleware

Internal endpoint used by the portal middleware to populate its in-memory cache. Requires X-Domain-Resolve-Secret header.

**Tags:** CustomerAccounts

### Responses

**200** – OK

Content-Type: `application/json`

```json
{
  "ok": true,
  "tenantId": "00000000-0000-4000-8000-000000000000",
  "organizationId": "00000000-0000-4000-8000-000000000000",
  "orgSlug": null,
  "status": "active"
}
```

**400** – Bad request

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**403** – Forbidden

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**404** – Not found

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**503** – Server misconfiguration

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/customer_accounts/domain-resolve" \
  -H "Accept: application/json"
```

## GET `/customer_accounts/domain-resolve/all`

Batch warm-up for the Node middleware

Returns every active domain mapping in a single payload so the middleware can populate its cache on process start. Requires X-Domain-Resolve-Secret header.

**Tags:** CustomerAccounts

### Responses

**200** – OK

Content-Type: `application/json`

```json
{
  "ok": true,
  "domains": [
    {
      "hostname": "string",
      "tenantId": "00000000-0000-4000-8000-000000000000",
      "organizationId": "00000000-0000-4000-8000-000000000000",
      "orgSlug": null,
      "status": "active"
    }
  ]
}
```

**403** – Forbidden

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**429** – Too many requests

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**503** – Server misconfiguration

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/customer_accounts/domain-resolve/all" \
  -H "Accept: application/json"
```

## POST `/customer_accounts/email/verify`

Verify customer email address

Validates the email verification token and marks the email as verified.

**Tags:** Customer Authentication

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – Email verified

Content-Type: `application/json`

```json
{
  "ok": true
}
```

**400** – Invalid or expired token

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/customer_accounts/email/verify" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d "{
  \"token\": \"string\"
}"
```

## POST `/customer_accounts/invitations/accept`

Accept customer invitation

Accepts an invitation, creates the user account, assigns roles, and auto-logs in.

**Tags:** Customer Authentication

### Request Body

Content-Type: `application/json`

```json
{
  "token": "string",
  "password": "string",
  "displayName": "string"
}
```

### Responses

**201** – Invitation accepted and user created

Content-Type: `application/json`

```json
{
  "ok": true,
  "user": {
    "id": "00000000-0000-4000-8000-000000000000",
    "email": "user@example.com",
    "displayName": "string",
    "emailVerified": true
  },
  "resolvedFeatures": [
    "string"
  ]
}
```

**400** – Invalid or expired invitation

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/customer_accounts/invitations/accept" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d "{
  \"token\": \"string\",
  \"password\": \"string\",
  \"displayName\": \"string\"
}"
```

## POST `/customer_accounts/login`

Authenticate customer credentials

Validates customer credentials and issues JWT + session cookies.

**Tags:** Customer Authentication

### Request Body

Content-Type: `application/json`

```json
{
  "email": "user@example.com",
  "password": "string"
}
```

### Responses

**200** – Login successful

Content-Type: `application/json`

```json
{
  "ok": true,
  "user": {
    "id": "00000000-0000-4000-8000-000000000000",
    "email": "user@example.com",
    "displayName": "string",
    "emailVerified": true
  },
  "resolvedFeatures": [
    "string"
  ]
}
```

**400** – Validation failed

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**401** – Invalid credentials

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**429** – Too many login attempts

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/customer_accounts/login" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d "{
  \"email\": \"user@example.com\",
  \"password\": \"string\"
}"
```

## POST `/customer_accounts/magic-link/request`

Request magic link login

Sends a magic link to the customer email. Always returns 200 to prevent enumeration.

**Tags:** Customer Authentication

### Request Body

Content-Type: `application/json`

```json
{
  "email": "user@example.com"
}
```

### Responses

**200** – Request accepted

Content-Type: `application/json`

```json
{
  "ok": true
}
```

**429** – Too many requests

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/customer_accounts/magic-link/request" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d "{
  \"email\": \"user@example.com\"
}"
```

## POST `/customer_accounts/magic-link/verify`

Verify magic link token

Validates the magic link token, auto-verifies email, and creates a session.

**Tags:** Customer Authentication

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – Login successful

Content-Type: `application/json`

```json
{
  "ok": true,
  "user": {
    "id": "00000000-0000-4000-8000-000000000000",
    "email": "user@example.com",
    "displayName": "string",
    "emailVerified": true
  },
  "resolvedFeatures": [
    "string"
  ]
}
```

**400** – Invalid or expired token

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**401** – Account not found

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/customer_accounts/magic-link/verify" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d "{
  \"token\": \"string\"
}"
```

## POST `/customer_accounts/password/reset-confirm`

Confirm customer password reset

Validates the reset token and sets a new password. Revokes all existing sessions.

**Tags:** Customer Authentication

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – Password reset successful

Content-Type: `application/json`

```json
{
  "ok": true
}
```

**400** – Invalid or expired token

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/customer_accounts/password/reset-confirm" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d "{
  \"token\": \"string\",
  \"password\": \"string\"
}"
```

## POST `/customer_accounts/password/reset-request`

Request customer password reset

Initiates a password reset flow. Always returns 200 to prevent email enumeration.

**Tags:** Customer Authentication

### Request Body

Content-Type: `application/json`

```json
{
  "email": "user@example.com"
}
```

### Responses

**200** – Request accepted

Content-Type: `application/json`

```json
{
  "ok": true
}
```

**429** – Too many requests

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/customer_accounts/password/reset-request" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d "{
  \"email\": \"user@example.com\"
}"
```

## GET `/customer_accounts/portal/events/stream`

Subscribe to portal events via SSE (Portal Event Bridge)

Long-lived SSE connection that receives server-side events marked with portalBroadcast: true. Events are filtered by the customer's tenant, organization, and recipient user audience.

**Tags:** Customer Portal

### Responses

**200** – Event stream (text/event-stream)

Content-Type: `application/json`

**401** – Not authenticated

Content-Type: `application/json`

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/customer_accounts/portal/events/stream" \
  -H "Accept: application/json"
```

## POST `/customer_accounts/portal/feature-check`

Check customer portal feature access

Checks which of the requested features the authenticated customer user has. Used by portal menu injection for feature-gating.

**Tags:** Customer Portal

### Request Body

Content-Type: `application/json`

```json
{
  "features": [
    "string"
  ]
}
```

### Responses

**200** – Feature check result

Content-Type: `application/json`

```json
{
  "ok": true,
  "granted": [
    "string"
  ]
}
```

**400** – Invalid request

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**401** – Not authenticated

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/customer_accounts/portal/feature-check" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d "{
  \"features\": [
    \"string\"
  ]
}"
```

## POST `/customer_accounts/portal/logout`

Customer logout

Revokes the current session and clears authentication cookies.

**Tags:** Customer Portal

### Responses

**200** – Logged out

Content-Type: `application/json`

```json
{
  "ok": true
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/customer_accounts/portal/logout" \
  -H "Accept: application/json"
```

## GET `/customer_accounts/portal/nav`

Portal sidebar navigation

Returns the portal sidebar for the authenticated customer. Items are derived from each portal page's `nav` metadata and filtered by `requireCustomerFeatures` against the customer's grants (wildcards honored).

**Tags:** Customer Portal

### Responses

**200** – Portal sidebar groups

Content-Type: `application/json`

```json
{
  "ok": true,
  "orgSlug": "string",
  "groups": [
    {
      "id": "main",
      "items": [
        {
          "id": "string",
          "label": "string",
          "href": "string",
          "order": 1
        }
      ]
    }
  ],
  "grantedFeatures": [
    "string"
  ],
  "isPortalAdmin": true
}
```

**401** – Not authenticated

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**404** – Organization not found

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/customer_accounts/portal/nav" \
  -H "Accept: application/json"
```

## GET `/customer_accounts/portal/notifications`

List customer notifications

Returns paginated notifications for the authenticated customer user. Dismissed notifications are excluded by default unless ?status=dismissed is specified.

**Tags:** Customer Portal

### Responses

**200** – Notification list

Content-Type: `application/json`

```json
{
  "ok": true,
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "type": "string",
      "title": "string",
      "body": null,
      "titleKey": null,
      "bodyKey": null,
      "titleVariables": null,
      "bodyVariables": null,
      "icon": null,
      "severity": "info",
      "status": "unread",
      "actions": [
        {
          "id": "string",
          "label": "string"
        }
      ],
      "sourceModule": null,
      "sourceEntityType": null,
      "sourceEntityId": null,
      "linkHref": null,
      "createdAt": "string",
      "readAt": null,
      "actionTaken": null
    }
  ],
  "total": 1,
  "page": 1,
  "pageSize": 1
}
```

**401** – Not authenticated

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/customer_accounts/portal/notifications" \
  -H "Accept: application/json"
```

## PUT `/customer_accounts/portal/notifications/{id}/dismiss`

Dismiss notification

Dismisses a single notification for the authenticated customer user.

**Tags:** Customer Portal

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| id | path | any | Required |

### Responses

**200** – Notification dismissed

Content-Type: `application/json`

```json
{
  "ok": true
}
```

**401** – Not authenticated

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**404** – Notification not found

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

### Example

```bash
curl -X PUT "https://trainmaxr.pboj.smallhost.pl/api/customer_accounts/portal/notifications/00000000-0000-4000-8000-000000000000/dismiss" \
  -H "Accept: application/json"
```

## PUT `/customer_accounts/portal/notifications/{id}/read`

Mark notification as read

Marks a single notification as read for the authenticated customer user.

**Tags:** Customer Portal

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| id | path | any | Required |

### Responses

**200** – Notification marked as read

Content-Type: `application/json`

```json
{
  "ok": true
}
```

**401** – Not authenticated

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**404** – Notification not found

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

### Example

```bash
curl -X PUT "https://trainmaxr.pboj.smallhost.pl/api/customer_accounts/portal/notifications/00000000-0000-4000-8000-000000000000/read" \
  -H "Accept: application/json"
```

## PUT `/customer_accounts/portal/notifications/mark-all-read`

Mark all notifications as read

Marks all unread notifications as read for the authenticated customer user.

**Tags:** Customer Portal

### Responses

**200** – All notifications marked as read

Content-Type: `application/json`

```json
{
  "ok": true,
  "count": 1
}
```

**401** – Not authenticated

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

### Example

```bash
curl -X PUT "https://trainmaxr.pboj.smallhost.pl/api/customer_accounts/portal/notifications/mark-all-read" \
  -H "Accept: application/json"
```

## GET `/customer_accounts/portal/notifications/unread-count`

Get unread notification count

Returns the number of unread notifications for the authenticated customer user.

**Tags:** Customer Portal

### Responses

**200** – Unread count

Content-Type: `application/json`

```json
{
  "ok": true,
  "unreadCount": 1
}
```

**401** – Not authenticated

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/customer_accounts/portal/notifications/unread-count" \
  -H "Accept: application/json"
```

## POST `/customer_accounts/portal/password-change`

Change customer password

Changes the authenticated customer user password after verifying the current password. Revokes all existing sessions.

**Tags:** Customer Portal

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – Password changed

Content-Type: `application/json`

```json
{
  "ok": true
}
```

**400** – Current password incorrect or validation failed

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**401** – Not authenticated

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/customer_accounts/portal/password-change" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d "{
  \"currentPassword\": \"string\",
  \"newPassword\": \"string\"
}"
```

## GET `/customer_accounts/portal/profile`

Get customer profile

Returns the authenticated customer user profile with roles and permissions.

**Tags:** Customer Portal

### Responses

**200** – Profile data

Content-Type: `application/json`

```json
{
  "ok": true,
  "user": {
    "id": "00000000-0000-4000-8000-000000000000",
    "email": "string",
    "displayName": "string",
    "emailVerified": true,
    "customerEntityId": null,
    "personEntityId": null,
    "isActive": true,
    "lastLoginAt": null,
    "createdAt": "string"
  },
  "roles": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "name": "string",
      "slug": "string"
    }
  ],
  "resolvedFeatures": [
    "string"
  ],
  "isPortalAdmin": true
}
```

**401** – Not authenticated

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/customer_accounts/portal/profile" \
  -H "Accept: application/json"
```

## PUT `/customer_accounts/portal/profile`

Update customer profile

Updates the authenticated customer user profile.

**Tags:** Customer Portal

### Request Body

Content-Type: `application/json`

```json
{}
```

### Responses

**200** – Profile updated

Content-Type: `application/json`

```json
{
  "ok": true,
  "user": {
    "id": "00000000-0000-4000-8000-000000000000",
    "email": "string",
    "displayName": "string"
  }
}
```

**401** – Not authenticated

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**403** – Insufficient permissions

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

### Example

```bash
curl -X PUT "https://trainmaxr.pboj.smallhost.pl/api/customer_accounts/portal/profile" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d "{}"
```

## GET `/customer_accounts/portal/sessions`

List customer sessions

Returns active sessions for the authenticated customer user.

**Tags:** Customer Portal

### Responses

**200** – Session list

Content-Type: `application/json`

```json
{
  "ok": true,
  "sessions": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "ipAddress": null,
      "userAgent": null,
      "lastUsedAt": null,
      "createdAt": "string",
      "expiresAt": "string",
      "isCurrent": true
    }
  ]
}
```

**401** – Not authenticated

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/customer_accounts/portal/sessions" \
  -H "Accept: application/json"
```

## POST `/customer_accounts/portal/sessions-refresh`

Refresh customer JWT from session token

Uses the session cookie to issue a fresh JWT access token.

**Tags:** Customer Portal

### Responses

**200** – Token refreshed

Content-Type: `application/json`

```json
{
  "ok": true,
  "resolvedFeatures": [
    "string"
  ]
}
```

**401** – Invalid session

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/customer_accounts/portal/sessions-refresh" \
  -H "Accept: application/json"
```

## DELETE `/customer_accounts/portal/sessions/{id}`

Revoke a customer session

Revokes a specific session (not the current one).

**Tags:** Customer Portal

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| id | path | any | Required |

### Responses

**200** – Session revoked

Content-Type: `application/json`

```json
{
  "ok": true
}
```

**400** – Cannot revoke current session

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**401** – Not authenticated

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**404** – Session not found

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

### Example

```bash
curl -X DELETE "https://trainmaxr.pboj.smallhost.pl/api/customer_accounts/portal/sessions/00000000-0000-4000-8000-000000000000" \
  -H "Accept: application/json"
```

## GET `/customer_accounts/portal/users`

List company portal users

Lists portal users associated with the same company. Paginated (default pageSize 25, max 100).

**Tags:** Customer Portal

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| page | query | any | Optional |
| pageSize | query | any | Optional |

### Responses

**200** – Paginated user list

Content-Type: `application/json`

```json
{
  "ok": true,
  "users": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "email": "string",
      "displayName": "string",
      "emailVerified": true,
      "isActive": true,
      "lastLoginAt": null,
      "createdAt": "string",
      "roles": [
        {
          "id": "00000000-0000-4000-8000-000000000000",
          "name": "string",
          "slug": "string"
        }
      ]
    }
  ],
  "total": 1,
  "totalPages": 1,
  "page": 1,
  "pageSize": 1
}
```

**401** – Not authenticated

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**403** – Insufficient permissions

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/customer_accounts/portal/users" \
  -H "Accept: application/json"
```

## POST `/customer_accounts/portal/users-invite`

Invite a user to the company portal

Creates an invitation for a new user to join the company portal.

**Tags:** Customer Portal

### Request Body

Content-Type: `application/json`

```json
{
  "email": "user@example.com",
  "roleIds": [
    "00000000-0000-4000-8000-000000000000"
  ]
}
```

### Responses

**201** – Invitation created

Content-Type: `application/json`

```json
{
  "ok": true,
  "invitation": {
    "id": "00000000-0000-4000-8000-000000000000",
    "email": "string",
    "expiresAt": "string"
  }
}
```

**400** – Validation failed

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**401** – Not authenticated

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**403** – Insufficient permissions or non-assignable role

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**429** – Too many invitation requests

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/customer_accounts/portal/users-invite" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d "{
  \"email\": \"user@example.com\",
  \"roleIds\": [
    \"00000000-0000-4000-8000-000000000000\"
  ]
}"
```

## DELETE `/customer_accounts/portal/users/{id}`

Delete a company portal user

Soft deletes a portal user and revokes all their sessions.

**Tags:** Customer Portal

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| id | path | any | Required |

### Responses

**200** – User deleted

Content-Type: `application/json`

```json
{
  "ok": true
}
```

**400** – Cannot delete self

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**401** – Not authenticated

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**403** – Insufficient permissions

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**404** – User not found

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

### Example

```bash
curl -X DELETE "https://trainmaxr.pboj.smallhost.pl/api/customer_accounts/portal/users/00000000-0000-4000-8000-000000000000" \
  -H "Accept: application/json"
```

## PUT `/customer_accounts/portal/users/{id}/roles`

Update portal user roles

Assigns new roles to a company portal user.

**Tags:** Customer Portal

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| id | path | any | Required |

### Request Body

Content-Type: `application/json`

```json
{
  "roleIds": [
    "00000000-0000-4000-8000-000000000000"
  ]
}
```

### Responses

**200** – Roles updated

Content-Type: `application/json`

```json
{
  "ok": true
}
```

**400** – Validation failed

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**401** – Not authenticated

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**403** – Insufficient permissions

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**404** – User not found

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

### Example

```bash
curl -X PUT "https://trainmaxr.pboj.smallhost.pl/api/customer_accounts/portal/users/00000000-0000-4000-8000-000000000000/roles" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d "{
  \"roleIds\": [
    \"00000000-0000-4000-8000-000000000000\"
  ]
}"
```

## POST `/customer_accounts/signup`

Register a new customer account

Accepts a signup request and always returns 202 to prevent account enumeration.

**Tags:** Customer Authentication

### Request Body

Content-Type: `application/json`

```json
{
  "email": "user@example.com",
  "password": "string",
  "displayName": "string"
}
```

### Responses

**202** – Signup accepted

Content-Type: `application/json`

```json
{
  "ok": true
}
```

**400** – Validation failed or invalid request origin

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

**429** – Too many signup attempts

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**500** – Signup email origin is not configured

Content-Type: `application/json`

```json
{
  "ok": false,
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/customer_accounts/signup" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d "{
  \"email\": \"user@example.com\",
  \"password\": \"string\",
  \"displayName\": \"string\"
}"
```

## GET `/dashboards/layout`

Load the current dashboard layout

Returns the saved widget layout together with the widgets the current user is allowed to place.

Requires features: dashboards.view

**Tags:** Dashboards

**Requires authentication.**

**Features:** dashboards.view

### Responses

**200** – Current dashboard layout and available widgets.

Content-Type: `application/json`

```json
{
  "layout": {
    "items": [
      {
        "id": "00000000-0000-4000-8000-000000000000",
        "widgetId": "string",
        "order": 1
      }
    ]
  },
  "allowedWidgetIds": [
    "string"
  ],
  "canConfigure": true,
  "context": {
    "userId": "00000000-0000-4000-8000-000000000000",
    "tenantId": null,
    "organizationId": null,
    "userName": null,
    "userEmail": null,
    "userLabel": "string"
  },
  "widgets": [
    {
      "id": "string",
      "title": "string",
      "description": null,
      "defaultSize": "sm",
      "defaultEnabled": true,
      "defaultSettings": null,
      "features": [
        "string"
      ],
      "moduleId": "string",
      "icon": null,
      "loaderKey": "string",
      "supportsRefresh": true
    }
  ]
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/dashboards/layout" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## PUT `/dashboards/layout`

Persist dashboard layout changes

Saves the provided widget ordering, sizes, and settings for the current user.

Requires features: dashboards.configure

**Tags:** Dashboards

**Requires authentication.**

**Features:** dashboards.configure

### Request Body

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "widgetId": "string",
      "order": 1
    }
  ]
}
```

### Responses

**200** – Layout updated successfully.

Content-Type: `application/json`

```json
{
  "ok": true
}
```

**400** – Invalid layout payload

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X PUT "https://trainmaxr.pboj.smallhost.pl/api/dashboards/layout" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"items\": [
    {
      \"id\": \"00000000-0000-4000-8000-000000000000\",
      \"widgetId\": \"string\",
      \"order\": 1
    }
  ]
}"
```

## PATCH `/dashboards/layout/{itemId}`

Update a dashboard layout item

Adjusts the size or settings for a single widget within the dashboard layout.

Requires features: dashboards.configure

**Tags:** Dashboards

**Requires authentication.**

**Features:** dashboards.configure

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| itemId | path | any | Required |

### Request Body

Content-Type: `application/json`

```json
{}
```

### Responses

**200** – Layout item updated.

Content-Type: `application/json`

```json
{
  "ok": true
}
```

**400** – Invalid payload or missing item id

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**404** – Item not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X PATCH "https://trainmaxr.pboj.smallhost.pl/api/dashboards/layout/00000000-0000-4000-8000-000000000000" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{}"
```

## GET `/dashboards/roles/widgets`

Fetch widget assignments for a role

Returns the widgets explicitly assigned to the given role together with the evaluation scope.

Requires features: dashboards.admin.assign-widgets

**Tags:** Dashboards

**Requires authentication.**

**Features:** dashboards.admin.assign-widgets

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| roleId | query | any | Required |
| tenantId | query | any | Optional |
| organizationId | query | any | Optional |

### Responses

**200** – Current widget configuration for the role.

Content-Type: `application/json`

```json
{
  "widgetIds": [
    "string"
  ],
  "hasCustom": true,
  "scope": {
    "tenantId": null,
    "organizationId": null
  }
}
```

**400** – Missing role identifier

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/dashboards/roles/widgets?roleId=00000000-0000-4000-8000-000000000000" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## PUT `/dashboards/roles/widgets`

Update widgets assigned to a role

Persists the widget list for a role within the provided tenant and organization scope.

Requires features: dashboards.admin.assign-widgets

**Tags:** Dashboards

**Requires authentication.**

**Features:** dashboards.admin.assign-widgets

### Request Body

Content-Type: `application/json`

```json
{
  "roleId": "00000000-0000-4000-8000-000000000000",
  "widgetIds": [
    "string"
  ]
}
```

### Responses

**200** – Widgets updated successfully.

Content-Type: `application/json`

```json
{
  "ok": true,
  "widgetIds": [
    "string"
  ]
}
```

**400** – Invalid payload or unknown widgets

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X PUT "https://trainmaxr.pboj.smallhost.pl/api/dashboards/roles/widgets" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"roleId\": \"00000000-0000-4000-8000-000000000000\",
  \"widgetIds\": [
    \"string\"
  ]
}"
```

## GET `/dashboards/users/widgets`

Read widget overrides for a user

Returns the widgets inherited and explicitly configured for the requested user within the current scope.

Requires features: dashboards.admin.assign-widgets

**Tags:** Dashboards

**Requires authentication.**

**Features:** dashboards.admin.assign-widgets

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| userId | query | any | Required |
| tenantId | query | any | Optional |
| organizationId | query | any | Optional |

### Responses

**200** – Widget settings for the user.

Content-Type: `application/json`

```json
{
  "mode": "inherit",
  "widgetIds": [
    "string"
  ],
  "hasCustom": true,
  "effectiveWidgetIds": [
    "string"
  ],
  "scope": {
    "tenantId": null,
    "organizationId": null
  }
}
```

**400** – Missing user identifier

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/dashboards/users/widgets?userId=00000000-0000-4000-8000-000000000000" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## PUT `/dashboards/users/widgets`

Update user-specific dashboard widgets

Sets the widget override mode and allowed widgets for a user. Passing `mode: inherit` clears overrides.

Requires features: dashboards.admin.assign-widgets

**Tags:** Dashboards

**Requires authentication.**

**Features:** dashboards.admin.assign-widgets

### Request Body

Content-Type: `application/json`

```json
{
  "userId": "00000000-0000-4000-8000-000000000000",
  "mode": "inherit",
  "widgetIds": [
    "string"
  ]
}
```

### Responses

**200** – Overrides saved.

Content-Type: `application/json`

```json
{
  "ok": true,
  "mode": "inherit",
  "widgetIds": [
    "string"
  ]
}
```

**400** – Invalid payload or unknown widgets

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X PUT "https://trainmaxr.pboj.smallhost.pl/api/dashboards/users/widgets" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"userId\": \"00000000-0000-4000-8000-000000000000\",
  \"mode\": \"inherit\",
  \"widgetIds\": [
    \"string\"
  ]
}"
```

## GET `/dashboards/widgets/catalog`

List available dashboard widgets

Returns the catalog of widgets that modules expose, including defaults and feature requirements.

Requires features: dashboards.admin.assign-widgets

**Tags:** Dashboards

**Requires authentication.**

**Features:** dashboards.admin.assign-widgets

### Responses

**200** – Widgets available for assignment.

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "string",
      "title": "string",
      "description": null,
      "defaultSize": "sm",
      "defaultEnabled": true,
      "defaultSettings": null,
      "features": [
        "string"
      ],
      "moduleId": "string",
      "icon": null,
      "loaderKey": "string",
      "supportsRefresh": true
    }
  ]
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/dashboards/widgets/catalog" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/dashboards/widgets/data`

Fetch aggregated data for dashboard widgets

Executes an aggregation query against the specified entity type and returns the result. Supports date range filtering, grouping, and period-over-period comparison.

Requires features: analytics.view

**Tags:** Dashboards

**Requires authentication.**

**Features:** analytics.view

### Request Body

Content-Type: `application/json`

```json
{
  "entityType": "string",
  "metric": {
    "field": "string",
    "aggregate": "count"
  }
}
```

### Responses

**200** – Aggregated data for the widget.

Content-Type: `application/json`

```json
{
  "value": null,
  "data": [
    {
      "value": null
    }
  ],
  "metadata": {
    "fetchedAt": "string",
    "recordCount": 1
  }
}
```

**400** – Invalid request payload

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**500** – Internal server error

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/dashboards/widgets/data" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"entityType\": \"string\",
  \"metric\": {
    \"field\": \"string\",
    \"aggregate\": \"count\"
  }
}"
```

## POST `/dashboards/widgets/data/batch`

Fetch aggregated data for multiple dashboard widgets in one request

Resolves a batch of widget data requests with a single authentication, RBAC, organization-scope, and database-context setup. Each request is keyed by an opaque widget id and resolved independently, so a failure in one widget does not fail the batch.

Requires features: analytics.view

**Tags:** Dashboards

**Requires authentication.**

**Features:** analytics.view

### Request Body

Content-Type: `application/json`

```json
{
  "requests": [
    {
      "id": "string",
      "request": {
        "entityType": "string",
        "metric": {
          "field": "string",
          "aggregate": "count"
        }
      }
    }
  ]
}
```

### Responses

**200** – Per-widget aggregation results keyed by request id.

Content-Type: `application/json`

```json
{
  "results": [
    {
      "id": "string",
      "ok": true,
      "data": {
        "value": null,
        "data": [
          {
            "value": null
          }
        ],
        "metadata": {
          "fetchedAt": "string",
          "recordCount": 1
        }
      }
    }
  ]
}
```

**400** – Invalid request payload

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**500** – Internal server error

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/dashboards/widgets/data/batch" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"requests\": [
    {
      \"id\": \"string\",
      \"request\": {
        \"entityType\": \"string\",
        \"metric\": {
          \"field\": \"string\",
          \"aggregate\": \"count\"
        }
      }
    }
  ]
}"
```

## GET `/data_archival_and_retention/requests`

List recorded deletion requests

The club-scoped audit ledger of request-only deletions: who asked, on what basis, and execution state.

Requires features: data_archival_and_retention.archival.manage

**Tags:** Data archival and retention

**Requires authentication.**

**Features:** data_archival_and_retention.archival.manage

### Responses

**200** – Deletion requests, newest first

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "playerId": "00000000-0000-4000-8000-000000000000",
      "requestedBy": "player",
      "eligibilityBasis": "self_legal_age",
      "intake": "out_of_band",
      "status": "recorded",
      "requestedAt": "string",
      "executedAt": null
    }
  ]
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/data_archival_and_retention/requests" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/data_archival_and_retention/requests`

Record an out-of-band deletion request

Validates the requester against the club legal age (player at/above, else guardian) while the date of birth still exists, then records the request. Execution is a separate call.

Requires features: data_archival_and_retention.archival.manage

**Tags:** Data archival and retention

**Requires authentication.**

**Features:** data_archival_and_retention.archival.manage

### Request Body

Content-Type: `application/json`

```json
{
  "playerId": "00000000-0000-4000-8000-000000000000",
  "requestedBy": "player",
  "intake": "out_of_band"
}
```

### Responses

**201** – Recorded request id

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

**404** – Player not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Missing requester, ineligible requester, or anonymized record

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/data_archival_and_retention/requests" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"playerId\": \"00000000-0000-4000-8000-000000000000\",
  \"requestedBy\": \"player\",
  \"intake\": \"out_of_band\"
}"
```

## POST `/data_archival_and_retention/requests/execute`

Execute an eligible deletion request (hard-remove cascade)

Hard-removes the player across every per-player domain, profile last, and stamps the ledger. An active player needs a recorded request or an inline eligible requester; an anonymized record purges without a data-subject gate.

Requires features: data_archival_and_retention.archival.manage

**Tags:** Data archival and retention

**Requires authentication.**

**Features:** data_archival_and_retention.archival.manage

### Request Body

Content-Type: `application/json`

```json
{
  "playerId": "00000000-0000-4000-8000-000000000000"
}
```

### Responses

**200** – Purged

Content-Type: `application/json`

```json
{
  "ok": true,
  "alreadyAnonymized": true
}
```

**404** – Player not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – No recorded request or ineligible requester

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/data_archival_and_retention/requests/execute" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"playerId\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## POST `/data_archival_and_retention/sweep`

Run the retention sweep for the caller club

Purges anonymized players archived strictly longer ago than the club retention window (null = indefinite, nothing purged) and re-applies the anonymization contract as the event backstop.

Requires features: data_archival_and_retention.archival.manage

**Tags:** Data archival and retention

**Requires authentication.**

**Features:** data_archival_and_retention.archival.manage

### Responses

**200** – Sweep counts

Content-Type: `application/json`

```json
{
  "backstopped": 1,
  "purged": 1
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/data_archival_and_retention/sweep" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/directory/organization-branding`

Read sidebar branding for the selected organization

Returns the logo URL used by the backend sidebar for the currently selected organization.

Requires features: directory.organizations.view

**Tags:** Directory

**Requires authentication.**

**Features:** directory.organizations.view

### Responses

**200** – Organization branding

Content-Type: `application/json`

```json
{
  "organizationId": "00000000-0000-4000-8000-000000000000",
  "organizationName": "string",
  "tenantId": "00000000-0000-4000-8000-000000000000",
  "logoUrl": null,
  "updatedAt": null
}
```

**400** – A concrete organization scope is required

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**404** – Organization not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/directory/organization-branding" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## PUT `/directory/organization-branding`

Update sidebar branding for the selected organization

Stores an external image URL or an internal attachment image URL as the selected organization logo.

Requires features: directory.organizations.manage

**Tags:** Directory

**Requires authentication.**

**Features:** directory.organizations.manage

### Request Body

Content-Type: `application/json`

```json
{
  "logoUrl": null
}
```

### Responses

**200** – Updated organization branding

Content-Type: `application/json`

```json
{
  "organizationId": "00000000-0000-4000-8000-000000000000",
  "organizationName": "string",
  "tenantId": "00000000-0000-4000-8000-000000000000",
  "logoUrl": null,
  "updatedAt": null
}
```

**400** – Save failed

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**409** – Organization branding changed since it was loaded

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid logo URL

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X PUT "https://trainmaxr.pboj.smallhost.pl/api/directory/organization-branding" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"logoUrl\": null
}"
```

## GET `/directory/organization-switcher`

Load organization switcher menu

Returns the hierarchical menu of organizations the current user may switch to within the active tenant.

**Tags:** Directory

**Requires authentication.**

### Responses

**200** – Organization switcher payload.

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "name": "string",
      "depth": 1,
      "selectable": true,
      "children": []
    }
  ],
  "selectedId": null,
  "canManage": true,
  "canViewAllOrganizations": true,
  "tenantId": null,
  "tenants": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "name": "string",
      "isActive": true
    }
  ],
  "isSuperAdmin": true
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/directory/organization-switcher" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## DELETE `/directory/organizations`

Delete organization

Soft deletes an organization identified by id.

Requires features: directory.organizations.manage

**Tags:** Directory

**Requires authentication.**

**Features:** directory.organizations.manage

### Request Body

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

### Responses

**200** – Organization deleted.

Content-Type: `application/json`

```json
{
  "ok": true
}
```

**400** – Validation failed

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X DELETE "https://trainmaxr.pboj.smallhost.pl/api/directory/organizations" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/directory/organizations`

List organizations

Returns organizations using options, tree, or paginated manage view depending on the `view` parameter.

Requires features: directory.organizations.view

**Tags:** Directory

**Requires authentication.**

**Features:** directory.organizations.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| page | query | any | Optional |
| pageSize | query | any | Optional |
| search | query | any | Optional |
| view | query | any | Optional |
| ids | query | any | Optional |
| tenantId | query | any | Optional |
| includeInactive | query | any | Optional |
| status | query | any | Optional |

### Responses

**200** – Organization data for the requested view.

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "name": "string",
      "parentId": null,
      "parentName": null,
      "tenantId": null,
      "tenantName": null,
      "rootId": null,
      "treePath": null
    }
  ]
}
```

**400** – Invalid query or tenant scope

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/directory/organizations?page=1&pageSize=50&view=options" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/directory/organizations`

Create organization

Creates a new organization within a tenant and optionally assigns hierarchy relationships.

Requires features: directory.organizations.manage

**Tags:** Directory

**Requires authentication.**

**Features:** directory.organizations.manage

### Request Body

Content-Type: `application/json`

```json
{
  "name": "string",
  "slug": null,
  "logoUrl": null,
  "parentId": null
}
```

### Responses

**201** – Organization created.

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

**400** – Validation failed

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/directory/organizations" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"name\": \"string\",
  \"slug\": null,
  \"logoUrl\": null,
  \"parentId\": null
}"
```

## PUT `/directory/organizations`

Update organization

Updates organization details and hierarchy assignments.

Requires features: directory.organizations.manage

**Tags:** Directory

**Requires authentication.**

**Features:** directory.organizations.manage

### Request Body

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "slug": null,
  "logoUrl": null,
  "parentId": null
}
```

### Responses

**200** – Organization updated.

Content-Type: `application/json`

```json
{
  "ok": true
}
```

**400** – Validation failed

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X PUT "https://trainmaxr.pboj.smallhost.pl/api/directory/organizations" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\",
  \"slug\": null,
  \"logoUrl\": null,
  \"parentId\": null
}"
```

## GET `/directory/organizations/lookup`

Public organization lookup by slug

**Tags:** Directory (Tenants & Organizations)

### Responses

**200** – Success response

Content-Type: `application/json`

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/directory/organizations/lookup" \
  -H "Accept: application/json"
```

## DELETE `/directory/tenants`

Delete tenant

Soft deletes the tenant identified by id.

Requires features: directory.tenants.manage

**Tags:** Directory

**Requires authentication.**

**Features:** directory.tenants.manage

### Request Body

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

### Responses

**200** – Tenant removed.

Content-Type: `application/json`

```json
{
  "ok": true
}
```

**400** – Validation failed

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X DELETE "https://trainmaxr.pboj.smallhost.pl/api/directory/tenants" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/directory/tenants`

List tenants

Returns tenants visible to the current user with optional search and pagination.

Requires features: directory.tenants.view

**Tags:** Directory

**Requires authentication.**

**Features:** directory.tenants.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| id | query | any | Optional |
| page | query | any | Optional |
| pageSize | query | any | Optional |
| search | query | any | Optional |
| sortField | query | any | Optional |
| sortDir | query | any | Optional |
| isActive | query | any | Optional |

### Responses

**200** – Paged list of tenants.

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "name": "string",
      "isActive": true,
      "createdAt": null,
      "updatedAt": null
    }
  ],
  "total": 1,
  "page": 1,
  "pageSize": 1,
  "totalPages": 1
}
```

**400** – Invalid query parameters

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/directory/tenants?page=1&pageSize=50" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/directory/tenants`

Create tenant

Creates a new tenant and returns its identifier.

Requires features: directory.tenants.manage

**Tags:** Directory

**Requires authentication.**

**Features:** directory.tenants.manage

### Request Body

Content-Type: `application/json`

```json
{
  "name": "string"
}
```

### Responses

**201** – Tenant created.

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

**400** – Validation failed

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/directory/tenants" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"name\": \"string\"
}"
```

## PUT `/directory/tenants`

Update tenant

Updates tenant properties such as name or activation state.

Requires features: directory.tenants.manage

**Tags:** Directory

**Requires authentication.**

**Features:** directory.tenants.manage

### Request Body

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

### Responses

**200** – Tenant updated.

Content-Type: `application/json`

```json
{
  "ok": true
}
```

**400** – Validation failed

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X PUT "https://trainmaxr.pboj.smallhost.pl/api/directory/tenants" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/directory/tenants/lookup`

Public tenant lookup

**Tags:** Directory (Tenants & Organizations)

### Responses

**200** – Success response

Content-Type: `application/json`

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/directory/tenants/lookup" \
  -H "Accept: application/json"
```

## DELETE `/entities/definitions`

Soft delete custom field definition

Marks the specified definition inactive and tombstones it for the current scope.

Requires features: entities.definitions.manage

**Tags:** Entities

**Requires authentication.**

**Features:** entities.definitions.manage

### Request Body

Content-Type: `application/json`

```json
{
  "entityId": "string",
  "key": "string"
}
```

### Responses

**200** – Definition deleted

Content-Type: `application/json`

```json
{
  "ok": true,
  "version": null
}
```

**400** – Missing entity id or key

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**404** – Definition not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X DELETE "https://trainmaxr.pboj.smallhost.pl/api/entities/definitions" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"entityId\": \"string\",
  \"key\": \"string\"
}"
```

## GET `/entities/definitions`

List active custom field definitions

Returns active custom field definitions for the supplied entity ids, respecting tenant scope and tombstones.

**Tags:** Entities

**Requires authentication.**

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| entityId | query | any | Optional |
| entityIds | query | any | Optional |
| fieldset | query | any | Optional |

### Responses

**200** – Definition list

Content-Type: `application/json`

```json
{
  "items": [
    {
      "key": "string",
      "kind": "string",
      "label": "string",
      "entityId": "string"
    }
  ]
}
```

**400** – Missing entity id

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/entities/definitions" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/entities/definitions`

Upsert custom field definition

Creates or updates a custom field definition for the current tenant/org scope.

Requires features: entities.definitions.manage

**Tags:** Entities

**Requires authentication.**

**Features:** entities.definitions.manage

### Request Body

Content-Type: `application/json`

```json
{
  "entityId": "string",
  "key": "string",
  "kind": "text"
}
```

### Responses

**200** – Definition saved

Content-Type: `application/json`

```json
{
  "ok": true,
  "item": {
    "id": "00000000-0000-4000-8000-000000000000",
    "key": "string",
    "kind": "string",
    "configJson": {}
  }
}
```

**400** – Validation failed

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/entities/definitions" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"entityId\": \"string\",
  \"key\": \"string\",
  \"kind\": \"text\"
}"
```

## POST `/entities/definitions.batch`

Save multiple custom field definitions

Creates or updates multiple definitions for a single entity in one transaction.

Requires features: entities.definitions.manage

**Tags:** Entities

**Requires authentication.**

**Features:** entities.definitions.manage

### Request Body

Content-Type: `application/json`

```json
{
  "entityId": "string",
  "definitions": [
    {
      "key": "string",
      "kind": "text"
    }
  ]
}
```

### Responses

**200** – Definitions saved

Content-Type: `application/json`

```json
{
  "ok": true,
  "version": null
}
```

**400** – Validation error

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**500** – Unexpected failure

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/entities/definitions.batch" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"entityId\": \"string\",
  \"definitions\": [
    {
      \"key\": \"string\",
      \"kind\": \"text\"
    }
  ]
}"
```

## GET `/entities/definitions.manage`

Get management snapshot

Returns scoped custom field definitions (including inactive tombstones) for administration interfaces.

Requires features: entities.definitions.manage

**Tags:** Entities

**Requires authentication.**

**Features:** entities.definitions.manage

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| entityId | query | any | Required |

### Responses

**200** – Scoped definitions and deleted keys

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "key": "string",
      "kind": "string",
      "configJson": null,
      "organizationId": null,
      "tenantId": null
    }
  ],
  "deletedKeys": [
    "string"
  ],
  "version": null
}
```

**400** – Missing entity id

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/entities/definitions.manage?entityId=string" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/entities/definitions.restore`

Restore definition

Reactivates a previously soft-deleted definition within the current tenant/org scope.

Requires features: entities.definitions.manage

**Tags:** Entities

**Requires authentication.**

**Features:** entities.definitions.manage

### Request Body

Content-Type: `application/json`

```json
{
  "entityId": "string",
  "key": "string"
}
```

### Responses

**200** – Definition restored

Content-Type: `application/json`

```json
{
  "ok": true
}
```

**400** – Missing entity id or key

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**404** – Definition not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/entities/definitions.restore" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"entityId\": \"string\",
  \"key\": \"string\"
}"
```

## GET `/entities/encryption`

Fetch encryption map

Returns the encrypted field map for the current tenant/organization scope.

Requires features: entities.definitions.manage

**Tags:** Entities

**Requires authentication.**

**Features:** entities.definitions.manage

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| entityId | query | any | Required |

### Responses

**200** – Map

Content-Type: `application/json`

```json
{
  "entityId": "string",
  "fields": [
    {
      "field": "string",
      "hashField": null
    }
  ],
  "updatedAt": null
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/entities/encryption?entityId=string" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/entities/encryption`

Upsert encryption map

Creates or updates the encryption map for the current tenant/organization scope. Enforces optimistic locking when the caller sends the expected version header.

Requires features: entities.definitions.manage

**Tags:** Entities

**Requires authentication.**

**Features:** entities.definitions.manage

### Request Body

Content-Type: `application/json`

```json
{
  "entityId": "string",
  "tenantId": null,
  "organizationId": null,
  "fields": [
    {
      "field": "string",
      "hashField": null
    }
  ]
}
```

### Responses

**200** – Saved

Content-Type: `application/json`

```json
{
  "ok": true,
  "updatedAt": null
}
```

**409** – Optimistic-lock conflict (stale write)

Content-Type: `application/json`

```json
{
  "error": "string",
  "code": "string",
  "currentUpdatedAt": "string",
  "expectedUpdatedAt": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/entities/encryption" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"entityId\": \"string\",
  \"tenantId\": null,
  \"organizationId\": null,
  \"fields\": [
    {
      \"field\": \"string\",
      \"hashField\": null
    }
  ]
}"
```

## DELETE `/entities/entities`

Soft delete custom entity

Marks the specified custom entity inactive within the current scope.

Requires features: entities.definitions.manage

**Tags:** Entities

**Requires authentication.**

**Features:** entities.definitions.manage

### Request Body

Content-Type: `application/json`

```json
{
  "entityId": "string"
}
```

### Responses

**200** – Entity deleted

Content-Type: `application/json`

```json
{
  "ok": true
}
```

**400** – Missing entity id

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**404** – Entity not found in scope

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X DELETE "https://trainmaxr.pboj.smallhost.pl/api/entities/entities" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"entityId\": \"string\"
}"
```

## GET `/entities/entities`

List available entities

Returns generated and custom entities scoped to the caller with field counts per entity.

**Tags:** Entities

**Requires authentication.**

### Responses

**200** – List of entities

Content-Type: `application/json`

```json
{
  "items": [
    {
      "entityId": "string",
      "source": "code",
      "label": "string",
      "count": 1
    }
  ]
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/entities/entities" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/entities/entities`

Upsert custom entity

Creates or updates a tenant/org scoped custom entity definition.

Requires features: entities.definitions.manage

**Tags:** Entities

**Requires authentication.**

**Features:** entities.definitions.manage

### Request Body

Content-Type: `application/json`

```json
{
  "entityId": "string",
  "label": "string",
  "description": null,
  "showInSidebar": false,
  "accessRestricted": false
}
```

### Responses

**200** – Entity saved

Content-Type: `application/json`

```json
{
  "ok": true,
  "item": {
    "id": "00000000-0000-4000-8000-000000000000",
    "entityId": "string",
    "label": "string"
  }
}
```

**400** – Validation error

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/entities/entities" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"entityId\": \"string\",
  \"label\": \"string\",
  \"description\": null,
  \"showInSidebar\": false,
  \"accessRestricted\": false
}"
```

## GET `/entities/entity-settings`

Get custom entity settings

Returns the tenant-scoped default-restricted policy for new custom entities.

Requires features: entities.definitions.view

**Tags:** Entities

**Requires authentication.**

**Features:** entities.definitions.view

### Responses

**200** – Current settings

Content-Type: `application/json`

```json
{
  "newEntitiesRestrictedByDefault": true
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/entities/entity-settings" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## PUT `/entities/entity-settings`

Update custom entity settings

Sets the tenant-scoped default-restricted policy for new custom entities.

Requires features: entities.definitions.manage

**Tags:** Entities

**Requires authentication.**

**Features:** entities.definitions.manage

### Request Body

Content-Type: `application/json`

```json
{
  "newEntitiesRestrictedByDefault": true
}
```

### Responses

**200** – Updated settings

Content-Type: `application/json`

```json
{
  "ok": true,
  "newEntitiesRestrictedByDefault": true
}
```

**400** – Invalid payload

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X PUT "https://trainmaxr.pboj.smallhost.pl/api/entities/entity-settings" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"newEntitiesRestrictedByDefault\": true
}"
```

## DELETE `/entities/records`

Delete record

Soft deletes the specified record within the current tenant/org scope.

Requires features: entities.records.manage

**Tags:** Entities

**Requires authentication.**

**Features:** entities.records.manage

### Request Body

Content-Type: `application/json`

```json
{
  "entityId": "string",
  "recordId": "string"
}
```

### Responses

**200** – Record deleted

Content-Type: `application/json`

```json
{
  "ok": true
}
```

**400** – Missing entity id or record id

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**404** – Record not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**500** – Unexpected failure

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X DELETE "https://trainmaxr.pboj.smallhost.pl/api/entities/records" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"entityId\": \"string\",
  \"recordId\": \"string\"
}"
```

## GET `/entities/records`

List records

Returns paginated records for the supplied entity. Supports custom field filters, exports, and soft-delete toggles.

Requires features: entities.records.view

**Tags:** Entities

**Requires authentication.**

**Features:** entities.records.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| entityId | query | any | Required |
| page | query | any | Optional |
| pageSize | query | any | Optional |
| sortField | query | any | Optional |
| sortDir | query | any | Optional |
| search | query | any | Optional |
| searchFields | query | any | Optional |
| withDeleted | query | any | Optional |
| format | query | any | Optional |
| exportScope | query | any | Optional |
| export_scope | query | any | Optional |
| all | query | any | Optional |
| full | query | any | Optional |

### Responses

**200** – Paginated records

Content-Type: `application/json`

```json
{
  "items": [
    {}
  ],
  "total": 1,
  "page": 1,
  "pageSize": 1,
  "totalPages": 1
}
```

**400** – Missing entity id

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**500** – Unexpected failure

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/entities/records?entityId=string" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/entities/records`

Create record

Creates a record for the given entity. When `recordId` is omitted or not a UUID the data engine will generate one automatically.

Requires features: entities.records.manage

**Tags:** Entities

**Requires authentication.**

**Features:** entities.records.manage

### Request Body

Content-Type: `application/json`

```json
{
  "entityId": "string",
  "values": {}
}
```

### Responses

**200** – Record created

Content-Type: `application/json`

```json
{
  "ok": true
}
```

**400** – Validation failure

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**500** – Unexpected failure

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/entities/records" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"entityId\": \"string\",
  \"values\": {}
}"
```

## PUT `/entities/records`

Update record

Updates an existing record. If the provided recordId is not a UUID the record will be created instead to support optimistic flows.

Requires features: entities.records.manage

**Tags:** Entities

**Requires authentication.**

**Features:** entities.records.manage

### Request Body

Content-Type: `application/json`

```json
{
  "entityId": "string",
  "recordId": "string",
  "values": {}
}
```

### Responses

**200** – Record updated

Content-Type: `application/json`

```json
{
  "ok": true
}
```

**400** – Validation failure

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**500** – Unexpected failure

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X PUT "https://trainmaxr.pboj.smallhost.pl/api/entities/records" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"entityId\": \"string\",
  \"recordId\": \"string\",
  \"values\": {}
}"
```

## GET `/entities/relations/options`

List relation options

Returns up to 200 option entries for populating relation dropdowns, automatically resolving label fields when omitted. An entityId that matches neither an active custom entity nor a registered ORM entity yields an empty option list.

Requires features: entities.definitions.view

**Tags:** Entities

**Requires authentication.**

**Features:** entities.definitions.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| entityId | query | any | Required |
| labelField | query | any | Optional |
| q | query | any | Optional |
| ids | query | any | Optional |
| routeContextFields | query | any | Optional |

### Responses

**200** – Option list

Content-Type: `application/json`

```json
{
  "items": [
    {
      "value": "string",
      "label": "string"
    }
  ]
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/entities/relations/options?entityId=string" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/entities/sidebar-entities`

Get sidebar entities

Returns custom entities flagged with `showInSidebar` for the current tenant/org scope.

**Tags:** Entities

**Requires authentication.**

### Responses

**200** – Sidebar entities for navigation

Content-Type: `application/json`

```json
{
  "items": [
    {
      "entityId": "string",
      "label": "string",
      "href": "string"
    }
  ]
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/entities/sidebar-entities" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/events`

List declared events

Returns every declared event. Filters: category, module, excludeTriggerExcluded (default true).

Requires features: workflows.view

**Tags:** Events

**Requires authentication.**

**Features:** workflows.view

### Responses

**200** – Declared events

Content-Type: `application/json`

```json
{
  "data": [
    {
      "id": "string",
      "label": "string"
    }
  ],
  "total": 1
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/events" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/events/stream`

GET /events/stream

**Tags:** Events

**Requires authentication.**

### Responses

**200** – Success response

Content-Type: `application/json`

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/events/stream" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## DELETE `/facilities_reservations`

Remove a reservation

Soft-deletes. While a match references the reservation the removal is refused (409) unless detachMatches: true clears every referencing match first - never a silent orphan. Emits facilities_reservations.reservation.cancelled.

Requires features: facilities_reservations.reservation.manage

**Tags:** Facility reservations

**Requires authentication.**

**Features:** facilities_reservations.reservation.manage

### Request Body

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

### Responses

**200** – Removed

Content-Type: `application/json`

```json
{
  "ok": true
}
```

**404** – Reservation not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**409** – A match still references this reservation

Content-Type: `application/json`

```json
{
  "error": "string",
  "attachedMatchIds": [
    "00000000-0000-4000-8000-000000000000"
  ]
}
```

### Example

```bash
curl -X DELETE "https://trainmaxr.pboj.smallhost.pl/api/facilities_reservations" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/facilities_reservations`

List reservations

Org-scoped to the teams the caller can see (coach and coordinator read their teams, the club admin all), each reservation carrying its locker rooms. Filters: id, team, pitch, date window.

Requires features: facilities_reservations.reservation.view

**Tags:** Facility reservations

**Requires authentication.**

**Features:** facilities_reservations.reservation.view

### Responses

**200** – Reservations

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "teamOrgId": "00000000-0000-4000-8000-000000000000",
      "pitchFacilityId": "00000000-0000-4000-8000-000000000000",
      "lockerFacilityIds": [
        "00000000-0000-4000-8000-000000000000"
      ],
      "startsAt": "string",
      "endsAt": "string",
      "title": null,
      "notes": null
    }
  ]
}
```

**400** – Invalid query

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**404** – Team not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/facilities_reservations" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/facilities_reservations`

Create a reservation

Assigns one pitch plus one-or-more locker rooms to one team for a window. An overlap on the same pitch or a shared locker room (tenant-wide) returns the informational already-reserved advisory (409, requiresConfirmation) instead of saving; resubmitting with confirm: true saves - the confirmation is transient and never persisted or attributed. Emits facilities_reservations.reservation.created.

Requires features: facilities_reservations.reservation.manage

**Tags:** Facility reservations

**Requires authentication.**

**Features:** facilities_reservations.reservation.manage

### Request Body

Content-Type: `application/json`

```json
{
  "teamOrgId": "00000000-0000-4000-8000-000000000000",
  "pitchFacilityId": "00000000-0000-4000-8000-000000000000",
  "lockerFacilityIds": [
    "00000000-0000-4000-8000-000000000000"
  ],
  "startsAt": "string",
  "endsAt": "string"
}
```

### Responses

**201** – Created reservation id

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

**404** – Team not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**409** – Already reserved in those hours (advisory)

Content-Type: `application/json`

```json
{
  "error": "string",
  "requiresConfirmation": true,
  "conflicts": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "teamOrgId": "00000000-0000-4000-8000-000000000000",
      "pitchFacilityId": "00000000-0000-4000-8000-000000000000",
      "lockerFacilityIds": [
        "00000000-0000-4000-8000-000000000000"
      ],
      "startsAt": "string",
      "endsAt": "string",
      "title": null
    }
  ]
}
```

**422** – Invalid input

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/facilities_reservations" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"teamOrgId\": \"00000000-0000-4000-8000-000000000000\",
  \"pitchFacilityId\": \"00000000-0000-4000-8000-000000000000\",
  \"lockerFacilityIds\": [
    \"00000000-0000-4000-8000-000000000000\"
  ],
  \"startsAt\": \"string\",
  \"endsAt\": \"string\"
}"
```

## PUT `/facilities_reservations`

Edit a reservation

Repitches, reslots, or re-windows a reservation; the overlap check re-runs (excluding the row itself) with the same flag-and-confirm advisory. Emits facilities_reservations.reservation.updated.

Requires features: facilities_reservations.reservation.manage

**Tags:** Facility reservations

**Requires authentication.**

**Features:** facilities_reservations.reservation.manage

### Request Body

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "title": null,
  "notes": null
}
```

### Responses

**200** – Updated reservation id

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

**404** – Reservation not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**409** – Already reserved in those hours (advisory)

Content-Type: `application/json`

```json
{
  "error": "string",
  "requiresConfirmation": true,
  "conflicts": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "teamOrgId": "00000000-0000-4000-8000-000000000000",
      "pitchFacilityId": "00000000-0000-4000-8000-000000000000",
      "lockerFacilityIds": [
        "00000000-0000-4000-8000-000000000000"
      ],
      "startsAt": "string",
      "endsAt": "string",
      "title": null
    }
  ]
}
```

**422** – Invalid input

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X PUT "https://trainmaxr.pboj.smallhost.pl/api/facilities_reservations" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\",
  \"title\": null,
  \"notes\": null
}"
```

## GET `/facilities_reservations/locker-schedule`

Read the locker-room schedule

Which locker room serves each reservation-attached scheduled match - derived on every read from the reservations and the match calendar, storing nothing. Cancelled matches drop off; reservation and match changes show on the next read. Scoped to the teams the caller can see and always to the club.

Requires features: facilities_reservations.reservation.view

**Tags:** Facility reservations

**Requires authentication.**

**Features:** facilities_reservations.reservation.view

### Responses

**200** – Locker-room x match rows

Content-Type: `application/json`

```json
{
  "items": [
    {
      "lockerFacilityId": "00000000-0000-4000-8000-000000000000",
      "lockerFacilityName": null,
      "reservationId": "00000000-0000-4000-8000-000000000000",
      "reservationStartsAt": "string",
      "reservationEndsAt": "string",
      "pitchFacilityId": "00000000-0000-4000-8000-000000000000",
      "teamOrgId": "00000000-0000-4000-8000-000000000000",
      "matchId": "00000000-0000-4000-8000-000000000000",
      "matchStartsAt": "string",
      "opponentName": null,
      "homeAway": null
    }
  ]
}
```

**400** – Invalid query

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**404** – Team not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/facilities_reservations/locker-schedule" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/feature_toggles/check/boolean`

Check if feature is enabled

Checks if a feature toggle is enabled for the current context.

**Tags:** Feature Toggles

**Requires authentication.**

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| identifier | query | any | Required. Feature toggle identifier |

### Responses

**200** – Feature status

Content-Type: `application/json`

```json
{
  "enabled": true,
  "source": "override",
  "toggleId": "string",
  "identifier": "string",
  "tenantId": "string"
}
```

**400** – Bad Request

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**404** – Tenant not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/feature_toggles/check/boolean?identifier=string" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/feature_toggles/check/json`

Get json config

Gets the json configuration for a feature toggle.

**Tags:** Feature Toggles

**Requires authentication.**

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| identifier | query | any | Required. Feature toggle identifier |

### Responses

**200** – Json config

Content-Type: `application/json`

```json
{
  "valueType": "json",
  "source": "override",
  "toggleId": "string",
  "identifier": "string",
  "tenantId": "string"
}
```

**400** – Bad Request

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**404** – Tenant not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/feature_toggles/check/json?identifier=string" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/feature_toggles/check/number`

Get number config

Gets the number configuration for a feature toggle.

**Tags:** Feature Toggles

**Requires authentication.**

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| identifier | query | any | Required. Feature toggle identifier |

### Responses

**200** – Number config

Content-Type: `application/json`

```json
{
  "valueType": "number",
  "value": 1,
  "source": "override",
  "toggleId": "string",
  "identifier": "string",
  "tenantId": "string"
}
```

**400** – Bad Request

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**404** – Tenant not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/feature_toggles/check/number?identifier=string" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/feature_toggles/check/string`

Get string config

Gets the string configuration for a feature toggle.

**Tags:** Feature Toggles

**Requires authentication.**

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| identifier | query | any | Required. Feature toggle identifier |

### Responses

**200** – String config

Content-Type: `application/json`

```json
{
  "valueType": "string",
  "value": "string",
  "source": "override",
  "toggleId": "string",
  "identifier": "string",
  "tenantId": "string"
}
```

**400** – Bad Request

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**404** – Tenant not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/feature_toggles/check/string?identifier=string" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## DELETE `/feature_toggles/global`

Delete global feature toggle

Soft deletes a global feature toggle by ID. Requires superadmin role.

Requires features: feature_toggles.global.manage

**Tags:** Feature Toggles

**Requires authentication.**

**Features:** feature_toggles.global.manage

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| id | query | any | Required. Feature toggle identifier |

### Responses

**200** – Feature toggle deleted

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

**400** – Invalid identifier

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**404** – Feature toggle not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X DELETE "https://trainmaxr.pboj.smallhost.pl/api/feature_toggles/global?id=00000000-0000-4000-8000-000000000000" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/feature_toggles/global`

List global feature toggles

Returns all global feature toggles with filtering and pagination. Requires superadmin role.

Requires features: feature_toggles.view

**Tags:** Feature Toggles

**Requires authentication.**

**Features:** feature_toggles.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| page | query | any | Optional. Page number for pagination |
| pageSize | query | any | Optional. Number of items per page (max 200) |
| search | query | any | Optional. Case-insensitive search across identifier, name, description, and category |
| type | query | any | Optional. Filter by toggle type (boolean, string, number, json) |
| category | query | any | Optional. Filter by category (case-insensitive partial match) |
| name | query | any | Optional. Filter by name (case-insensitive partial match) |
| identifier | query | any | Optional. Filter by identifier (case-insensitive partial match) |
| sortField | query | any | Optional. Field to sort by |
| sortDir | query | any | Optional. Sort direction (ascending or descending) |

### Responses

**200** – Feature toggles collection

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "identifier": "string",
      "name": "string",
      "description": null,
      "category": null,
      "type": "boolean",
      "defaultValue": null,
      "createdAt": null,
      "updatedAt": null
    }
  ],
  "total": 1,
  "page": 1,
  "pageSize": 1,
  "totalPages": 1
}
```

**400** – Invalid query parameters

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/feature_toggles/global?page=1&pageSize=50" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/feature_toggles/global`

Create global feature toggle

Creates a new global feature toggle. Requires superadmin role.

Requires features: feature_toggles.global.manage

**Tags:** Feature Toggles

**Requires authentication.**

**Features:** feature_toggles.global.manage

### Request Body

Content-Type: `application/json`

```json
{
  "identifier": "string",
  "name": "string",
  "description": null,
  "category": null,
  "type": "boolean",
  "defaultValue": null
}
```

### Responses

**201** – Feature toggle created

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

**400** – Invalid payload

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/feature_toggles/global" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"identifier\": \"string\",
  \"name\": \"string\",
  \"description\": null,
  \"category\": null,
  \"type\": \"boolean\",
  \"defaultValue\": null
}"
```

## PUT `/feature_toggles/global`

Update global feature toggle

Updates an existing global feature toggle. Requires superadmin role.

Requires features: feature_toggles.global.manage

**Tags:** Feature Toggles

**Requires authentication.**

**Features:** feature_toggles.global.manage

### Request Body

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "description": null,
  "category": null,
  "defaultValue": null
}
```

### Responses

**200** – Feature toggle updated

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

**400** – Invalid payload

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**404** – Feature toggle not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X PUT "https://trainmaxr.pboj.smallhost.pl/api/feature_toggles/global" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\",
  \"description\": null,
  \"category\": null,
  \"defaultValue\": null
}"
```

## GET `/feature_toggles/global/{id}`

Fetch feature toggle by ID

Returns complete details of a feature toggle.

Requires features: feature_toggles.view

**Tags:** Feature Toggles

**Requires authentication.**

**Features:** feature_toggles.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| id | path | any | Required |

### Responses

**200** – Feature toggle detail

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "identifier": "string",
  "name": "string",
  "description": null,
  "category": null,
  "type": "boolean",
  "defaultValue": null,
  "createdAt": null,
  "updatedAt": null
}
```

**400** – Invalid identifier

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**404** – Feature toggle not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/feature_toggles/global/:id" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/feature_toggles/global/{id}/override`

Fetch feature toggle override

Returns feature toggle override.

Requires features: feature_toggles.view

**Tags:** Feature Toggles

**Requires authentication.**

**Features:** feature_toggles.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| id | path | any | Required |

### Responses

**200** – Feature toggle overrides

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "tenantName": "string",
  "tenantId": "00000000-0000-4000-8000-000000000000",
  "toggleType": "boolean",
  "updatedAt": null
}
```

**400** – Invalid request

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**404** – Feature toggle not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/feature_toggles/global/:id/override" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/feature_toggles/overrides`

List overrides

Returns list of feature toggle overrides.

Requires features: feature_toggles.view

**Tags:** Feature Toggles

**Requires authentication.**

**Features:** feature_toggles.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| category | query | any | Optional |
| name | query | any | Optional |
| identifier | query | any | Optional |
| sortField | query | any | Optional |
| sortDir | query | any | Optional |
| page | query | any | Optional |
| pageSize | query | any | Optional |

### Responses

**200** – List of overrides

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "toggleId": "00000000-0000-4000-8000-000000000000",
      "tenantName": "string",
      "tenantId": "00000000-0000-4000-8000-000000000000",
      "identifier": "string",
      "name": "string",
      "category": "string",
      "isOverride": true
    }
  ],
  "total": 1,
  "page": 1,
  "pageSize": 1,
  "totalPages": 1,
  "isSuperAdmin": true
}
```

**400** – Invalid query parameters

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/feature_toggles/overrides?page=1&pageSize=25" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## PUT `/feature_toggles/overrides`

Change override state

Enable, disable or inherit a feature toggle for a specific tenant.

Requires features: feature_toggles.manage

**Tags:** Feature Toggles

**Requires authentication.**

**Features:** feature_toggles.manage

### Request Body

Content-Type: `application/json`

```json
{
  "toggleId": "00000000-0000-4000-8000-000000000000",
  "isOverride": true
}
```

### Responses

**200** – Override updated

Content-Type: `application/json`

```json
{
  "ok": true,
  "overrideToggleId": null
}
```

**400** – Validation failed

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**404** – Not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**500** – Internal server error

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X PUT "https://trainmaxr.pboj.smallhost.pl/api/feature_toggles/overrides" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"toggleId\": \"00000000-0000-4000-8000-000000000000\",
  \"isOverride\": true
}"
```

## GET `/health_monitoring/compliance`

List every in-scope player with an expired, expiring, or missing health check

Derived per request from each player's most-recent check per type against today and the club threshold — nothing stored or swept. Scope: coordinator = overseen teams, admin = all teams.

Requires features: health_monitoring.health.view

**Tags:** Health monitoring

**Requires authentication.**

**Features:** health_monitoring.health.view

### Responses

**200** – Compliance items

Content-Type: `application/json`

```json
{
  "items": [
    {
      "playerId": "00000000-0000-4000-8000-000000000000",
      "firstName": null,
      "lastName": null,
      "teamOrgIds": [
        "00000000-0000-4000-8000-000000000000"
      ],
      "checkType": null,
      "expiresAt": null,
      "state": "expiring"
    }
  ]
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/health_monitoring/compliance" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/health_monitoring/health-checks`

List a player's health checks with derived expiry states

The player's full check history, newest first, decrypted per request. Visibility follows live team memberships; the club admin reads club-wide.

Requires features: health_monitoring.health.view

**Tags:** Health monitoring

**Requires authentication.**

**Features:** health_monitoring.health.view

### Responses

**200** – Check history

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "playerId": "00000000-0000-4000-8000-000000000000",
      "checkType": "sports_physical",
      "result": null,
      "issuedAt": "string",
      "expiresAt": "string",
      "state": "ok",
      "fileUrl": "string",
      "enteredByUserId": "00000000-0000-4000-8000-000000000000",
      "createdAt": "string",
      "updatedAt": "string"
    }
  ]
}
```

**400** – Invalid query

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**404** – Player not found or out of scope

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/health_monitoring/health-checks" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/health_monitoring/health-checks`

Enter a health check (multipart, mandatory file)

multipart/form-data: playerId, checkType, issuedAt, expiresAt, optional result, and the mandatory `file` (photo/screenshot of the test result). An entry without a file is rejected.

Requires features: health_monitoring.health.manage

**Tags:** Health monitoring

**Requires authentication.**

**Features:** health_monitoring.health.manage

### Responses

**201** – Created check id

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

**404** – Player not found or out of scope

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Missing file / invalid fields

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/health_monitoring/health-checks" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## PUT `/health_monitoring/health-checks`

Correct a health check (multipart, optional file replacement)

multipart/form-data: id plus corrected dates/result; a `file` part replaces the stored test-result file.

Requires features: health_monitoring.health.manage

**Tags:** Health monitoring

**Requires authentication.**

**Features:** health_monitoring.health.manage

### Responses

**200** – Updated check id

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

**404** – Check or player out of scope

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid fields or replacement file

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X PUT "https://trainmaxr.pboj.smallhost.pl/api/health_monitoring/health-checks" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/health_monitoring/health-checks/file`

Stream the attached test-result file (authorized broker)

Serves the decrypted photo/screenshot for one check to an in-scope caller. There is no public or anonymous link to a medical file.

Requires features: health_monitoring.health.view

**Tags:** Health monitoring

**Requires authentication.**

**Features:** health_monitoring.health.view

### Responses

**200** – The file bytes (inline)

Content-Type: `application/json`

**400** – Invalid query

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**404** – Check, player, or file out of scope

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/health_monitoring/health-checks/file" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/health_monitoring/physical-measurements`

List a player's measurement series in time order

The per-player series, decrypted per request and ordered by measurement date; filter by kind for one series. Visibility follows live team memberships.

Requires features: health_monitoring.health.view

**Tags:** Health monitoring

**Requires authentication.**

**Features:** health_monitoring.health.view

### Responses

**200** – Measurement series

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "playerId": "00000000-0000-4000-8000-000000000000",
      "kind": "fitness",
      "metric": "string",
      "value": 1,
      "unit": null,
      "measuredAt": "string",
      "enteredByUserId": "00000000-0000-4000-8000-000000000000",
      "createdAt": "string"
    }
  ]
}
```

**400** – Invalid query

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**404** – Player not found or out of scope

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/health_monitoring/physical-measurements" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/health_monitoring/physical-measurements`

Record a fitness / PHV / somatic measurement

Requires features: health_monitoring.health.manage

**Tags:** Health monitoring

**Requires authentication.**

**Features:** health_monitoring.health.manage

### Request Body

Content-Type: `application/json`

```json
{
  "playerId": "00000000-0000-4000-8000-000000000000",
  "kind": "fitness",
  "metric": "string",
  "value": 1,
  "measuredAt": "string"
}
```

### Responses

**201** – Created measurement id

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

**404** – Player not found or out of scope

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid payload

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/health_monitoring/physical-measurements" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"playerId\": \"00000000-0000-4000-8000-000000000000\",
  \"kind\": \"fitness\",
  \"metric\": \"string\",
  \"value\": 1,
  \"measuredAt\": \"string\"
}"
```

## GET `/health_monitoring/player-status`

Read a player's derived health-check status

Per-type derived states plus one advisory nonCompliant flag (an expired type, or nothing on file). Advisory only — never blocks a lineup.

Requires features: health_monitoring.health.view

**Tags:** Health monitoring

**Requires authentication.**

**Features:** health_monitoring.health.view

### Responses

**200** – Derived status

Content-Type: `application/json`

```json
{
  "playerId": "00000000-0000-4000-8000-000000000000",
  "statuses": [
    {
      "checkType": "sports_physical",
      "expiresAt": "string",
      "state": "ok"
    }
  ],
  "nonCompliant": true
}
```

**400** – Invalid query

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**404** – Player not found or out of scope

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/health_monitoring/player-status" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## DELETE `/lineups_and_positioning/lineups`

Delete a lineup or template

Soft-deletes the lineup and its slots; the event can then be planned afresh.

Requires features: lineups_and_positioning.lineup.manage

**Tags:** Lineups

**Requires authentication.**

**Features:** lineups_and_positioning.lineup.manage

### Request Body

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

### Responses

**200** – Deleted lineup id

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

**404** – Lineup not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**409** – Cancelled event

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X DELETE "https://trainmaxr.pboj.smallhost.pl/api/lineups_and_positioning/lineups" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/lineups_and_positioning/lineups`

List lineups and templates

The lineup and template list, scoped to the caller's visible teams. An event lineup carries its session; a template carries none. Filters: session, team, template-ness.

Requires features: lineups_and_positioning.lineup.view

**Tags:** Lineups

**Requires authentication.**

**Features:** lineups_and_positioning.lineup.view

### Responses

**200** – Lineups

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "sessionId": null,
      "teamOrgId": "00000000-0000-4000-8000-000000000000",
      "name": null,
      "formation": null,
      "isTemplate": true,
      "createdByUserId": null
    }
  ]
}
```

**400** – Invalid query

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**404** – Team or session not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/lineups_and_positioning/lineups" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/lineups_and_positioning/lineups`

Create an event lineup or a team template

Creates the session's single event lineup (optionally seeded by copying a template's formation and positions - later edits to either side stay independent) or a named team template bound to no event. Rejected when the session already carries its lineup (409), is cancelled (409), or the template belongs to another team (422).

Requires features: lineups_and_positioning.lineup.manage

**Tags:** Lineups

**Requires authentication.**

**Features:** lineups_and_positioning.lineup.manage

### Request Body

Content-Type: `application/json`

```json
{
  "isTemplate": false
}
```

### Responses

**200** – Created lineup id with any seeded slot ids

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

**404** – Session, team, or template not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**409** – Cancelled event or the session already has its lineup

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid input or a cross-team template

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/lineups_and_positioning/lineups" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"isTemplate\": false
}"
```

## PUT `/lineups_and_positioning/lineups`

Correct a lineup's name or formation

Building, saving, reusing, and correcting are one management capability; a cancelled event lineup is closed (409).

Requires features: lineups_and_positioning.lineup.manage

**Tags:** Lineups

**Requires authentication.**

**Features:** lineups_and_positioning.lineup.manage

### Request Body

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "name": null,
  "formation": null
}
```

### Responses

**200** – Corrected lineup id

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

**404** – Lineup not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**409** – Cancelled event

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid input or a nameless template

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X PUT "https://trainmaxr.pboj.smallhost.pl/api/lineups_and_positioning/lineups" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\",
  \"name\": null,
  \"formation\": null
}"
```

## POST `/lineups_and_positioning/save-as-template`

Copy a lineup into a new named template

Copies the lineup's formation and positioned players into a new template on the same team, bound to no event (feature §5). A copy, not a link - later edits to either side stay independent.

Requires features: lineups_and_positioning.lineup.manage

**Tags:** Lineups

**Requires authentication.**

**Features:** lineups_and_positioning.lineup.manage

### Request Body

Content-Type: `application/json`

```json
{
  "lineupId": "00000000-0000-4000-8000-000000000000",
  "name": "string"
}
```

### Responses

**200** – The new template id and its copied slot ids

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "slotIds": [
    "00000000-0000-4000-8000-000000000000"
  ]
}
```

**404** – Lineup not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid input or a missing template name

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/lineups_and_positioning/save-as-template" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"lineupId\": \"00000000-0000-4000-8000-000000000000\",
  \"name\": \"string\"
}"
```

## GET `/lineups_and_positioning/sheet`

Read the builder sheet for one lineup or session

The lineup (or a blank for a session not planned yet), its event, the team's live-membership pool joined to the saved placements, off-roster stragglers marked for adjustment, and the per-player advisory pre-selection flag (red/orange Live fatigue band 5.2.4, expired/overdue health check 5.7.1) composed live from the sibling reads - stored nowhere, re-evaluated on every open, degrading to an absent signal when a sibling read is unavailable, and never blocking selection. No health detail beyond the derived boolean crosses over.

Requires features: lineups_and_positioning.lineup.view

**Tags:** Lineups

**Requires authentication.**

**Features:** lineups_and_positioning.lineup.view

### Responses

**200** – The builder sheet

Content-Type: `application/json`

```json
{
  "lineup": null,
  "session": null,
  "teamOrgId": "00000000-0000-4000-8000-000000000000",
  "rows": [
    {
      "playerId": "00000000-0000-4000-8000-000000000000",
      "firstName": null,
      "lastName": null,
      "onRoster": true,
      "slot": null,
      "flags": {}
    }
  ]
}
```

**400** – Invalid query

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**404** – Lineup or session not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/lineups_and_positioning/sheet" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/lineups_and_positioning/slots`

Replace the lineup's slot set in one pass

The coach's primary write path (feature §7): replaces the arrangement - starters with normalized 0-1 pitch coordinates, bench substitutes without, a player at most once. The pool is the lineup team's live-membership roster; a player already positioned on this lineup is grandfathered so a saved off-roster straggler stays correctable. Nothing consults the fatigue or health signal - the flag is advisory, never a gate. Rejected when the event is cancelled (409).

Requires features: lineups_and_positioning.lineup.manage

**Tags:** Lineups

**Requires authentication.**

**Features:** lineups_and_positioning.lineup.manage

### Request Body

Content-Type: `application/json`

```json
{
  "lineupId": "00000000-0000-4000-8000-000000000000",
  "slots": []
}
```

### Responses

**200** – The lineup id and its saved slot ids

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "slotIds": [
    "00000000-0000-4000-8000-000000000000"
  ]
}
```

**404** – Lineup not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**409** – Cancelled event

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid input, duplicate player, or a player outside the roster pool

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/lineups_and_positioning/slots" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"lineupId\": \"00000000-0000-4000-8000-000000000000\",
  \"slots\": []
}"
```

## GET `/match_data_entry/protocol`

Read the protocol sheet for one match

The derived sheet the coach fills, in one request: the match team's memberships active at match time joined to the decrypted player identity and each player's current stat line (with the role derived from the recorded substitutions over the stored flag), plus any off-roster guest line, plus the shared timeline events ready to edit (live-tagging pre-fill). A cancelled match returns the sheet closed (recordable: false); a training session is refused (409).

Requires features: match_data_entry.match_data.view

**Tags:** Match data

**Requires authentication.**

**Features:** match_data_entry.match_data.view

### Responses

**200** – The protocol sheet

Content-Type: `application/json`

```json
{
  "session": {
    "id": "00000000-0000-4000-8000-000000000000",
    "teamOrgId": "00000000-0000-4000-8000-000000000000",
    "eventType": "string",
    "status": "string",
    "startsAt": "string",
    "durationMinutes": null,
    "recordable": true
  },
  "rows": [
    {
      "playerId": "00000000-0000-4000-8000-000000000000",
      "firstName": null,
      "lastName": null,
      "onRoster": true,
      "derivedRole": "starter",
      "statLine": null
    }
  ],
  "events": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "sessionId": "00000000-0000-4000-8000-000000000000",
      "teamOrgId": "00000000-0000-4000-8000-000000000000",
      "minute": 1,
      "eventType": "goal",
      "side": "home",
      "playerId": null,
      "recordedByUserId": null,
      "recordedAt": "string"
    }
  ]
}
```

**400** – Invalid query

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**404** – Match not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**409** – Not a match

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/match_data_entry/protocol" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/match_data_entry/protocol`

Save the whole protocol in one pass

The coach's primary write path (feature §5) and the single-store endpoint live tagging pre-fills: upserts every stat line, REPLACES the match's timeline events, and reconciles each event-named scorer's goal/assist counts so nothing is double-counted. A substitution is a sub_off + sub_on pair at one minute; an assist requires a team goal at its minute; an empty protocol is a valid save.

Requires features: match_data_entry.match_data.manage

**Tags:** Match data

**Requires authentication.**

**Features:** match_data_entry.match_data.manage

### Request Body

Content-Type: `application/json`

```json
{
  "sessionId": "00000000-0000-4000-8000-000000000000",
  "lines": [],
  "events": []
}
```

### Responses

**200** – Saved line and event ids

Content-Type: `application/json`

```json
{
  "lineIds": [
    "00000000-0000-4000-8000-000000000000"
  ],
  "eventIds": [
    "00000000-0000-4000-8000-000000000000"
  ]
}
```

**404** – Match not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**409** – Not a match or cancelled match

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid input, duplicate player, unpaired substitution, or goal-less assist

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/match_data_entry/protocol" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"sessionId\": \"00000000-0000-4000-8000-000000000000\",
  \"lines\": [],
  \"events\": []
}"
```

## GET `/match_data_entry/stat-lines`

List stat lines

The match-data history read, scoped to the caller's visible teams. A stat line FKs the persistent player, so a player's match history survives roster removal and transfers; a guest's line carries the match team-org. Filters: session, player, team, match role.

Requires features: match_data_entry.match_data.view

**Tags:** Match data

**Requires authentication.**

**Features:** match_data_entry.match_data.view

### Responses

**200** – Stat lines

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "sessionId": "00000000-0000-4000-8000-000000000000",
      "playerId": "00000000-0000-4000-8000-000000000000",
      "teamOrgId": "00000000-0000-4000-8000-000000000000",
      "goals": 1,
      "assists": 1,
      "yellowCards": 1,
      "redCards": 1,
      "minutesPlayed": null,
      "matchRole": "starter",
      "recordedByUserId": null,
      "recordedAt": "string"
    }
  ]
}
```

**400** – Invalid query

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**404** – Team or match not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/match_data_entry/stat-lines" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/match_data_entry/stat-lines`

Record one stat line

Creates the single live stat line for (match, player). The player is validated to the club only - a guest with no membership on the match team is recorded like any roster player. Rejected when the session is not a match (409), is cancelled (409), or the player already has a line (409).

Requires features: match_data_entry.match_data.manage

**Tags:** Match data

**Requires authentication.**

**Features:** match_data_entry.match_data.manage

### Request Body

Content-Type: `application/json`

```json
{
  "sessionId": "00000000-0000-4000-8000-000000000000",
  "playerId": "00000000-0000-4000-8000-000000000000",
  "goals": 0,
  "assists": 0,
  "yellowCards": 0,
  "redCards": 0,
  "matchRole": "unused"
}
```

### Responses

**200** – Created stat line id

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

**404** – Match not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**409** – Not a match, cancelled match, or duplicate line

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid input or out-of-club player

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/match_data_entry/stat-lines" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"sessionId\": \"00000000-0000-4000-8000-000000000000\",
  \"playerId\": \"00000000-0000-4000-8000-000000000000\",
  \"goals\": 0,
  \"assists\": 0,
  \"yellowCards\": 0,
  \"redCards\": 0,
  \"matchRole\": \"unused\"
}"
```

## PUT `/match_data_entry/stat-lines`

Correct one stat line

Updates the existing live stat line for (match, player) - recording and correcting are the same management capability.

Requires features: match_data_entry.match_data.manage

**Tags:** Match data

**Requires authentication.**

**Features:** match_data_entry.match_data.manage

### Request Body

Content-Type: `application/json`

```json
{
  "sessionId": "00000000-0000-4000-8000-000000000000",
  "playerId": "00000000-0000-4000-8000-000000000000",
  "goals": 0,
  "assists": 0,
  "yellowCards": 0,
  "redCards": 0,
  "matchRole": "unused"
}
```

### Responses

**200** – Corrected stat line id

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

**404** – Match or stat line not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**409** – Not a match or cancelled match

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid input

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X PUT "https://trainmaxr.pboj.smallhost.pl/api/match_data_entry/stat-lines" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"sessionId\": \"00000000-0000-4000-8000-000000000000\",
  \"playerId\": \"00000000-0000-4000-8000-000000000000\",
  \"goals\": 0,
  \"assists\": 0,
  \"yellowCards\": 0,
  \"redCards\": 0,
  \"matchRole\": \"unused\"
}"
```

## DELETE `/match_data_entry/timeline-events`

Remove one timeline event

Soft-deletes one event (e.g. a goal later ruled out) - a cancelled match takes no change.

Requires features: match_data_entry.match_data.manage

**Tags:** Match data

**Requires authentication.**

**Features:** match_data_entry.match_data.manage

### Request Body

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

### Responses

**200** – Removed event id

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

**404** – Event not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**409** – Cancelled match

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid input

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X DELETE "https://trainmaxr.pboj.smallhost.pl/api/match_data_entry/timeline-events" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/match_data_entry/timeline-events`

List timeline events

The raw minute-stamped inputs the statistics slice derives from - nothing aggregated here. One store for the post-match protocol and the live-tagging companion, so a tagged datum is its protocol record. Filters: session, player, team, event type, side.

Requires features: match_data_entry.match_data.view

**Tags:** Match data

**Requires authentication.**

**Features:** match_data_entry.match_data.view

### Responses

**200** – Timeline events

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "sessionId": "00000000-0000-4000-8000-000000000000",
      "teamOrgId": "00000000-0000-4000-8000-000000000000",
      "minute": 1,
      "eventType": "goal",
      "side": "home",
      "playerId": null,
      "recordedByUserId": null,
      "recordedAt": "string"
    }
  ]
}
```

**400** – Invalid query

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**404** – Team or match not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/match_data_entry/timeline-events" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/match_data_entry/timeline-events`

Add one timeline event

Adds a minute-stamped event to the match timeline - no cap on the count. An own goal and an opponent-side or team-metric event attribute to nobody; a home goal, assist, card, or substitution half names its player; an assist requires a recorded team goal at its minute. Disallowed goals are never inserted - only goals that stood become rows.

Requires features: match_data_entry.match_data.manage

**Tags:** Match data

**Requires authentication.**

**Features:** match_data_entry.match_data.manage

### Request Body

Content-Type: `application/json`

```json
{
  "sessionId": "00000000-0000-4000-8000-000000000000",
  "minute": 1,
  "eventType": "goal",
  "side": "home"
}
```

### Responses

**200** – Created event id

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

**404** – Match not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**409** – Not a match or cancelled match

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid attribution, out-of-club player, or goal-less assist

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/match_data_entry/timeline-events" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"sessionId\": \"00000000-0000-4000-8000-000000000000\",
  \"minute\": 1,
  \"eventType\": \"goal\",
  \"side\": \"home\"
}"
```

## PUT `/match_data_entry/timeline-events`

Correct one timeline event

Replaces the facts of one event - the same single record whether it was entered post-match or pre-filled by live tagging.

Requires features: match_data_entry.match_data.manage

**Tags:** Match data

**Requires authentication.**

**Features:** match_data_entry.match_data.manage

### Request Body

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "minute": 1,
  "eventType": "goal",
  "side": "home"
}
```

### Responses

**200** – Corrected event id

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

**404** – Event or match not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**409** – Not a match or cancelled match

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid input

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X PUT "https://trainmaxr.pboj.smallhost.pl/api/match_data_entry/timeline-events" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\",
  \"minute\": 1,
  \"eventType\": \"goal\",
  \"side\": \"home\"
}"
```

## GET `/notifications`

List notifications

Returns a paginated collection of notifications.

**Tags:** Notifications

**Requires authentication.**

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| status | query | any | Optional |
| type | query | any | Optional |
| severity | query | any | Optional |
| sourceEntityType | query | any | Optional |
| sourceEntityId | query | any | Optional |
| since | query | any | Optional |
| page | query | any | Optional |
| pageSize | query | any | Optional |
| ids | query | any | Optional. Comma-separated list of record UUIDs to filter by (max 200). |

### Responses

**200** – Paginated notifications

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "type": "string",
      "title": "string",
      "body": null,
      "titleKey": null,
      "bodyKey": null,
      "titleVariables": null,
      "bodyVariables": null,
      "icon": null,
      "severity": "string",
      "status": "string",
      "actions": [
        {
          "id": "string",
          "label": "string"
        }
      ],
      "sourceModule": null,
      "sourceEntityType": null,
      "sourceEntityId": null,
      "linkHref": null,
      "createdAt": "string",
      "readAt": null,
      "actionTaken": null
    }
  ],
  "total": 1,
  "page": 1,
  "pageSize": 1,
  "totalPages": 1
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/notifications?page=1&pageSize=20" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/notifications`

Create notification

Creates a notification for a user.

Requires features: notifications.create

**Tags:** Notifications

**Requires authentication.**

**Features:** notifications.create

### Request Body

Content-Type: `application/json`

```json
{
  "type": "string",
  "severity": "info",
  "recipientUserId": "00000000-0000-4000-8000-000000000000"
}
```

### Responses

**201** – Notification created

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/notifications" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"type\": \"string\",
  \"severity\": \"info\",
  \"recipientUserId\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## POST `/notifications/{id}/action`

POST /notifications/{id}/action

**Tags:** Notifications

**Requires authentication.**

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| id | path | any | Required |

### Responses

**201** – Success response

Content-Type: `application/json`

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/notifications/:id/action" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## PUT `/notifications/{id}/dismiss`

PUT /notifications/{id}/dismiss

**Tags:** Notifications

**Requires authentication.**

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| id | path | any | Required |

### Responses

**200** – Success response

Content-Type: `application/json`

### Example

```bash
curl -X PUT "https://trainmaxr.pboj.smallhost.pl/api/notifications/:id/dismiss" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## PUT `/notifications/{id}/read`

PUT /notifications/{id}/read

**Tags:** Notifications

**Requires authentication.**

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| id | path | any | Required |

### Responses

**200** – Success response

Content-Type: `application/json`

### Example

```bash
curl -X PUT "https://trainmaxr.pboj.smallhost.pl/api/notifications/:id/read" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## PUT `/notifications/{id}/restore`

PUT /notifications/{id}/restore

**Tags:** Notifications

**Requires authentication.**

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| id | path | any | Required |

### Responses

**200** – Success response

Content-Type: `application/json`

### Example

```bash
curl -X PUT "https://trainmaxr.pboj.smallhost.pl/api/notifications/:id/restore" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/notifications/batch`

POST /notifications/batch

Requires features: notifications.create

**Tags:** Notifications

**Requires authentication.**

**Features:** notifications.create

### Responses

**201** – Success response

Content-Type: `application/json`

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/notifications/batch" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/notifications/feature`

POST /notifications/feature

Requires features: notifications.create

**Tags:** Notifications

**Requires authentication.**

**Features:** notifications.create

### Responses

**201** – Success response

Content-Type: `application/json`

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/notifications/feature" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## PUT `/notifications/mark-all-read`

PUT /notifications/mark-all-read

**Tags:** Notifications

**Requires authentication.**

### Responses

**200** – Success response

Content-Type: `application/json`

### Example

```bash
curl -X PUT "https://trainmaxr.pboj.smallhost.pl/api/notifications/mark-all-read" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/notifications/role`

POST /notifications/role

Requires features: notifications.create

**Tags:** Notifications

**Requires authentication.**

**Features:** notifications.create

### Responses

**201** – Success response

Content-Type: `application/json`

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/notifications/role" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/notifications/settings`

GET /notifications/settings

Requires features: notifications.manage

**Tags:** Notifications

**Requires authentication.**

**Features:** notifications.manage

### Responses

**200** – Success response

Content-Type: `application/json`

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/notifications/settings" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/notifications/settings`

POST /notifications/settings

Requires features: notifications.manage

**Tags:** Notifications

**Requires authentication.**

**Features:** notifications.manage

### Responses

**201** – Success response

Content-Type: `application/json`

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/notifications/settings" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/notifications/unread-count`

GET /notifications/unread-count

**Tags:** Notifications

**Requires authentication.**

### Responses

**200** – Success response

Content-Type: `application/json`

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/notifications/unread-count" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## DELETE `/player_guardian_portal/guardian-links`

End a link (the legal-age transition also ends reach automatically)

Requires features: player_guardian_portal.guardian_link.manage

**Tags:** Player and guardian portal

**Requires authentication.**

**Features:** player_guardian_portal.guardian_link.manage

### Request Body

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

### Responses

**200** – Ended link

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "status": "string"
}
```

**404** – No such link

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X DELETE "https://trainmaxr.pboj.smallhost.pl/api/player_guardian_portal/guardian-links" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/player_guardian_portal/guardian-links`

List live links

Requires features: player_guardian_portal.guardian_link.manage

**Tags:** Player and guardian portal

**Requires authentication.**

**Features:** player_guardian_portal.guardian_link.manage

### Responses

**200** – Links

Content-Type: `application/json`

```json
{
  "items": [
    {}
  ]
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/player_guardian_portal/guardian-links" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/player_guardian_portal/guardian-links`

Link a guardian account to a player (one live link per pair)

Requires features: player_guardian_portal.guardian_link.manage

**Tags:** Player and guardian portal

**Requires authentication.**

**Features:** player_guardian_portal.guardian_link.manage

### Request Body

Content-Type: `application/json`

```json
{
  "playerId": "00000000-0000-4000-8000-000000000000",
  "guardianCustomerUserId": "00000000-0000-4000-8000-000000000000"
}
```

### Responses

**200** – Created link

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

**404** – No such player

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**409** – Already linked

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/player_guardian_portal/guardian-links" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"playerId\": \"00000000-0000-4000-8000-000000000000\",
  \"guardianCustomerUserId\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/player_guardian_portal/onboarding`

Portal account state per in-scope player

Requires features: player_guardian_portal.onboarding.manage

**Tags:** Player and guardian portal

**Requires authentication.**

**Features:** player_guardian_portal.onboarding.manage

### Responses

**200** – Account state

Content-Type: `application/json`

```json
{
  "items": [
    {
      "playerId": "00000000-0000-4000-8000-000000000000",
      "account": null,
      "guardians": [
        {
          "customerUserId": "00000000-0000-4000-8000-000000000000",
          "email": "string",
          "displayName": "string",
          "status": "pending",
          "linkId": "00000000-0000-4000-8000-000000000000"
        }
      ]
    }
  ]
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/player_guardian_portal/onboarding" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/player_guardian_portal/onboarding`

Create (or re-invite) a player or guardian portal account

Creates the CustomerUser in the club organization, assigns the seeded player/guardian CustomerRole, links it to the player (guardians through the guardian-link store), and mails a set-credentials link to the portal reset-password page. Re-posting the same email resends without duplicating the account.

Requires features: player_guardian_portal.onboarding.manage

**Tags:** Player and guardian portal

**Requires authentication.**

**Features:** player_guardian_portal.onboarding.manage

### Request Body

Content-Type: `application/json`

```json
{
  "playerId": "00000000-0000-4000-8000-000000000000",
  "subject": "player",
  "email": "user@example.com"
}
```

### Responses

**200** – Provisioned account

Content-Type: `application/json`

```json
{
  "playerId": "00000000-0000-4000-8000-000000000000",
  "subject": "player",
  "customerUserId": "00000000-0000-4000-8000-000000000000",
  "email": "string",
  "isNewAccount": true,
  "inviteEmailSent": true
}
```

**404** – No such player in scope

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**409** – Already onboarded elsewhere, or the club is not portal-ready

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/player_guardian_portal/onboarding" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"playerId\": \"00000000-0000-4000-8000-000000000000\",
  \"subject\": \"player\",
  \"email\": \"user@example.com\"
}"
```

## GET `/player_guardian_portal/portal-access`

Read whether the portal is enabled for this club

Requires features: player_guardian_portal.portal_access.manage

**Tags:** Player and guardian portal

**Requires authentication.**

**Features:** player_guardian_portal.portal_access.manage

### Responses

**200** – Toggle state

Content-Type: `application/json`

```json
{
  "isEnabled": true
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/player_guardian_portal/portal-access" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## PUT `/player_guardian_portal/portal-access`

Turn the club portal on or off

Requires features: player_guardian_portal.portal_access.manage

**Tags:** Player and guardian portal

**Requires authentication.**

**Features:** player_guardian_portal.portal_access.manage

### Request Body

Content-Type: `application/json`

```json
{
  "isEnabled": true
}
```

### Responses

**200** – New toggle state

Content-Type: `application/json`

```json
{
  "isEnabled": true
}
```

### Example

```bash
curl -X PUT "https://trainmaxr.pboj.smallhost.pl/api/player_guardian_portal/portal-access" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"isEnabled\": true
}"
```

## POST `/player_guardian_portal/portal/attendance`

Upsert the one pending attendance record (availability only for a match)

**Tags:** Player and guardian portal

### Request Body

Content-Type: `application/json`

```json
{
  "playerId": "00000000-0000-4000-8000-000000000000",
  "sessionId": "00000000-0000-4000-8000-000000000000",
  "status": "present"
}
```

### Responses

**200** – The single record, pending coach acceptance

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "status": "string",
  "confirmationStatus": "string",
  "source": "string"
}
```

**404** – Player or session outside scope

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**409** – Cancelled session or player-mark precedence

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid input (an absence needs a reason)

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/player_guardian_portal/portal/attendance" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d "{
  \"playerId\": \"00000000-0000-4000-8000-000000000000\",
  \"sessionId\": \"00000000-0000-4000-8000-000000000000\",
  \"status\": \"present\"
}"
```

## POST `/player_guardian_portal/portal/deletion-requests`

Player at/above legal age, guardian below it; the archival flow fulfils it

**Tags:** Player and guardian portal

### Request Body

Content-Type: `application/json`

```json
{
  "playerId": "00000000-0000-4000-8000-000000000000"
}
```

### Responses

**202** – Request raised

Content-Type: `application/json`

```json
{
  "ok": true
}
```

**403** – The other party owns the request at this age

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**404** – Player outside the account scope

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/player_guardian_portal/portal/deletion-requests" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d "{
  \"playerId\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/player_guardian_portal/portal/matches`

Visibility-gated match list of the scoped player team

**Tags:** Player and guardian portal

### Responses

**200** – Match list, statistical pieces per club exposure

Content-Type: `application/json`

```json
{
  "items": [
    {}
  ]
}
```

**404** – Player outside the account scope

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/player_guardian_portal/portal/matches" \
  -H "Accept: application/json"
```

## GET `/player_guardian_portal/portal/notifications`

Own notifications only, newest first

**Tags:** Player and guardian portal

### Responses

**200** – Notification list

Content-Type: `application/json`

```json
{
  "items": [
    {}
  ]
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/player_guardian_portal/portal/notifications" \
  -H "Accept: application/json"
```

## GET `/player_guardian_portal/portal/players`

List the own player (player account) or linked minors (guardian account)

**Tags:** Player and guardian portal

### Responses

**200** – Scoped players

Content-Type: `application/json`

```json
{
  "items": [
    {
      "playerId": "00000000-0000-4000-8000-000000000000",
      "relation": "self",
      "firstName": null,
      "lastName": null
    }
  ]
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/player_guardian_portal/portal/players" \
  -H "Accept: application/json"
```

## PUT `/player_guardian_portal/portal/profile`

Preferred positions and dominant foot only; identity stays staff-owned

**Tags:** Player and guardian portal

### Request Body

Content-Type: `application/json`

```json
{
  "playerId": "00000000-0000-4000-8000-000000000000",
  "dominantFoot": null
}
```

### Responses

**200** – Updated player id

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

**403** – Guardian without the manage capability

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**404** – Player outside the account scope

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X PUT "https://trainmaxr.pboj.smallhost.pl/api/player_guardian_portal/portal/profile" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d "{
  \"playerId\": \"00000000-0000-4000-8000-000000000000\",
  \"dominantFoot\": null
}"
```

## GET `/player_guardian_portal/portal/schedule`

Roster-derived schedule with the one player own attendance and survey state

**Tags:** Player and guardian portal

### Responses

**200** – Schedule window

Content-Type: `application/json`

```json
{
  "items": [
    {}
  ]
}
```

**404** – Player outside the account scope

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/player_guardian_portal/portal/schedule" \
  -H "Accept: application/json"
```

## GET `/player_guardian_portal/portal/stats`

Derived figures narrowed to the statistics the club exposes

**Tags:** Player and guardian portal

### Responses

**200** – Visible statistics only; empty when the club exposes nothing

Content-Type: `application/json`

```json
{
  "playerId": "00000000-0000-4000-8000-000000000000",
  "visibleStatistics": [
    "string"
  ],
  "stats": {}
}
```

**404** – Player outside the account scope

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/player_guardian_portal/portal/stats" \
  -H "Accept: application/json"
```

## POST `/player_guardian_portal/portal/surveys`

One response per (session, player, survey kind); pre-fills a pending present mark

**Tags:** Player and guardian portal

### Request Body

Content-Type: `application/json`

```json
{
  "playerId": "00000000-0000-4000-8000-000000000000",
  "sessionId": "00000000-0000-4000-8000-000000000000"
}
```

### Responses

**200** – Write outcomes plus whether attendance was pre-filled

Content-Type: `application/json`

```json
{
  "results": [
    {
      "id": "string",
      "templateType": "string",
      "outcome": "string"
    }
  ],
  "prefilledAttendance": true
}
```

**404** – Player or session outside scope

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**409** – Cancelled session, missing minor consent, or capture mode

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid answers or empty submission

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/player_guardian_portal/portal/surveys" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d "{
  \"playerId\": \"00000000-0000-4000-8000-000000000000\",
  \"sessionId\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/player_guardian_portal/visibility`

List the live toggle rows

Requires features: player_guardian_portal.visibility.manage

**Tags:** Player and guardian portal

**Requires authentication.**

**Features:** player_guardian_portal.visibility.manage

### Responses

**200** – Toggle rows

Content-Type: `application/json`

```json
{
  "items": [
    {}
  ]
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/player_guardian_portal/visibility" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/player_guardian_portal/visibility`

Toggle one statistic at club or team scope (never per player)

Requires features: player_guardian_portal.visibility.manage

**Tags:** Player and guardian portal

**Requires authentication.**

**Features:** player_guardian_portal.visibility.manage

### Request Body

Content-Type: `application/json`

```json
{
  "scope": "club",
  "statisticKey": "match_result",
  "isVisible": true
}
```

### Responses

**200** – The one live row

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "isVisible": true
}
```

**404** – Team org not in this club

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid input

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/player_guardian_portal/visibility" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"scope\": \"club\",
  \"statisticKey\": \"match_result\",
  \"isVisible\": true
}"
```

## DELETE `/players`

Delete a player (explicit request only)

Hard-deletes whatever remains of the record. Club-admin capability, distinct from archival.

Requires features: players.player.delete

**Tags:** Players

**Requires authentication.**

**Features:** players.player.delete

### Responses

**200** – Deleted

Content-Type: `application/json`

```json
{
  "ok": true
}
```

**404** – Player not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X DELETE "https://trainmaxr.pboj.smallhost.pl/api/players" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/players`

List player profiles or fetch one by id

Team-scoped for staff, club-wide for the org-less club admin. Identity fields are decrypted per request; minor status is derived from the club legal age.

Requires features: players.player.view

**Tags:** Players

**Requires authentication.**

**Features:** players.player.view

### Responses

**200** – Players page (or `{ item }` when `id` is passed)

Content-Type: `application/json`

```json
{}
```

**400** – Invalid query

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**404** – Player not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/players" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/players`

Create a player profile

Creates an active profile on a team in the caller scope, or team-less for the club admin.

Requires features: players.player.manage

**Tags:** Players

**Requires authentication.**

**Features:** players.player.manage

### Request Body

Content-Type: `application/json`

```json
{
  "firstName": "string",
  "lastName": "string",
  "dateOfBirth": "string",
  "homeTeamOrgId": null
}
```

### Responses

**201** – Created player id

Content-Type: `application/json`

```json
{
  "id": null
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/players" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"firstName\": \"string\",
  \"lastName\": \"string\",
  \"dateOfBirth\": \"string\",
  \"homeTeamOrgId\": null
}"
```

## PUT `/players`

Update a player profile

Edits identity/sport attributes or repoints the home team within the caller scope.

Requires features: players.player.manage

**Tags:** Players

**Requires authentication.**

**Features:** players.player.manage

### Request Body

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "homeTeamOrgId": null
}
```

### Responses

**200** – Updated player id

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

**404** – Player not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**409** – Archived record is locked

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X PUT "https://trainmaxr.pboj.smallhost.pl/api/players" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\",
  \"homeTeamOrgId\": null
}"
```

## POST `/players/{id}/archive`

Anonymize and archive a player

Strips personal identifiers and hash siblings in place, keeps the de-identified row reviewable, ends the home-team pointer, and emits players.player.archived. Idempotent; club-admin capability; NOT a delete.

Requires features: players.player.archive

**Tags:** Players

**Requires authentication.**

**Features:** players.player.archive

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| id | path | any | Required |

### Responses

**200** – Archived (or already archived)

Content-Type: `application/json`

```json
{
  "ok": true,
  "alreadyArchived": true
}
```

**404** – Player not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/players/:id/archive" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/players/unassigned`

List club players on no team

Every club player with no live home team - active by default, or archived to review departed, de-identified records. Admin + coordinator capability; tenant-scoped, never cross-club.

Requires features: players.player.view_unassigned

**Tags:** Players

**Requires authentication.**

**Features:** players.player.view_unassigned

### Responses

**200** – Unassigned players page

Content-Type: `application/json`

```json
{
  "total": 1
}
```

**400** – Invalid query

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/players/unassigned" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## DELETE `/push_notifications/devices`

Unregister one of the caller own devices; push to it stops, the inbox is untouched

**Tags:** Push notifications

### Responses

**200** – Unregistered

Content-Type: `application/json`

```json
{
  "ok": true
}
```

### Example

```bash
curl -X DELETE "https://trainmaxr.pboj.smallhost.pl/api/push_notifications/devices" \
  -H "Accept: application/json"
```

## GET `/push_notifications/devices`

Own registered devices only, newest first

**Tags:** Push notifications

### Responses

**200** – Device list

Content-Type: `application/json`

```json
{
  "items": [
    {}
  ]
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/push_notifications/devices" \
  -H "Accept: application/json"
```

## POST `/push_notifications/devices`

Register (or refresh) one of the caller own devices

**Tags:** Push notifications

### Responses

**200** – Registered device id

Content-Type: `application/json`

```json
{
  "id": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/push_notifications/devices" \
  -H "Accept: application/json"
```

## GET `/push_notifications/preference`

Own effective channel preference; both is the default once a device exists

**Tags:** Push notifications

### Responses

**200** – Effective preference

Content-Type: `application/json`

```json
{
  "channel": "in-app",
  "isDefault": true
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/push_notifications/preference" \
  -H "Accept: application/json"
```

## PUT `/push_notifications/preference`

Set the caller own channel preference; push-only is not offered

**Tags:** Push notifications

### Responses

**200** – Stored preference

Content-Type: `application/json`

```json
{
  "channel": "in-app",
  "isDefault": true
}
```

### Example

```bash
curl -X PUT "https://trainmaxr.pboj.smallhost.pl/api/push_notifications/preference" \
  -H "Accept: application/json"
```

## POST `/query_index/purge`

Purge query index records

Queues a purge job to remove indexed records for an entity type within the active scope.

Requires features: query_index.purge

**Tags:** Query Index

**Requires authentication.**

**Features:** query_index.purge

### Request Body

Content-Type: `application/json`

```json
{
  "entityType": "string"
}
```

### Responses

**200** – Purge job accepted.

Content-Type: `application/json`

```json
{
  "ok": true
}
```

**400** – Missing entity type

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/query_index/purge" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"entityType\": \"string\"
}"
```

## POST `/query_index/reindex`

Trigger query index rebuild

Queues a reindex job for the specified entity type within the current tenant scope.

Requires features: query_index.reindex

**Tags:** Query Index

**Requires authentication.**

**Features:** query_index.reindex

### Request Body

Content-Type: `application/json`

```json
{
  "entityType": "string"
}
```

### Responses

**200** – Reindex job accepted.

Content-Type: `application/json`

```json
{
  "ok": true
}
```

**400** – Missing entity type

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/query_index/reindex" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"entityType\": \"string\"
}"
```

## GET `/query_index/status`

Inspect query index coverage

Returns entity counts comparing base tables with the query index along with the latest job status.

Requires features: query_index.status.view

**Tags:** Query Index

**Requires authentication.**

**Features:** query_index.status.view

### Responses

**200** – Current query index status.

Content-Type: `application/json`

```json
{
  "items": [
    {
      "entityId": "string",
      "label": "string",
      "baseCount": null,
      "indexCount": null,
      "vectorCount": null,
      "ok": true,
      "job": {
        "status": "idle",
        "startedAt": null,
        "finishedAt": null,
        "heartbeatAt": null,
        "processedCount": null,
        "totalCount": null,
        "scope": null
      }
    }
  ],
  "errors": [
    {
      "id": "string",
      "source": "string",
      "handler": "string",
      "entityType": null,
      "recordId": null,
      "tenantId": null,
      "organizationId": null,
      "message": "string",
      "stack": null,
      "payload": null,
      "occurredAt": "string"
    }
  ],
  "logs": [
    {
      "id": "string",
      "source": "string",
      "handler": "string",
      "level": "info",
      "entityType": null,
      "recordId": null,
      "tenantId": null,
      "organizationId": null,
      "message": "string",
      "details": null,
      "occurredAt": "string"
    }
  ]
}
```

**400** – Tenant or organization context required

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/query_index/status" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/reporting_and_filtering/reports/compare-teams`

Pick teams (within the caller scope) and read the KPI comparison: top-8 academy header + per-metric COUNT, AVERAGE, TREND, RANKING, COMPARISON

The selected team-org ids are intersected with the caller scope server-side (coordinator = overseen, admin = all); the header rolls up the whole scope. The portal visibility setting never gates this staff surface.

Requires features: reporting_and_filtering.reporting.view

**Tags:** Reporting and filtering

**Requires authentication.**

**Features:** reporting_and_filtering.reporting.view

### Responses

**200** – Comparison table

Content-Type: `application/json`

```json
{
  "period": {
    "source": "app"
  },
  "header": [
    {
      "key": "string",
      "value": 1
    }
  ],
  "metrics": [
    {
      "key": "string",
      "teams": [
        {
          "teamOrgId": "string",
          "count": 1,
          "average": 1,
          "trend": {
            "firstHalf": 1,
            "secondHalf": 1,
            "direction": "string"
          },
          "rank": 1,
          "comparison": {
            "selectionAverage": 1,
            "delta": 1
          }
        }
      ]
    }
  ]
}
```

**404** – A selected team is outside the caller scope

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Empty or invalid team selection or period

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/reporting_and_filtering/reports/compare-teams" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/reporting_and_filtering/reports/group`

The group report filtered by period (default: the resolved current season for the group team) and by specific statistic

Requires features: reporting_and_filtering.reporting.view

**Tags:** Reporting and filtering

**Requires authentication.**

**Features:** reporting_and_filtering.reporting.view

### Responses

**200** – Group report

Content-Type: `application/json`

```json
{
  "groupId": "00000000-0000-4000-8000-000000000000",
  "teamOrgId": "00000000-0000-4000-8000-000000000000",
  "playerCount": 1,
  "period": {
    "source": "app"
  },
  "statistics": [
    "string"
  ],
  "metrics": {}
}
```

**404** – Group not visible in the caller scope

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid period or statistic selection

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/reporting_and_filtering/reports/group" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/reporting_and_filtering/reports/player`

The individual report filtered by period (default: the resolved current season) and by specific statistic

Wraps the statistics-and-analytics player computation: with no explicit from/to the period defaults to the season resolved for the optional team context (team override, club override, app default - nearest wins). The statistics param narrows the catalogue; an unknown or team-only key is rejected. Keys on the persistent player, scoped to the caller-visible teams.

Requires features: reporting_and_filtering.reporting.view

**Tags:** Reporting and filtering

**Requires authentication.**

**Features:** reporting_and_filtering.reporting.view

### Responses

**200** – Player report

Content-Type: `application/json`

```json
{
  "playerId": "00000000-0000-4000-8000-000000000000",
  "period": {
    "source": "app"
  },
  "statistics": [
    "string"
  ],
  "metrics": {}
}
```

**404** – Player or team not visible in the caller scope

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid period or statistic selection

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/reporting_and_filtering/reports/player" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/reporting_and_filtering/reports/team`

The team report filtered by period (default: the resolved current season) and by specific statistic

The full catalogue: appearances/minutes, goals/assists/shooting, cards, clean sheets, training realisation with the per-type time breakdown, the live workload/fatigue snapshot, team results/points/form, and coach-report compliance (counts, 48h timeliness, gaps).

Requires features: reporting_and_filtering.reporting.view

**Tags:** Reporting and filtering

**Requires authentication.**

**Features:** reporting_and_filtering.reporting.view

### Responses

**200** – Team report

Content-Type: `application/json`

```json
{
  "teamOrgId": "00000000-0000-4000-8000-000000000000",
  "playerCount": 1,
  "period": {
    "source": "app"
  },
  "statistics": [
    "string"
  ],
  "metrics": {}
}
```

**404** – Team not visible in the caller scope

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid period or statistic selection

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/reporting_and_filtering/reports/team" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/reporting_and_filtering/season-window/club`

The app default, the club override (null = inherit), the resolved effective window, and the previous season

Requires features: reporting_and_filtering.reporting.view

**Tags:** Reporting and filtering

**Requires authentication.**

**Features:** reporting_and_filtering.reporting.view

### Responses

**200** – Club season window

Content-Type: `application/json`

```json
{
  "appDefault": {
    "start": "string",
    "end": "string"
  },
  "override": null,
  "effective": {
    "from": "string",
    "to": "string",
    "source": "app"
  },
  "previous": {
    "from": "string",
    "to": "string"
  }
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/reporting_and_filtering/season-window/club" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## PUT `/reporting_and_filtering/season-window/club`

Set (one concrete season instance as full dates, stored as recurring MM-DD) or clear (both null) the club override - club admin only

Requires features: reporting_and_filtering.reporting.config

**Tags:** Reporting and filtering

**Requires authentication.**

**Features:** reporting_and_filtering.reporting.config

### Responses

**200** – Stored override

Content-Type: `application/json`

```json
{
  "seasonStart": null,
  "seasonEnd": null
}
```

**422** – Impossible or half-set window

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X PUT "https://trainmaxr.pboj.smallhost.pl/api/reporting_and_filtering/season-window/club" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/reporting_and_filtering/season-window/team`

The team override (null = inherit), the resolved effective window (team, club, or app source), and the previous season

Requires features: reporting_and_filtering.reporting.view

**Tags:** Reporting and filtering

**Requires authentication.**

**Features:** reporting_and_filtering.reporting.view

### Responses

**200** – Team season window

Content-Type: `application/json`

```json
{
  "override": null,
  "effective": {
    "from": "string",
    "to": "string",
    "source": "app"
  },
  "previous": {
    "from": "string",
    "to": "string"
  },
  "teamOrgId": "00000000-0000-4000-8000-000000000000"
}
```

**404** – Team not visible in the caller scope

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid query

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/reporting_and_filtering/season-window/team" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## PUT `/reporting_and_filtering/season-window/team`

Set (a concrete season instance stored as recurring MM-DD) or clear (both null, row kept) the team override - team coach/coordinator or club admin

Requires features: reporting_and_filtering.reporting.config

**Tags:** Reporting and filtering

**Requires authentication.**

**Features:** reporting_and_filtering.reporting.config

### Responses

**200** – Stored override row

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "teamOrgId": "00000000-0000-4000-8000-000000000000",
  "seasonStart": null,
  "seasonEnd": null
}
```

**404** – Team not visible in the caller scope

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Impossible or half-set window

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X PUT "https://trainmaxr.pboj.smallhost.pl/api/reporting_and_filtering/season-window/team" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## DELETE `/rosters`

Remove a player from a roster

Ends the membership (status=ended + left date) and keeps the row as history; the player profile persists, and losing the last live membership leaves the player unassigned.

Requires features: rosters.roster.manage

**Tags:** Rosters

**Requires authentication.**

**Features:** rosters.roster.manage

### Request Body

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

### Responses

**200** – Removed

Content-Type: `application/json`

```json
{
  "ok": true
}
```

**404** – Membership not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X DELETE "https://trainmaxr.pboj.smallhost.pl/api/rosters" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/rosters`

List a team's roster

Live (active + transitioning) memberships joined with the decrypted player identity; includeEnded=true adds the retained history rows. Team-scoped: out-of-scope teams read as 404.

Requires features: rosters.roster.view

**Tags:** Rosters

**Requires authentication.**

**Features:** rosters.roster.view

### Responses

**200** – Roster rows

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "playerId": "00000000-0000-4000-8000-000000000000",
      "teamOrgId": "00000000-0000-4000-8000-000000000000",
      "status": "active",
      "joinedAt": "string",
      "leftAt": null,
      "shirtNumber": null,
      "position": null,
      "transferId": null,
      "firstName": null,
      "lastName": null
    }
  ]
}
```

**400** – Invalid query

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**404** – Team not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/rosters" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/rosters`

Add an existing club player to a roster

Creates an active membership with optional shirt number and position. A duplicate shirt number warns but saves.

Requires features: rosters.roster.manage

**Tags:** Rosters

**Requires authentication.**

**Features:** rosters.roster.manage

### Request Body

Content-Type: `application/json`

```json
{
  "playerId": "00000000-0000-4000-8000-000000000000",
  "teamOrgId": "00000000-0000-4000-8000-000000000000"
}
```

### Responses

**201** – Created membership id with soft warnings

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "warnings": [
    "string"
  ]
}
```

**404** – Team or player not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**409** – Already on this roster

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid input

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/rosters" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"playerId\": \"00000000-0000-4000-8000-000000000000\",
  \"teamOrgId\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## PUT `/rosters`

Edit the roster-level attributes of a membership

Shirt number and position only — identity edits are a player-profile action and are rejected.

Requires features: rosters.roster.manage

**Tags:** Rosters

**Requires authentication.**

**Features:** rosters.roster.manage

### Request Body

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "shirtNumber": null,
  "position": null
}
```

### Responses

**200** – Updated membership id with soft warnings

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "warnings": [
    "string"
  ]
}
```

**404** – Membership not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**409** – Membership already ended

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid input

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X PUT "https://trainmaxr.pboj.smallhost.pl/api/rosters" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\",
  \"shirtNumber\": null,
  \"position\": null
}"
```

## POST `/rosters/add-player`

Create a player profile and its first membership in one step

The common coach action: profile + active membership in one transaction. A hash-based duplicate match and a taken shirt number return soft warnings and never block.

Requires features: rosters.roster.manage

**Tags:** Rosters

**Requires authentication.**

**Features:** rosters.roster.manage

### Request Body

Content-Type: `application/json`

```json
{
  "firstName": "string",
  "lastName": "string",
  "dateOfBirth": "string",
  "teamOrgId": "00000000-0000-4000-8000-000000000000"
}
```

### Responses

**201** – Created ids with soft warnings

Content-Type: `application/json`

```json
{
  "playerId": "00000000-0000-4000-8000-000000000000",
  "membershipId": "00000000-0000-4000-8000-000000000000",
  "warnings": [
    "string"
  ],
  "duplicatePlayerIds": [
    "00000000-0000-4000-8000-000000000000"
  ]
}
```

**404** – Team not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Missing or invalid identity fields

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/rosters/add-player" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"firstName\": \"string\",
  \"lastName\": \"string\",
  \"dateOfBirth\": \"string\",
  \"teamOrgId\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/rosters/guest-candidates`

List in-club players outside the caller teams as guest candidates

The event editor's guest picker (in-club only): minimal fields, never the profile. consentStatus tells the caller whether a home-team-coach consent is required — pending for a coach, not_required for a coordinator or admin.

Requires features: rosters.roster.guest

**Tags:** Rosters

**Requires authentication.**

**Features:** rosters.roster.guest

### Responses

**200** – Guest candidates page

Content-Type: `application/json`

```json
{
  "items": [
    {
      "playerId": "00000000-0000-4000-8000-000000000000",
      "firstName": null,
      "lastName": null,
      "homeTeamOrgId": "00000000-0000-4000-8000-000000000000",
      "shirtNumber": null
    }
  ],
  "total": 1,
  "page": 1,
  "pageSize": 1,
  "consentStatus": "not_required"
}
```

**400** – Invalid query

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/rosters/guest-candidates" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/rosters/memberships`

List a player's memberships as an ordered team history

Every membership — live and ended — ordered by join date, scoped to the teams the caller can see. The history behind the profile page panel.

Requires features: rosters.roster.view

**Tags:** Rosters

**Requires authentication.**

**Features:** rosters.roster.view

### Responses

**200** – Membership history

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "playerId": "00000000-0000-4000-8000-000000000000",
      "teamOrgId": "00000000-0000-4000-8000-000000000000",
      "status": "active",
      "joinedAt": "string",
      "leftAt": null,
      "shirtNumber": null,
      "position": null,
      "transferId": null
    }
  ]
}
```

**400** – Invalid query

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**404** – Player not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/rosters/memberships" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/rosters/transfers`

Open a transfer between two club teams

Immediate mode ends the source membership and activates the destination in one action; transition mode keeps the player on both rosters until completed or cancelled. Club admin, or a coordinator overseeing both teams; a coach never.

Requires features: rosters.transfer.manage

**Tags:** Rosters

**Requires authentication.**

**Features:** rosters.transfer.manage

### Request Body

Content-Type: `application/json`

```json
{
  "playerId": "00000000-0000-4000-8000-000000000000",
  "fromTeamOrgId": "00000000-0000-4000-8000-000000000000",
  "toTeamOrgId": "00000000-0000-4000-8000-000000000000",
  "mode": "immediate"
}
```

### Responses

**201** – Opened transfer id

Content-Type: `application/json`

```json
{
  "transferId": "00000000-0000-4000-8000-000000000000"
}
```

**404** – Player not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**409** – Window already open, not on source, or already on destination

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid input or unknown team

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/rosters/transfers" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"playerId\": \"00000000-0000-4000-8000-000000000000\",
  \"fromTeamOrgId\": \"00000000-0000-4000-8000-000000000000\",
  \"toTeamOrgId\": \"00000000-0000-4000-8000-000000000000\",
  \"mode\": \"immediate\"
}"
```

## POST `/rosters/transfers/cancel`

Cancel the open transition window for a player

Ends the destination membership — never the source — leaving the player where they started.

Requires features: rosters.transfer.manage

**Tags:** Rosters

**Requires authentication.**

**Features:** rosters.transfer.manage

### Request Body

Content-Type: `application/json`

```json
{
  "playerId": "00000000-0000-4000-8000-000000000000"
}
```

### Responses

**200** – Cancelled

Content-Type: `application/json`

```json
{
  "ok": true
}
```

**409** – No open transfer

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/rosters/transfers/cancel" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"playerId\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## POST `/rosters/transfers/complete`

Complete the open transition window for a player

Ends the source membership and repoints the home org; the destination membership stays active.

Requires features: rosters.transfer.manage

**Tags:** Rosters

**Requires authentication.**

**Features:** rosters.transfer.manage

### Request Body

Content-Type: `application/json`

```json
{
  "playerId": "00000000-0000-4000-8000-000000000000"
}
```

### Responses

**200** – Completed

Content-Type: `application/json`

```json
{
  "ok": true
}
```

**409** – No open transfer

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/rosters/transfers/complete" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"playerId\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## DELETE `/sessions`

Remove an event created in error

Outright removal, distinct from a cancellation: the row is soft-deleted and disappears from every read. Use the cancel route for a real cancellation.

Requires features: sessions.session.manage

**Tags:** Sessions

**Requires authentication.**

**Features:** sessions.session.manage

### Request Body

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

### Responses

**200** – Removed

Content-Type: `application/json`

```json
{
  "ok": true
}
```

**404** – Session not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X DELETE "https://trainmaxr.pboj.smallhost.pl/api/sessions" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/sessions`

List a team's sessions and matches

The schedule read: org-scoped to the teams the caller can see, cancelled rows stay visible and marked (cancellation is not deletion). Filters: team, type, status, series, date window.

Requires features: sessions.session.view

**Tags:** Sessions

**Requires authentication.**

**Features:** sessions.session.view

### Responses

**200** – Schedule rows

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "teamOrgId": "00000000-0000-4000-8000-000000000000",
      "eventType": "training",
      "status": "scheduled",
      "startsAt": "string",
      "meetingTime": null,
      "durationMinutes": null,
      "notes": null,
      "seriesId": null,
      "matchType": null,
      "opponentName": null,
      "homeAway": null,
      "locationName": null,
      "locationAddress": null,
      "plannedPlayerCount": null,
      "reservationId": null,
      "cancelledAt": null,
      "cancelReason": null,
      "cancelNotifiedAt": null
    }
  ]
}
```

**400** – Invalid query

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**404** – Team not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/sessions" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/sessions`

Create a training session or match

One event model, two types: a match requires opponent, match type (league/friendly/tournament), home/away, a location, and a duration in minutes (any number, not fixed to 90); a training session takes only an optional duration. Emits sessions.session.created.

Requires features: sessions.session.manage

**Tags:** Sessions

**Requires authentication.**

**Features:** sessions.session.manage

### Request Body

Content-Type: `application/json`

```json
{
  "teamOrgId": "00000000-0000-4000-8000-000000000000",
  "eventType": "training",
  "startsAt": "string"
}
```

### Responses

**201** – Created event id

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

**404** – Team not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid input

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/sessions" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"teamOrgId\": \"00000000-0000-4000-8000-000000000000\",
  \"eventType\": \"training\",
  \"startsAt\": \"string\"
}"
```

## PUT `/sessions`

Edit a session or match

Delegation-aware: a delegated match-coach may edit their one delegated match. A cancelled event is not editable (409). reservationId attaches a match to a same-team facility reservation (null detaches); the reservation then supplies the match location. Emits sessions.session.updated.

Requires features: sessions.session.manage

**Tags:** Sessions

**Requires authentication.**

**Features:** sessions.session.manage

### Request Body

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "reservationId": null,
  "meetingTime": null,
  "durationMinutes": null,
  "notes": null,
  "locationName": null,
  "locationAddress": null,
  "plannedPlayerCount": null
}
```

### Responses

**200** – Updated event id

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

**404** – Session not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**409** – Cancelled event

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid input

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X PUT "https://trainmaxr.pboj.smallhost.pl/api/sessions" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\",
  \"reservationId\": null,
  \"meetingTime\": null,
  \"durationMinutes\": null,
  \"notes\": null,
  \"locationName\": null,
  \"locationAddress\": null,
  \"plannedPlayerCount\": null
}"
```

## GET `/sessions/{id}`

Read one session or match

Delegation-aware: resolvable via standing team scope OR an active single-match delegation. Anything else reads as 404.

Requires features: sessions.session.view

**Tags:** Sessions

**Requires authentication.**

**Features:** sessions.session.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| id | path | any | Required |

### Responses

**200** – The event

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

**404** – Session not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/sessions/:id" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/sessions/{id}/cancel`

Mark an event cancelled (retained, never deleted)

Sets status=cancelled with an optional free-text reason and emits sessions.session.cancelled. The cancelled event is excluded from attendance and every workload/fatigue read. Re-posting updates the reason only while the cancellation notification has not been sent (cancel_notified_at null - inert at launch).

Requires features: sessions.session.manage

**Tags:** Sessions

**Requires authentication.**

**Features:** sessions.session.manage

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| id | path | any | Required |

### Request Body

Content-Type: `application/json`

```json
{}
```

### Responses

**200** – The cancelled event

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "status": "string",
  "cancelReason": null
}
```

**404** – Session not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**409** – Reason locked after notification

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid input

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/sessions/:id/cancel" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{}"
```

## DELETE `/sessions/matches/{id}/delegation`

Revoke a delegation

Deactivates the grant; the stand-in loses access to the match immediately.

Requires features: sessions.match.delegate

**Tags:** Sessions

**Requires authentication.**

**Features:** sessions.match.delegate

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| id | path | any | Required |

### Request Body

Content-Type: `application/json`

```json
{
  "userId": "00000000-0000-4000-8000-000000000000"
}
```

### Responses

**200** – Revoked

Content-Type: `application/json`

```json
{
  "ok": true
}
```

**404** – Match or delegation not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X DELETE "https://trainmaxr.pboj.smallhost.pl/api/sessions/matches/:id/delegation" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"userId\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/sessions/matches/{id}/delegation`

List a match's active delegations

Delegation-aware read: standing team scope or the delegate themselves.

Requires features: sessions.match.view

**Tags:** Sessions

**Requires authentication.**

**Features:** sessions.match.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| id | path | any | Required |

### Responses

**200** – Active grants

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "userId": "00000000-0000-4000-8000-000000000000",
      "addedByUserId": "00000000-0000-4000-8000-000000000000",
      "createdAt": "string"
    }
  ]
}
```

**404** – Match not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/sessions/matches/:id/delegation" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/sessions/matches/{id}/delegation`

Delegate a stand-in coach for this one match

Grants a club coach view/edit access to exactly this match - no staff assignment, no standing team access. Requires standing scope on the match team (a delegate cannot re-delegate).

Requires features: sessions.match.delegate

**Tags:** Sessions

**Requires authentication.**

**Features:** sessions.match.delegate

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| id | path | any | Required |

### Request Body

Content-Type: `application/json`

```json
{
  "userId": "00000000-0000-4000-8000-000000000000"
}
```

### Responses

**201** – The grant id

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

**404** – Match or stand-in not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**409** – Already delegated

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid input or not a match

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/sessions/matches/:id/delegation" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"userId\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## POST `/sessions/series`

Create a weekly training series

Training-only by construction - a match has no recurrence path. The weekly pattern (weekdays + time, bounded by an end date or an occurrence count) is materialized into individual session rows in one transaction, emitting sessions.session.created per occurrence.

Requires features: sessions.session.manage

**Tags:** Sessions

**Requires authentication.**

**Features:** sessions.session.manage

### Request Body

Content-Type: `application/json`

```json
{
  "teamOrgId": "00000000-0000-4000-8000-000000000000",
  "weekdays": [
    "mon"
  ],
  "startDate": "string",
  "timeOfDay": "string",
  "endMode": "until_date"
}
```

### Responses

**201** – The series id with the materialized occurrence ids

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "sessionIds": [
    "00000000-0000-4000-8000-000000000000"
  ]
}
```

**404** – Team not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid input

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/sessions/series" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"teamOrgId\": \"00000000-0000-4000-8000-000000000000\",
  \"weekdays\": [
    \"mon\"
  ],
  \"startDate\": \"string\",
  \"timeOfDay\": \"string\",
  \"endMode\": \"until_date\"
}"
```

## POST `/sessions/series/cancel-forward`

Cancel the selected occurrence and all future ones

Cancels the pivot plus future scheduled occurrences (past ones stay as recorded), ends the weekly pattern, and emits sessions.session.cancelled per row. A series cancel carries NO series-level reason - the body accepts none.

Requires features: sessions.session.manage

**Tags:** Sessions

**Requires authentication.**

**Features:** sessions.session.manage

### Request Body

Content-Type: `application/json`

```json
{
  "fromSessionId": "00000000-0000-4000-8000-000000000000"
}
```

### Responses

**200** – The series id with the cancelled occurrence ids

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "sessionIds": [
    "00000000-0000-4000-8000-000000000000"
  ]
}
```

**404** – Session or series not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid input or not a series occurrence

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/sessions/series/cancel-forward" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"fromSessionId\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## POST `/sessions/series/edit-forward`

Apply a pattern edit to the selected occurrence and all future ones

Pivots on the acted-on occurrence: only scheduled rows starting at or after it change; past occurrences are never touched. A weekday/end-bound change re-materializes the future occurrences. Emits sessions.session.updated per touched row.

Requires features: sessions.session.manage

**Tags:** Sessions

**Requires authentication.**

**Features:** sessions.session.manage

### Request Body

Content-Type: `application/json`

```json
{
  "fromSessionId": "00000000-0000-4000-8000-000000000000",
  "patch": {
    "durationMinutes": null,
    "meetingTime": null,
    "notes": null
  }
}
```

### Responses

**200** – The series id with the touched occurrence ids

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "sessionIds": [
    "00000000-0000-4000-8000-000000000000"
  ]
}
```

**404** – Session or series not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid input or not a series occurrence

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/sessions/series/edit-forward" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"fromSessionId\": \"00000000-0000-4000-8000-000000000000\",
  \"patch\": {
    \"durationMinutes\": null,
    \"meetingTime\": null,
    \"notes\": null
  }
}"
```

## DELETE `/statistics_and_analytics/player-groups`

Delete a group and its membership; the underlying statistics are untouched

Requires features: statistics_and_analytics.player_group.manage

**Tags:** Statistics and analytics

**Requires authentication.**

**Features:** statistics_and_analytics.player_group.manage

### Request Body

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

### Responses

**200** – Deleted group id

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

**404** – Group not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid input

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X DELETE "https://trainmaxr.pboj.smallhost.pl/api/statistics_and_analytics/player-groups" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/statistics_and_analytics/player-groups`

List the visible teams' player groups with their memberships

Requires features: statistics_and_analytics.statistics.view

**Tags:** Statistics and analytics

**Requires authentication.**

**Features:** statistics_and_analytics.statistics.view

### Responses

**200** – Player groups

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "teamOrgId": "00000000-0000-4000-8000-000000000000",
      "name": "string",
      "description": null,
      "memberPlayerIds": [
        "00000000-0000-4000-8000-000000000000"
      ]
    }
  ]
}
```

**404** – Team not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid query

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/statistics_and_analytics/player-groups" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## PATCH `/statistics_and_analytics/player-groups`

Rename a group (a team move is not expressible)

Requires features: statistics_and_analytics.player_group.manage

**Tags:** Statistics and analytics

**Requires authentication.**

**Features:** statistics_and_analytics.player_group.manage

### Request Body

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "name": "string"
}
```

### Responses

**200** – Renamed group id

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

**404** – Group not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid input

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X PATCH "https://trainmaxr.pboj.smallhost.pl/api/statistics_and_analytics/player-groups" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\",
  \"name\": \"string\"
}"
```

## POST `/statistics_and_analytics/player-groups`

Create a named group on one team, optionally seeded with members

The group belongs to exactly one team and its members FK the persistent club player, so a membership survives roster changes and transfers. Rejected out of the caller's per-team scope (404) or when a member is not a club player (422).

Requires features: statistics_and_analytics.player_group.manage

**Tags:** Statistics and analytics

**Requires authentication.**

**Features:** statistics_and_analytics.player_group.manage

### Request Body

Content-Type: `application/json`

```json
{
  "teamOrgId": "00000000-0000-4000-8000-000000000000",
  "name": "string",
  "memberPlayerIds": []
}
```

### Responses

**200** – Created group id

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

**404** – Team not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid input

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/statistics_and_analytics/player-groups" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"teamOrgId\": \"00000000-0000-4000-8000-000000000000\",
  \"name\": \"string\",
  \"memberPlayerIds\": []
}"
```

## DELETE `/statistics_and_analytics/player-groups/members`

Remove a player from a group; the group aggregates recompute over the rest

Requires features: statistics_and_analytics.player_group.manage

**Tags:** Statistics and analytics

**Requires authentication.**

**Features:** statistics_and_analytics.player_group.manage

### Request Body

Content-Type: `application/json`

```json
{
  "groupId": "00000000-0000-4000-8000-000000000000",
  "playerId": "00000000-0000-4000-8000-000000000000"
}
```

### Responses

**200** – Removed member id

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

**404** – Group or member not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid input

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X DELETE "https://trainmaxr.pboj.smallhost.pl/api/statistics_and_analytics/player-groups/members" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"groupId\": \"00000000-0000-4000-8000-000000000000\",
  \"playerId\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## POST `/statistics_and_analytics/player-groups/members`

Add a club player to a group (idempotent for an existing member)

Requires features: statistics_and_analytics.player_group.manage

**Tags:** Statistics and analytics

**Requires authentication.**

**Features:** statistics_and_analytics.player_group.manage

### Request Body

Content-Type: `application/json`

```json
{
  "groupId": "00000000-0000-4000-8000-000000000000",
  "playerId": "00000000-0000-4000-8000-000000000000"
}
```

### Responses

**200** – Member id

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

**404** – Group not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid input or not a club player

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/statistics_and_analytics/player-groups/members" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"groupId\": \"00000000-0000-4000-8000-000000000000\",
  \"playerId\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/statistics_and_analytics/statistics/group`

The same aggregates as the team read, restricted to the group's member set

An empty group reads zero aggregates rather than erroring. Accepts an optional from/to period.

Requires features: statistics_and_analytics.statistics.view

**Tags:** Statistics and analytics

**Requires authentication.**

**Features:** statistics_and_analytics.statistics.view

### Responses

**200** – Group statistics

Content-Type: `application/json`

```json
{
  "teamOrgId": "00000000-0000-4000-8000-000000000000",
  "playerCount": 1,
  "totals": {
    "goals": 1,
    "assists": 1,
    "yellowCards": 1,
    "redCards": 1,
    "minutesPlayed": 1,
    "starterAppearances": 1,
    "substituteAppearances": 1,
    "cleanSheets": 1,
    "goalsConcededOnPitch": 1,
    "trainingSessions": 1
  },
  "averages": {
    "goals": 1,
    "assists": 1,
    "yellowCards": 1,
    "redCards": 1,
    "minutesPlayed": 1,
    "starterAppearances": 1,
    "substituteAppearances": 1,
    "cleanSheets": 1,
    "goalsConcededOnPitch": 1,
    "trainingSessions": 1
  },
  "timePerTrainingElement": [
    {
      "categoryId": "string",
      "categoryName": null,
      "minutes": 1
    }
  ],
  "players": [
    {
      "playerId": "00000000-0000-4000-8000-000000000000",
      "goals": 1,
      "assists": 1,
      "yellowCards": 1,
      "redCards": 1,
      "minutesPlayed": 1,
      "starterAppearances": 1,
      "substituteAppearances": 1,
      "cleanSheets": 1,
      "goalsConcededOnPitch": 1,
      "trainingSessions": 1
    }
  ],
  "groupId": "00000000-0000-4000-8000-000000000000"
}
```

**404** – Group not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid query or period

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/statistics_and_analytics/statistics/group" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/statistics_and_analytics/statistics/player`

A player's goals, assists, cards, minutes (total and per month), appearances split, training sessions, time per training element, clean sheets, and goals conceded while on the pitch

Computed live over the input layer (PRD 02 derived data) and scoped to the caller's visible teams: a coach reads only the assigned-team portion of the career, the club admin the whole. Clean sheets and goals-conceded are interval-accurate over the player's on-pitch intervals from the recorded goal minutes. Team-level tagging metrics are never part of this read. Accepts an optional from/to period.

Requires features: statistics_and_analytics.statistics.view

**Tags:** Statistics and analytics

**Requires authentication.**

**Features:** statistics_and_analytics.statistics.view

### Responses

**200** – Player statistics

Content-Type: `application/json`

```json
{
  "playerId": "00000000-0000-4000-8000-000000000000",
  "goals": 1,
  "assists": 1,
  "yellowCards": 1,
  "redCards": 1,
  "minutesPlayed": 1,
  "starterAppearances": 1,
  "substituteAppearances": 1,
  "cleanSheets": 1,
  "goalsConcededOnPitch": 1,
  "trainingSessions": 1,
  "minutesPerMonth": [
    {
      "month": "string",
      "minutes": 1
    }
  ],
  "timePerTrainingElement": [
    {
      "categoryId": "string",
      "categoryName": null,
      "minutes": 1
    }
  ]
}
```

**404** – Player not found in any visible team

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid query or period

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/statistics_and_analytics/statistics/player" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/statistics_and_analytics/statistics/rankings`

Teams and top players ranked across the teams the caller may see

The coordinator sees everything on overseen teams, including cross-team rankings; the org-less club admin ranks every club team. The broad academy KPI catalogue is assembled by the reporting slice on top of these figures. Accepts an optional from/to period.

Requires features: statistics_and_analytics.statistics.view

**Tags:** Statistics and analytics

**Requires authentication.**

**Features:** statistics_and_analytics.statistics.view

### Responses

**200** – Rankings

Content-Type: `application/json`

```json
{
  "teams": [
    {
      "teamOrgId": "00000000-0000-4000-8000-000000000000",
      "goals": 1,
      "assists": 1,
      "minutesPlayed": 1
    }
  ],
  "players": [
    {
      "goals": 1,
      "assists": 1,
      "minutesPlayed": 1,
      "playerId": "00000000-0000-4000-8000-000000000000",
      "teamOrgIds": [
        "00000000-0000-4000-8000-000000000000"
      ]
    }
  ]
}
```

**422** – Invalid period

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/statistics_and_analytics/statistics/rankings" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/statistics_and_analytics/statistics/team`

The individual metrics aggregated over the team's players as totals and per-player averages, with a per-player table

Aggregation is over the input records of the team's scope, so a mid-transfer player contributes only that team's rows - never double-counted. Team-level tagging metrics (shots/tackles/fouls) are not part of this aggregate. Accepts an optional from/to period.

Requires features: statistics_and_analytics.statistics.view

**Tags:** Statistics and analytics

**Requires authentication.**

**Features:** statistics_and_analytics.statistics.view

### Responses

**200** – Team statistics

Content-Type: `application/json`

```json
{
  "teamOrgId": "00000000-0000-4000-8000-000000000000",
  "playerCount": 1,
  "totals": {
    "goals": 1,
    "assists": 1,
    "yellowCards": 1,
    "redCards": 1,
    "minutesPlayed": 1,
    "starterAppearances": 1,
    "substituteAppearances": 1,
    "cleanSheets": 1,
    "goalsConcededOnPitch": 1,
    "trainingSessions": 1
  },
  "averages": {
    "goals": 1,
    "assists": 1,
    "yellowCards": 1,
    "redCards": 1,
    "minutesPlayed": 1,
    "starterAppearances": 1,
    "substituteAppearances": 1,
    "cleanSheets": 1,
    "goalsConcededOnPitch": 1,
    "trainingSessions": 1
  },
  "timePerTrainingElement": [
    {
      "categoryId": "string",
      "categoryName": null,
      "minutes": 1
    }
  ],
  "players": [
    {
      "playerId": "00000000-0000-4000-8000-000000000000",
      "goals": 1,
      "assists": 1,
      "yellowCards": 1,
      "redCards": 1,
      "minutesPlayed": 1,
      "starterAppearances": 1,
      "substituteAppearances": 1,
      "cleanSheets": 1,
      "goalsConcededOnPitch": 1,
      "trainingSessions": 1
    }
  ]
}
```

**404** – Team not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid query or period

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/statistics_and_analytics/statistics/team" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/surveys_rpe`

List RPE and wellness responses

The per-player survey history attached to the persistent player profile: filter by session, player, team, or template type. Scoped staff read only their visible team-orgs; the history follows the player across roster changes and transfers.

Requires features: surveys_rpe.survey.view

**Tags:** Surveys and RPE

**Requires authentication.**

**Features:** surveys_rpe.survey.view

### Responses

**200** – Responses

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "sessionId": "00000000-0000-4000-8000-000000000000",
      "playerId": "00000000-0000-4000-8000-000000000000",
      "teamOrgId": "00000000-0000-4000-8000-000000000000",
      "templateId": null,
      "templateType": "rpe",
      "rpe": null,
      "answers": {
        "key": "string"
      },
      "source": "staff",
      "submittedByUserId": null,
      "submittedAt": "string",
      "exerciseLines": [
        {
          "label": "string",
          "rpe": 1,
          "sortOrder": 1
        }
      ]
    }
  ]
}
```

**400** – Invalid query

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**404** – Out-of-scope team or session

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/surveys_rpe" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/surveys_rpe/consents`

List a player's live recorded consents

Requires features: surveys_rpe.survey.view

**Tags:** Surveys and RPE

**Requires authentication.**

**Features:** surveys_rpe.survey.view

### Responses

**200** – Consents

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "playerId": "00000000-0000-4000-8000-000000000000",
      "teamOrgId": null,
      "guardianName": "string",
      "grantedByUserId": "00000000-0000-4000-8000-000000000000",
      "grantedAt": "string"
    }
  ]
}
```

**400** – Invalid query

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**404** – Player out of scope

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/surveys_rpe/consents" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/surveys_rpe/consents`

Record a guardian consent for a player

Files the guardian's consent so a minor's RPE/wellness capture passes the club's consent gate; a re-grant corrects the single live row.

Requires features: surveys_rpe.survey.manage

**Tags:** Surveys and RPE

**Requires authentication.**

**Features:** surveys_rpe.survey.manage

### Request Body

Content-Type: `application/json`

```json
{
  "playerId": "00000000-0000-4000-8000-000000000000",
  "guardianName": "string"
}
```

### Responses

**200** – The recorded consent

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "playerId": "00000000-0000-4000-8000-000000000000",
  "teamOrgId": null,
  "guardianName": "string",
  "grantedByUserId": "00000000-0000-4000-8000-000000000000",
  "grantedAt": "string"
}
```

**404** – Player out of scope

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid input

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/surveys_rpe/consents" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"playerId\": \"00000000-0000-4000-8000-000000000000\",
  \"guardianName\": \"string\"
}"
```

## GET `/surveys_rpe/sessions/{id}/kiosk`

List the kiosk's selectable roster names

The session team's active-membership roster as selectable names with per-player submitted flags - only names, never another player's answers. A cancelled session offers no names.

Requires features: surveys_rpe.survey.manage

**Tags:** Surveys and RPE

**Requires authentication.**

**Features:** surveys_rpe.survey.manage

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| id | path | any | Required |

### Responses

**200** – The selectable roster

Content-Type: `application/json`

```json
{
  "session": {
    "id": "00000000-0000-4000-8000-000000000000",
    "status": "string"
  },
  "rows": [
    {
      "playerId": "00000000-0000-4000-8000-000000000000",
      "firstName": null,
      "lastName": null,
      "hasRpe": true,
      "hasWellness": true
    }
  ]
}
```

**404** – Session not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/surveys_rpe/sessions/:id/kiosk" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/surveys_rpe/sessions/{id}/kiosk`

Submit one player's own RPE/wellness survey with no login

A player selects their name and answers; the response saves to their profile with source 'kiosk' and no personal login, within the opening staff's team scope. The kiosk never takes attendance, and in hybrid mode an already-recorded response stands (first-write-wins).

Requires features: surveys_rpe.survey.manage

**Tags:** Surveys and RPE

**Requires authentication.**

**Features:** surveys_rpe.survey.manage

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| id | path | any | Required |

### Request Body

Content-Type: `application/json`

```json
{
  "playerId": "00000000-0000-4000-8000-000000000000"
}
```

### Responses

**200** – Capture outcomes

Content-Type: `application/json`

```json
{
  "results": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "templateType": "string",
      "outcome": "created"
    }
  ]
}
```

**404** – Session not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**409** – Cancelled session, capture-mode mismatch, or missing minor consent

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid input or off-roster player

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/surveys_rpe/sessions/:id/kiosk" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"playerId\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/surveys_rpe/sessions/{id}/sheet`

Read the roster survey sheet for one session

The derived sheet the coach (or kiosk) fills, in one request: the session team's memberships active at session time joined to the decrypted player identity, each player's current RPE (with exercise breakdown) and wellness responses, the attendance state for the combined roll-call, and the resolved RPE/wellness templates with their capture modes. A cancelled session returns the sheet closed (no row markable).

Requires features: surveys_rpe.survey.view

**Tags:** Surveys and RPE

**Requires authentication.**

**Features:** surveys_rpe.survey.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| id | path | any | Required |

### Responses

**200** – The survey sheet

Content-Type: `application/json`

```json
{
  "session": {
    "id": "00000000-0000-4000-8000-000000000000",
    "teamOrgId": "00000000-0000-4000-8000-000000000000",
    "eventType": "string",
    "status": "string",
    "startsAt": "string"
  },
  "templates": {
    "rpe": null,
    "wellness": null,
    "rpeCaptureMode": "string",
    "wellnessCaptureMode": "string"
  },
  "rows": [
    {
      "playerId": "00000000-0000-4000-8000-000000000000",
      "firstName": null,
      "lastName": null,
      "markable": true,
      "rpe": null,
      "wellness": null,
      "attendanceStatus": null
    }
  ]
}
```

**404** – Session not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/surveys_rpe/sessions/:id/sheet" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/surveys_rpe/sessions/{id}/sheet`

Capture the whole sheet in one pass (combined roll-call)

Upserts per player at most one RPE response (with its in-survey exercise breakdown) and one wellness response on (session, player, type), honoring the capture-mode source gate, the cancelled-session reject, and the minors consent gate; an optional attendanceStatus per entry upserts the independent attendance record in the same pass. In hybrid mode an existing response from the other path is kept, first-write-wins.

Requires features: surveys_rpe.survey.manage

**Tags:** Surveys and RPE

**Requires authentication.**

**Features:** surveys_rpe.survey.manage

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| id | path | any | Required |

### Request Body

Content-Type: `application/json`

```json
{
  "entries": [
    {
      "playerId": "00000000-0000-4000-8000-000000000000"
    }
  ]
}
```

### Responses

**200** – Capture outcomes per written response

Content-Type: `application/json`

```json
{
  "results": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "playerId": "00000000-0000-4000-8000-000000000000",
      "templateType": "string",
      "outcome": "created"
    }
  ]
}
```

**404** – Session not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**409** – Cancelled session, capture-mode mismatch, or missing minor consent

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid input, off-roster player, or invalid answers

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/surveys_rpe/sessions/:id/sheet" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"entries\": [
    {
      \"playerId\": \"00000000-0000-4000-8000-000000000000\"
    }
  ]
}"
```

## GET `/surveys_rpe/settings`

Read a team's default capture mode

Requires features: surveys_rpe.survey.view

**Tags:** Surveys and RPE

**Requires authentication.**

**Features:** surveys_rpe.survey.view

### Responses

**200** – The stored default (null = inherit coach_panel)

Content-Type: `application/json`

```json
{
  "teamOrgId": "00000000-0000-4000-8000-000000000000",
  "defaultCaptureMode": null
}
```

**400** – Invalid query

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**404** – Team out of scope

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/surveys_rpe/settings" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/surveys_rpe/settings`

Set a team's default capture mode

Stores the team-level default a survey without an explicit capture mode inherits. Coach on that team or club_admin; a coordinator never configures surveys.

Requires features: surveys_rpe.template.manage

**Tags:** Surveys and RPE

**Requires authentication.**

**Features:** surveys_rpe.template.manage

### Request Body

Content-Type: `application/json`

```json
{
  "teamOrgId": "00000000-0000-4000-8000-000000000000",
  "defaultCaptureMode": "self_report"
}
```

### Responses

**200** – The stored default

Content-Type: `application/json`

```json
{
  "teamOrgId": "00000000-0000-4000-8000-000000000000",
  "defaultCaptureMode": null
}
```

**422** – Unrecognized capture mode

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/surveys_rpe/settings" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"teamOrgId\": \"00000000-0000-4000-8000-000000000000\",
  \"defaultCaptureMode\": \"self_report\"
}"
```

## GET `/surveys_rpe/templates`

List the club default and per-team survey templates

Requires features: surveys_rpe.survey.view

**Tags:** Surveys and RPE

**Requires authentication.**

**Features:** surveys_rpe.survey.view

### Responses

**200** – Templates

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "teamOrgId": null,
      "templateType": "string",
      "captureMode": null,
      "isDefault": true,
      "definition": {
        "questions": [
          {}
        ]
      },
      "createdByUserId": null
    }
  ]
}
```

**400** – Invalid query

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/surveys_rpe/templates" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/surveys_rpe/templates`

Create or correct one survey template

Upserts the single live template per (type, scope): the reused club default RPE survey (teamOrgId null, club_admin only) or a per-team wellness survey composed from the question bank plus custom fields (coach on that team or club_admin). A coordinator never configures surveys.

Requires features: surveys_rpe.template.manage

**Tags:** Surveys and RPE

**Requires authentication.**

**Features:** surveys_rpe.template.manage

### Request Body

Content-Type: `application/json`

```json
{
  "templateType": "rpe",
  "teamOrgId": null,
  "captureMode": null,
  "definition": {
    "questions": [
      {
        "id": "string",
        "label": "string",
        "type": "scale_1_5",
        "required": true
      }
    ]
  }
}
```

### Responses

**200** – The saved template

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "teamOrgId": null,
  "templateType": "string",
  "captureMode": null,
  "isDefault": true,
  "definition": {
    "questions": [
      {}
    ]
  },
  "createdByUserId": null
}
```

**422** – Invalid definition or capture mode

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/surveys_rpe/templates" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"templateType\": \"rpe\",
  \"teamOrgId\": null,
  \"captureMode\": null,
  \"definition\": {
    \"questions\": [
      {
        \"id\": \"string\",
        \"label\": \"string\",
        \"type\": \"scale_1_5\",
        \"required\": true
      }
    ]
  }
}"
```

## DELETE `/teams/staff`

Remove a member from a team

Ends team access for that team; club membership and identity persist.

Requires features: teams.team.manage

**Tags:** Teams

**Requires authentication.**

**Features:** teams.team.manage

### Request Body

Content-Type: `application/json`

```json
{
  "userId": "00000000-0000-4000-8000-000000000000",
  "teamOrgId": "00000000-0000-4000-8000-000000000000"
}
```

### Responses

**200** – Removed

Content-Type: `application/json`

```json
{
  "ok": true
}
```

**404** – Assignment not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X DELETE "https://trainmaxr.pboj.smallhost.pl/api/teams/staff" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"userId\": \"00000000-0000-4000-8000-000000000000\",
  \"teamOrgId\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/teams/staff`

List staff assignments

Lists coach/coordinator assignments over the caller's visible teams (optionally one team).

Requires features: teams.team.view

**Tags:** Teams

**Requires authentication.**

**Features:** teams.team.view

### Responses

**200** – Assignments

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "string",
      "userId": "string",
      "teamOrgId": "string",
      "role": "string",
      "userEmail": null,
      "userName": null
    }
  ]
}
```

**404** – Team not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/teams/staff" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/teams/staff`

Assign an existing club member to teams

Admin: any role on any club team. Coordinator: only an existing club coach onto an overseen team.

Requires features: teams.staff.assign

**Tags:** Teams

**Requires authentication.**

**Features:** teams.staff.assign

### Request Body

Content-Type: `application/json`

```json
{
  "userId": "00000000-0000-4000-8000-000000000000",
  "assignments": [
    {
      "teamOrgId": "00000000-0000-4000-8000-000000000000",
      "role": "coach"
    }
  ]
}
```

### Responses

**200** – Assigned

Content-Type: `application/json`

```json
{
  "ok": true
}
```

**404** – Out of scope

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**409** – Already assigned

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/teams/staff" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"userId\": \"00000000-0000-4000-8000-000000000000\",
  \"assignments\": [
    {
      \"teamOrgId\": \"00000000-0000-4000-8000-000000000000\",
      \"role\": \"coach\"
    }
  ]
}"
```

## POST `/teams/staff/invite`

Invite staff

Club admin only: invites a net-new email into the club with team assignments. The invitee sets their own password from the emailed link.

Requires features: teams.invite.staff

**Tags:** Teams

**Requires authentication.**

**Features:** teams.invite.staff

### Request Body

Content-Type: `application/json`

```json
{
  "email": "user@example.com",
  "name": null,
  "assignments": [
    {
      "teamOrgId": "00000000-0000-4000-8000-000000000000",
      "role": "coach"
    }
  ]
}
```

### Responses

**201** – Invited

Content-Type: `application/json`

```json
{
  "userId": "string",
  "inviteEmailSent": true
}
```

**409** – Already a club member

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid payload

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/teams/staff/invite" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"email\": \"user@example.com\",
  \"name\": null,
  \"assignments\": [
    {
      \"teamOrgId\": \"00000000-0000-4000-8000-000000000000\",
      \"role\": \"coach\"
    }
  ]
}"
```

## GET `/teams/teams`

List teams

Lists the caller's visible teams (per-team scope; admins see the whole club). Archived teams are excluded unless includeArchived=true.

Requires features: teams.team.view

**Tags:** Teams

**Requires authentication.**

**Features:** teams.team.view

### Responses

**200** – Teams

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "string",
      "name": "string",
      "slug": null,
      "sport": "string",
      "ageGroup": "string",
      "parentId": null,
      "logoUrl": null,
      "isActive": true
    }
  ]
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/teams/teams" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/teams/teams`

Create a team

Creates a team (directory org + profile) in the caller club. Club admin only.

Requires features: teams.team.manage

**Tags:** Teams

**Requires authentication.**

**Features:** teams.team.manage

### Request Body

Content-Type: `application/json`

```json
{
  "name": "string",
  "sport": "string",
  "ageGroup": "string",
  "parentId": null,
  "logoUrl": ""
}
```

### Responses

**201** – Created team

Content-Type: `application/json`

```json
{
  "team": {
    "id": "string",
    "name": "string",
    "slug": null,
    "sport": "string",
    "ageGroup": "string",
    "parentId": null,
    "logoUrl": null,
    "isActive": true
  }
}
```

**422** – Invalid payload

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/teams/teams" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"name\": \"string\",
  \"sport\": \"string\",
  \"ageGroup\": \"string\",
  \"parentId\": null,
  \"logoUrl\": \"\"
}"
```

## PUT `/teams/teams`

Update a team

Renames, re-attributes, sets/clears the logo override, re-groups, or archives/reactivates a team. Club admin only.

Requires features: teams.team.manage

**Tags:** Teams

**Requires authentication.**

**Features:** teams.team.manage

### Request Body

Content-Type: `application/json`

```json
{
  "parentId": null,
  "id": "00000000-0000-4000-8000-000000000000"
}
```

### Responses

**200** – Updated team

Content-Type: `application/json`

```json
{
  "team": {
    "id": "string",
    "name": "string",
    "slug": null,
    "sport": "string",
    "ageGroup": "string",
    "parentId": null,
    "logoUrl": null,
    "isActive": true
  }
}
```

**404** – Team not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid payload

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X PUT "https://trainmaxr.pboj.smallhost.pl/api/teams/teams" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"parentId\": null,
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## DELETE `/training_content_tally/categories`

Remove an empty club-custom category

Requires features: training_content_tally.catalogue.manage

**Tags:** Training content

**Requires authentication.**

**Features:** training_content_tally.catalogue.manage

### Request Body

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

### Responses

**200** – Removed

Content-Type: `application/json`

```json
{
  "ok": true
}
```

**409** – Category still holds behaviors

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Core category protected

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X DELETE "https://trainmaxr.pboj.smallhost.pl/api/training_content_tally/categories" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/training_content_tally/categories`

List the club categories (core first), seeding the defaults for a fresh club

Requires features: training_content_tally.content.view

**Tags:** Training content

**Requires authentication.**

**Features:** training_content_tally.content.view

### Responses

**200** – Categories

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "name": "string",
      "code": null,
      "isCore": true,
      "isActive": true
    }
  ]
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/training_content_tally/categories" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/training_content_tally/categories`

Add a club-custom category

Requires features: training_content_tally.catalogue.manage

**Tags:** Training content

**Requires authentication.**

**Features:** training_content_tally.catalogue.manage

### Request Body

Content-Type: `application/json`

```json
{
  "name": "string"
}
```

### Responses

**201** – Created category id

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

**422** – Invalid input

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/training_content_tally/categories" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"name\": \"string\"
}"
```

## PUT `/training_content_tally/categories`

Rename or retire a club-custom category (core rows are protected)

Requires features: training_content_tally.catalogue.manage

**Tags:** Training content

**Requires authentication.**

**Features:** training_content_tally.catalogue.manage

### Request Body

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

### Responses

**200** – Updated category id

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

**404** – Category not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Core category protected

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X PUT "https://trainmaxr.pboj.smallhost.pl/api/training_content_tally/categories" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/training_content_tally/drift`

A team's per-category drift over the configured window (no target means no alert)

Requires features: training_content_tally.content.view

**Tags:** Training content

**Requires authentication.**

**Features:** training_content_tally.content.view

### Responses

**200** – Drift read

Content-Type: `application/json`

```json
{
  "teamOrgId": "00000000-0000-4000-8000-000000000000",
  "roundKey": "string",
  "thresholdPct": 1,
  "window": null,
  "target": null,
  "actual": [
    {
      "categoryId": "string",
      "count": 1,
      "pct": 1
    }
  ],
  "alerts": [
    {
      "categoryId": "string",
      "targetPct": 1,
      "actualPct": 1,
      "deviationPp": 1,
      "drifted": true
    }
  ],
  "hasDrift": true
}
```

**400** – Invalid query

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**404** – Team not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/training_content_tally/drift" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## DELETE `/training_content_tally/elements`

Remove a behavior created in error (a tagged behavior is retired instead)

Requires features: training_content_tally.catalogue.manage

**Tags:** Training content

**Requires authentication.**

**Features:** training_content_tally.catalogue.manage

### Request Body

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

### Responses

**200** – Removed

Content-Type: `application/json`

```json
{
  "ok": true
}
```

**409** – Behavior already tagged

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X DELETE "https://trainmaxr.pboj.smallhost.pl/api/training_content_tally/elements" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/training_content_tally/elements`

List catalogue behaviors, filterable by category and active flag

Requires features: training_content_tally.content.view

**Tags:** Training content

**Requires authentication.**

**Features:** training_content_tally.content.view

### Responses

**200** – Behaviors

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "name": "string",
      "categoryId": "00000000-0000-4000-8000-000000000000",
      "methodId": null,
      "isActive": true
    }
  ]
}
```

**400** – Invalid query

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/training_content_tally/elements" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/training_content_tally/elements`

Add a behavior under exactly one category, optionally linked to a library method

Requires features: training_content_tally.catalogue.manage

**Tags:** Training content

**Requires authentication.**

**Features:** training_content_tally.catalogue.manage

### Request Body

Content-Type: `application/json`

```json
{
  "name": "string",
  "categoryId": "00000000-0000-4000-8000-000000000000"
}
```

### Responses

**201** – Created behavior id

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

**422** – Invalid category

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/training_content_tally/elements" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"name\": \"string\",
  \"categoryId\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## PUT `/training_content_tally/elements`

Rename, recategorize, retire, or relink a behavior

Requires features: training_content_tally.catalogue.manage

**Tags:** Training content

**Requires authentication.**

**Features:** training_content_tally.catalogue.manage

### Request Body

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "methodId": null
}
```

### Responses

**200** – Updated behavior id

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

**404** – Behavior not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid category

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X PUT "https://trainmaxr.pboj.smallhost.pl/api/training_content_tally/elements" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\",
  \"methodId\": null
}"
```

## GET `/training_content_tally/fatigue-correlation`

A team's tagged elements with the correlation deferred until fatigue figures exist

Requires features: training_content_tally.content.view

**Tags:** Training content

**Requires authentication.**

**Features:** training_content_tally.content.view

### Responses

**200** – Correlation contract read

Content-Type: `application/json`

```json
{
  "teamOrgId": "00000000-0000-4000-8000-000000000000",
  "status": "deferred",
  "elements": [
    {
      "elementId": "string",
      "categoryId": "string",
      "tagCount": 1,
      "sessionCount": 1
    }
  ]
}
```

**400** – Invalid query

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**404** – Team not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/training_content_tally/fatigue-correlation" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## DELETE `/training_content_tally/plan-targets`

Clear a (team, round) target set

Requires features: training_content_tally.content.manage

**Tags:** Training content

**Requires authentication.**

**Features:** training_content_tally.content.manage

### Request Body

Content-Type: `application/json`

```json
{
  "teamOrgId": "00000000-0000-4000-8000-000000000000",
  "roundKey": "string"
}
```

### Responses

**200** – Cleared

Content-Type: `application/json`

```json
{
  "ok": true
}
```

**404** – Team not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X DELETE "https://trainmaxr.pboj.smallhost.pl/api/training_content_tally/plan-targets" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"teamOrgId\": \"00000000-0000-4000-8000-000000000000\",
  \"roundKey\": \"string\"
}"
```

## GET `/training_content_tally/plan-targets`

List a team's targets, filterable by round

Requires features: training_content_tally.content.view

**Tags:** Training content

**Requires authentication.**

**Features:** training_content_tally.content.view

### Responses

**200** – Targets

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "teamOrgId": "00000000-0000-4000-8000-000000000000",
      "roundKey": "string",
      "categoryId": "00000000-0000-4000-8000-000000000000",
      "targetPct": 1
    }
  ]
}
```

**400** – Invalid query

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**404** – Team not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/training_content_tally/plan-targets" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## PUT `/training_content_tally/plan-targets`

Replace a (team, round) target set - the percentages must total exactly 100

Requires features: training_content_tally.content.manage

**Tags:** Training content

**Requires authentication.**

**Features:** training_content_tally.content.manage

### Request Body

Content-Type: `application/json`

```json
{
  "teamOrgId": "00000000-0000-4000-8000-000000000000",
  "roundKey": "string",
  "targets": [
    {
      "categoryId": "00000000-0000-4000-8000-000000000000",
      "targetPct": 1
    }
  ]
}
```

### Responses

**200** – The stored set

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "teamOrgId": "00000000-0000-4000-8000-000000000000",
      "roundKey": "string",
      "categoryId": "00000000-0000-4000-8000-000000000000",
      "targetPct": 1
    }
  ]
}
```

**404** – Team not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid targets

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X PUT "https://trainmaxr.pboj.smallhost.pl/api/training_content_tally/plan-targets" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"teamOrgId\": \"00000000-0000-4000-8000-000000000000\",
  \"roundKey\": \"string\",
  \"targets\": [
    {
      \"categoryId\": \"00000000-0000-4000-8000-000000000000\",
      \"targetPct\": 1
    }
  ]
}"
```

## GET `/training_content_tally/priorities`

List a team's live priority set or a template's item set

Requires features: training_content_tally.content.view

**Tags:** Training content

**Requires authentication.**

**Features:** training_content_tally.content.view

### Responses

**200** – Priorities

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "teamOrgId": null,
      "templateId": null,
      "scope": "goal",
      "categoryId": null,
      "elementId": null,
      "priority": 1
    }
  ]
}
```

**400** – Invalid query

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/training_content_tally/priorities" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## PUT `/training_content_tally/priorities`

Replace a team's or template's whole priority set

Requires features: training_content_tally.priority.manage

**Tags:** Training content

**Requires authentication.**

**Features:** training_content_tally.priority.manage

### Request Body

Content-Type: `application/json`

```json
{
  "entries": [
    {
      "scope": "goal",
      "priority": 1
    }
  ]
}
```

### Responses

**200** – The stored set

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "teamOrgId": null,
      "templateId": null,
      "scope": "goal",
      "categoryId": null,
      "elementId": null,
      "priority": 1
    }
  ]
}
```

**404** – Team or template not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid entries

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X PUT "https://trainmaxr.pboj.smallhost.pl/api/training_content_tally/priorities" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"entries\": [
    {
      \"scope\": \"goal\",
      \"priority\": 1
    }
  ]
}"
```

## POST `/training_content_tally/priorities/copy`

Clone the source set into each target team's live priorities

Requires features: training_content_tally.priority.manage

**Tags:** Training content

**Requires authentication.**

**Features:** training_content_tally.priority.manage

### Request Body

Content-Type: `application/json`

```json
{
  "toTeamOrgIds": [
    "00000000-0000-4000-8000-000000000000"
  ]
}
```

### Responses

**200** – Copied

Content-Type: `application/json`

```json
{
  "ok": true
}
```

**404** – Source or target not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Empty source set

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/training_content_tally/priorities/copy" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"toTeamOrgIds\": [
    \"00000000-0000-4000-8000-000000000000\"
  ]
}"
```

## DELETE `/training_content_tally/priority-templates`

Remove a template with its items

Requires features: training_content_tally.priority.manage

**Tags:** Training content

**Requires authentication.**

**Features:** training_content_tally.priority.manage

### Request Body

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

### Responses

**200** – Removed

Content-Type: `application/json`

```json
{
  "ok": true
}
```

**404** – Template not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X DELETE "https://trainmaxr.pboj.smallhost.pl/api/training_content_tally/priority-templates" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/training_content_tally/priority-templates`

List the club templates

Requires features: training_content_tally.content.view

**Tags:** Training content

**Requires authentication.**

**Features:** training_content_tally.content.view

### Responses

**200** – Templates

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "name": "string",
      "isActive": true
    }
  ]
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/training_content_tally/priority-templates" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/training_content_tally/priority-templates`

Create a template header

Requires features: training_content_tally.priority.manage

**Tags:** Training content

**Requires authentication.**

**Features:** training_content_tally.priority.manage

### Request Body

Content-Type: `application/json`

```json
{
  "name": "string"
}
```

### Responses

**201** – Created template id

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/training_content_tally/priority-templates" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"name\": \"string\"
}"
```

## PUT `/training_content_tally/priority-templates`

Rename or retire a template

Requires features: training_content_tally.priority.manage

**Tags:** Training content

**Requires authentication.**

**Features:** training_content_tally.priority.manage

### Request Body

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

### Responses

**200** – Updated template id

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

**404** – Template not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X PUT "https://trainmaxr.pboj.smallhost.pl/api/training_content_tally/priority-templates" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/training_content_tally/required-split`

A team's required split from (behavior count x priority), admin-modulated

Requires features: training_content_tally.content.view

**Tags:** Training content

**Requires authentication.**

**Features:** training_content_tally.content.view

### Responses

**200** – Required split

Content-Type: `application/json`

```json
{
  "teamOrgId": "00000000-0000-4000-8000-000000000000",
  "items": [
    {
      "categoryId": "string",
      "weight": 1,
      "pct": 1
    }
  ]
}
```

**400** – Invalid query

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**404** – Team not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/training_content_tally/required-split" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/training_content_tally/round-config`

Read the effective round config (defaults applied when never set)

Requires features: training_content_tally.content.view

**Tags:** Training content

**Requires authentication.**

**Features:** training_content_tally.content.view

### Responses

**200** – Effective config

Content-Type: `application/json`

```json
{
  "seasonStartMonth": 1,
  "boundaryMonth": 1,
  "boundaryDay": 1,
  "driftWindowWeeks": 1,
  "driftThresholdPct": 1,
  "configured": true
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/training_content_tally/round-config" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## PUT `/training_content_tally/round-config`

Set the season start, boundary (default Dec/Jan), and drift defaults - one live row per club

Requires features: training_content_tally.priority.manage

**Tags:** Training content

**Requires authentication.**

**Features:** training_content_tally.priority.manage

### Request Body

Content-Type: `application/json`

```json
{
  "seasonStartMonth": 1,
  "boundaryMonth": 1,
  "boundaryDay": 1
}
```

### Responses

**200** – Config row id

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000"
}
```

**422** – Invalid input

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X PUT "https://trainmaxr.pboj.smallhost.pl/api/training_content_tally/round-config" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"seasonStartMonth\": 1,
  \"boundaryMonth\": 1,
  \"boundaryDay\": 1
}"
```

## GET `/training_content_tally/season-summary`

A team's season summary across its two half-season rounds

Requires features: training_content_tally.content.view

**Tags:** Training content

**Requires authentication.**

**Features:** training_content_tally.content.view

### Responses

**200** – Season summary

Content-Type: `application/json`

```json
{
  "season": 1,
  "rounds": [
    {
      "roundKey": "string",
      "target": null,
      "actual": [
        {
          "categoryId": "string",
          "count": 1,
          "pct": 1
        }
      ]
    }
  ],
  "targetSum": [
    {
      "categoryId": "string",
      "totalTargetPct": 1
    }
  ],
  "actualTotal": [
    {
      "categoryId": "string",
      "count": 1,
      "pct": 1
    }
  ]
}
```

**400** – Invalid query

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**404** – Team not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/training_content_tally/season-summary" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/training_content_tally/sessions/{id}/content`

Read the session's current goal set and tag set

Requires features: training_content_tally.content.view

**Tags:** Training content

**Requires authentication.**

**Features:** training_content_tally.content.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| id | path | any | Required |

### Responses

**200** – Session content

Content-Type: `application/json`

```json
{
  "sessionId": "00000000-0000-4000-8000-000000000000",
  "teamOrgId": "00000000-0000-4000-8000-000000000000",
  "eventType": "string",
  "status": "string",
  "goals": [
    "00000000-0000-4000-8000-000000000000"
  ],
  "elements": [
    {
      "elementId": "00000000-0000-4000-8000-000000000000",
      "note": null
    }
  ]
}
```

**404** – Session not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/training_content_tally/sessions/:id/content" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/training_content_tally/sessions/{id}/content`

Replace the goal set (max 2) and behavior tags in one transaction

Rejected on a match or a cancelled session; behaviors must be active catalogue entries grouped under a declared goal, at most one tag per element.

Requires features: training_content_tally.content.manage

**Tags:** Training content

**Requires authentication.**

**Features:** training_content_tally.content.manage

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| id | path | any | Required |

### Request Body

Content-Type: `application/json`

```json
{
  "goals": [
    "00000000-0000-4000-8000-000000000000"
  ],
  "elements": [
    {
      "elementId": "00000000-0000-4000-8000-000000000000"
    }
  ]
}
```

### Responses

**200** – The stored content

Content-Type: `application/json`

```json
{
  "sessionId": "00000000-0000-4000-8000-000000000000",
  "teamOrgId": "00000000-0000-4000-8000-000000000000",
  "eventType": "string",
  "status": "string",
  "goals": [
    "00000000-0000-4000-8000-000000000000"
  ],
  "elements": [
    {
      "elementId": "00000000-0000-4000-8000-000000000000",
      "note": null
    }
  ]
}
```

**404** – Session not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**409** – Cancelled session

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid tagging

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X POST "https://trainmaxr.pboj.smallhost.pl/api/training_content_tally/sessions/:id/content" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"goals\": [
    \"00000000-0000-4000-8000-000000000000\"
  ],
  \"elements\": [
    {
      \"elementId\": \"00000000-0000-4000-8000-000000000000\"
    }
  ]
}"
```

## GET `/training_content_tally/stats`

A team's content statistics over the window (default: the recent six weeks)

Requires features: training_content_tally.content.view

**Tags:** Training content

**Requires authentication.**

**Features:** training_content_tally.content.view

### Responses

**200** – Content statistics

Content-Type: `application/json`

```json
{
  "teamOrgId": "00000000-0000-4000-8000-000000000000",
  "window": {
    "from": "string",
    "to": "string"
  },
  "sessions": [
    {
      "sessionId": "00000000-0000-4000-8000-000000000000",
      "startsAt": "string",
      "split": [
        {
          "categoryId": "string",
          "count": 1,
          "pct": 1
        }
      ]
    }
  ],
  "cumulative": {
    "totalSessions": 1,
    "categories": [
      {
        "categoryId": "string",
        "tagCount": 1,
        "sessionCount": 1,
        "pct": 1
      }
    ]
  }
}
```

**400** – Invalid query

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**404** – Team not found

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/training_content_tally/stats" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/user_profile/me`

Read the caller's own optional profile

Always resolves to the session subject; every field is optional and an all-null profile is the normal state.

**Tags:** User profile

**Requires authentication.**

### Responses

**200** – The profile

Content-Type: `application/json`

```json
{
  "photoUrl": null,
  "coachingLicenseNumber": null,
  "coachingLicenseExpiresOn": null,
  "phone": null,
  "bio": null
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/user_profile/me" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## PUT `/user_profile/me`

Update the caller's own optional profile

Partial write: absent fields stay untouched, null (or an empty string) clears. No field is required and nothing anywhere depends on any of them.

**Tags:** User profile

**Requires authentication.**

### Request Body

Content-Type: `application/json`

```json
{
  "photoUrl": null,
  "coachingLicenseNumber": null,
  "coachingLicenseExpiresOn": null,
  "phone": null,
  "bio": null
}
```

### Responses

**200** – The updated profile

Content-Type: `application/json`

```json
{
  "photoUrl": null,
  "coachingLicenseNumber": null,
  "coachingLicenseExpiresOn": null,
  "phone": null,
  "bio": null
}
```

**422** – Validation error

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X PUT "https://trainmaxr.pboj.smallhost.pl/api/user_profile/me" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"photoUrl\": null,
  \"coachingLicenseNumber\": null,
  \"coachingLicenseExpiresOn\": null,
  \"phone\": null,
  \"bio\": null
}"
```

## GET `/user_profile/staff-compliance`

List in-scope coaches with their license {number, expiry} and derived status

Derived per request against the inclusive 183-day (~6-month) window — nothing stored, no push. Scope: club admin = all coaches, coordinator = coaches on overseen teams.

Requires features: user_profile.staff_compliance.view

**Tags:** User profile

**Requires authentication.**

**Features:** user_profile.staff_compliance.view

### Responses

**200** – Compliance items

Content-Type: `application/json`

```json
{
  "items": [
    {
      "userId": "00000000-0000-4000-8000-000000000000",
      "name": null,
      "email": null,
      "teamOrgIds": [
        "00000000-0000-4000-8000-000000000000"
      ],
      "coachingLicenseNumber": null,
      "coachingLicenseExpiresOn": null,
      "status": "ok"
    }
  ]
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/user_profile/staff-compliance" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/version`

Deployed Open Mercato version

**Tags:** API Documentation

### Responses

**200** – Success response

Content-Type: `application/json`

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/version" \
  -H "Accept: application/json"
```

## GET `/workload_and_fatigue_monitoring/fatigue/cross-team`

Every red/orange-flagged player across the caller visible teams (a coordinator overseen set; the whole club for an admin), worst first

Requires features: workload_and_fatigue_monitoring.fatigue.cross_team_view

**Tags:** Workload and fatigue monitoring

**Requires authentication.**

**Features:** workload_and_fatigue_monitoring.fatigue.cross_team_view

### Responses

**200** – Red/orange players

Content-Type: `application/json`

```json
{
  "players": [
    {
      "playerId": "00000000-0000-4000-8000-000000000000",
      "teamOrgId": "00000000-0000-4000-8000-000000000000",
      "value": 1,
      "band": "orange"
    }
  ]
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/workload_and_fatigue_monitoring/fatigue/cross-team" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/workload_and_fatigue_monitoring/fatigue/current`

Each roster player's time-decayed fatigue value and band, with the forecast onto the next scheduled events up to the next match

Requires features: workload_and_fatigue_monitoring.fatigue.view

**Tags:** Workload and fatigue monitoring

**Requires authentication.**

**Features:** workload_and_fatigue_monitoring.fatigue.view

### Responses

**200** – Current fatigue + forecast

Content-Type: `application/json`

```json
{
  "players": [
    {
      "playerId": "00000000-0000-4000-8000-000000000000",
      "value": 1,
      "band": "green",
      "forecast": [
        {
          "sessionId": "00000000-0000-4000-8000-000000000000",
          "startsAt": "string",
          "eventType": "training",
          "value": 1,
          "band": "green"
        }
      ]
    }
  ]
}
```

**404** – Team not visible in the caller scope

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid query

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/workload_and_fatigue_monitoring/fatigue/current" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/workload_and_fatigue_monitoring/fatigue/live`

Each roster player's trailing-7-day fatigue percent and band, plus the team age-group average

Requires features: workload_and_fatigue_monitoring.fatigue.view

**Tags:** Workload and fatigue monitoring

**Requires authentication.**

**Features:** workload_and_fatigue_monitoring.fatigue.view

### Responses

**200** – Live fatigue readings

Content-Type: `application/json`

```json
{
  "players": [
    {
      "playerId": "00000000-0000-4000-8000-000000000000",
      "value": 1,
      "band": "green"
    }
  ],
  "teamAverage": 1
}
```

**404** – Team not visible in the caller scope

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid query

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/workload_and_fatigue_monitoring/fatigue/live" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/workload_and_fatigue_monitoring/fatigue/min-minutes`

Players below the team's minimum share of available match minutes in the period

Requires features: workload_and_fatigue_monitoring.fatigue.view

**Tags:** Workload and fatigue monitoring

**Requires authentication.**

**Features:** workload_and_fatigue_monitoring.fatigue.view

### Responses

**200** – Min-minutes report

Content-Type: `application/json`

```json
{
  "periodFrom": "string",
  "periodTo": "string",
  "minMinutesPct": 1,
  "availableMinutes": 1,
  "flagged": [
    {
      "playerId": "00000000-0000-4000-8000-000000000000",
      "playedMinutes": 1,
      "availableMinutes": 1,
      "sharePct": 1
    }
  ]
}
```

**404** – Team not visible in the caller scope

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid query or period

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/workload_and_fatigue_monitoring/fatigue/min-minutes" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/workload_and_fatigue_monitoring/fatigue/workload`

A team's microcycle load, per-player individual loads, and the light/moderate/heavy classification

Requires features: workload_and_fatigue_monitoring.fatigue.view

**Tags:** Workload and fatigue monitoring

**Requires authentication.**

**Features:** workload_and_fatigue_monitoring.fatigue.view

### Responses

**200** – Microcycle workload

Content-Type: `application/json`

```json
{
  "window": {
    "weekStart": "string",
    "weekEnd": "string",
    "isoWeek": "string"
  },
  "teamLoadAu": 1,
  "classification": "light",
  "sessions": [
    {
      "sessionId": "00000000-0000-4000-8000-000000000000",
      "eventType": "training",
      "startsAt": "string",
      "durationMinutes": null,
      "missingDuration": true,
      "submitterCount": 1,
      "meanRpe": null,
      "teamLoadAu": 1
    }
  ],
  "players": [
    {
      "playerId": "00000000-0000-4000-8000-000000000000",
      "loadAu": 1,
      "sessions": [
        {
          "sessionId": "00000000-0000-4000-8000-000000000000",
          "srpeAu": 1
        }
      ]
    }
  ]
}
```

**404** – Team not visible in the caller scope

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid query

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/workload_and_fatigue_monitoring/fatigue/workload" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/workload_and_fatigue_monitoring/team-workload-config`

Read a team's effective workload configuration (code defaults when no row exists)

Requires features: workload_and_fatigue_monitoring.fatigue.view

**Tags:** Workload and fatigue monitoring

**Requires authentication.**

**Features:** workload_and_fatigue_monitoring.fatigue.view

### Responses

**200** – Effective configuration

Content-Type: `application/json`

```json
{
  "teamOrgId": "00000000-0000-4000-8000-000000000000",
  "heavyThresholdAu": 1,
  "moderatePct": 1,
  "minMinutesPct": 1,
  "rpeCompletionPct": 1,
  "ewmaMatchWeight": 1,
  "ewmaTrainingWeight": 1,
  "ewmaRecoveryHalfLifeHours": 1
}
```

**404** – Team not visible in the caller scope

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid query

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X GET "https://trainmaxr.pboj.smallhost.pl/api/workload_and_fatigue_monitoring/team-workload-config" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## PUT `/workload_and_fatigue_monitoring/team-workload-config`

Create or edit a team's thresholds and EWMA parameters (coordinator/admin)

Requires features: workload_and_fatigue_monitoring.fatigue.configure

**Tags:** Workload and fatigue monitoring

**Requires authentication.**

**Features:** workload_and_fatigue_monitoring.fatigue.configure

### Responses

**200** – Saved configuration row

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "teamOrgId": "00000000-0000-4000-8000-000000000000"
}
```

**404** – Team not visible in the caller scope

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

**422** – Invalid threshold or EWMA values

Content-Type: `application/json`

```json
{
  "error": "string"
}
```

### Example

```bash
curl -X PUT "https://trainmaxr.pboj.smallhost.pl/api/workload_and_fatigue_monitoring/team-workload-config" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```