# Open Mercato API

Version: 0.6.7

Auto-generated OpenAPI definition for all enabled modules.

## Servers
- https://crm.nf-hotel.pl – Default environment

## GET `/ai_assistant/ai/actions/{id}`

Fetch the current state of an AI pending action by id.

Returns the tenant-scoped {@link AiPendingAction} addressed by `:id`. Powers the chat UI reconnect/polling path: after a page reload or SSE reconnect the client carries the `pendingActionId` from an earlier `mutation-preview-card` UI part and calls this route to re-hydrate the card. Server-internal fields (`normalizedInput`, `createdByUserId`, `idempotencyKey`) are stripped by a whitelist serializer. Enforces tenant/org scoping via the repository.

Requires features: ai_assistant.view

**Tags:** AI Assistant

**Requires authentication.**

**Features:** ai_assistant.view

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

### Responses

**200** – Serialized pending action. Never includes normalizedInput, createdByUserId, or idempotencyKey.

Content-Type: `application/json`

**404** – No pending action with the given id is accessible to the caller.

Content-Type: `application/json`

**500** – Internal runtime failure.

Content-Type: `application/json`

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/ai_assistant/ai/actions/:id" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/ai_assistant/ai/actions/{id}/cancel`

Cancel an AI pending action without executing the wrapped tool.

Flips a pending AI action from `pending` to `cancelled` and emits the `ai.action.cancelled` event. The tool handler is never invoked. Idempotent: a second call on a row already in `cancelled` status returns 200 with the current row without re-emitting the event. Rows whose `expiresAt` is in the past are flipped to `expired` and returned as 409 `expired` so the client can surface the TTL loss instead of silently masking it as a cancellation.

Requires features: ai_assistant.view

**Tags:** AI Assistant

**Requires authentication.**

**Features:** ai_assistant.view

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

### Responses

**200** – Cancellation complete (or idempotent replay); body includes the serialized pending action with status `cancelled`.

Content-Type: `application/json`

**400** – Invalid cancel request body (unknown field, reason exceeds 500 chars, wrong type).

Content-Type: `application/json`

**404** – Pending action not found in the caller scope.

Content-Type: `application/json`

**409** – Pending action is not in `pending` status (already confirmed/failed/executing) or has expired.

Content-Type: `application/json`

**500** – Unexpected server failure during cancel.

Content-Type: `application/json`

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/ai_assistant/ai/actions/:id/cancel" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/ai_assistant/ai/actions/{id}/confirm`

Confirm an AI pending action, re-running every server-side check before execution.

Re-verifies the full contract from spec §9.4 (status, expiry, agent registration, required features, mutation policy, tool whitelist, attachment tenant scope, record version, and schema drift), flips the pending-action state machine to `executing`, invokes the wrapped tool handler, and persists `executionResult`. Idempotent: a second call on a row already in `confirmed` state returns the prior result without re-executing the handler.

Requires features: ai_assistant.view

**Tags:** AI Assistant

**Requires authentication.**

**Features:** ai_assistant.view

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

### Responses

**200** – Confirmation complete; body includes the serialized pending action and the mutation result.

Content-Type: `application/json`

**404** – Pending action or agent not found in the caller scope.

Content-Type: `application/json`

**409** – Pending action is not in `pending` status or has expired.

Content-Type: `application/json`

**412** – Record version changed between propose and confirm, or the input schema no longer accepts the stored payload.

Content-Type: `application/json`

**500** – Unexpected server failure during confirm.

Content-Type: `application/json`

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/ai_assistant/ai/actions/:id/confirm" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/ai_assistant/ai/agents`

List registered AI agents, filtered by the caller's features.

Returns `{ agents: [...] }` — the subset of agents from `ai-agents.generated.ts` that the authenticated caller can invoke based on each agent's `requiredFeatures`. Mirrors the `meta.list_agents` tool handler so backoffice pages (e.g. the playground) can render an agent picker without going through the MCP tool transport.

Requires features: ai_assistant.view

**Tags:** AI Assistant

**Requires authentication.**

**Features:** ai_assistant.view

### Responses

**200** – Accessible agent summaries.

Content-Type: `application/json`

**500** – Internal failure while loading the agent registry.

Content-Type: `application/json`

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/ai_assistant/ai/agents" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## DELETE `/ai_assistant/ai/agents/{agentId}/loop-override`

Remove the loop-policy columns from the agent-scoped runtime override row.

Nulls out all seven loop columns on the agent-scoped `ai_agent_runtime_overrides` row. Idempotent — returns 200 even when no override exists. Requires `ai_assistant.settings.manage`.

Requires features: ai_assistant.settings.manage

**Tags:** AI Assistant

**Requires authentication.**

**Features:** ai_assistant.settings.manage

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

### Responses

**200** – Loop override cleared (or already absent).

Content-Type: `application/json`

**400** – Invalid agent id.

Content-Type: `application/json`

**404** – Unknown agent id.

Content-Type: `application/json`

### Example

```bash
curl -X DELETE "https://crm.nf-hotel.pl/ai_assistant/ai/agents/:agentId/loop-override" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/ai_assistant/ai/agents/{agentId}/loop-override`

Read the current loop-policy override for this agent, if any.

Returns `{ agentId, override }` where `override` is the agent-scoped loop-policy row from `ai_agent_runtime_overrides` (or `null`). Requires `ai_assistant.view`.

Requires features: ai_assistant.view

**Tags:** AI Assistant

**Requires authentication.**

**Features:** ai_assistant.view

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

### Responses

**200** – Loop override payload.

Content-Type: `application/json`

**400** – Invalid agent id.

Content-Type: `application/json`

**404** – Unknown agent id.

Content-Type: `application/json`

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/ai_assistant/ai/agents/:agentId/loop-override" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## PUT `/ai_assistant/ai/agents/{agentId}/loop-override`

Set (or replace) the tenant-scoped loop-policy override for this agent.

Body: loop columns. All fields are nullable/optional; `null` explicitly clears that axis. Validates `loopStopWhenJson` items and `loopActiveToolsJson` membership. Requires `ai_assistant.settings.manage`.

Requires features: ai_assistant.settings.manage

**Tags:** AI Assistant

**Requires authentication.**

**Features:** ai_assistant.settings.manage

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

### Request Body

Content-Type: `application/json`

```json
{
  "loopDisabled": null,
  "loopMaxSteps": null,
  "loopMaxToolCalls": null,
  "loopMaxWallClockMs": null,
  "loopMaxTokens": null,
  "loopStopWhenJson": null,
  "loopActiveToolsJson": null
}
```

### Responses

**200** – Override persisted.

Content-Type: `application/json`

**400** – Invalid agent id or validation error.

Content-Type: `application/json`

**404** – Unknown agent id.

Content-Type: `application/json`

### Example

```bash
curl -X PUT "https://crm.nf-hotel.pl/ai_assistant/ai/agents/:agentId/loop-override" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"loopDisabled\": null,
  \"loopMaxSteps\": null,
  \"loopMaxToolCalls\": null,
  \"loopMaxWallClockMs\": null,
  \"loopMaxTokens\": null,
  \"loopStopWhenJson\": null,
  \"loopActiveToolsJson\": null
}"
```

## GET `/ai_assistant/ai/agents/{agentId}/models`

Get the providers and curated models available for the chat-UI picker for this agent

Returns all configured providers with their curated model catalogs, filtered to providers that have an API key configured in the current environment. When the agent declares `allowRuntimeOverride: false`, the response reflects that constraint so the UI picker can hide itself. Includes the agent's resolved default provider/model so the picker can render a "(default)" badge next to the right entry. RBAC: requires the same features as the agent itself (typically `ai_assistant.view`).

Requires features: ai_assistant.view

**Tags:** AI Assistant

**Requires authentication.**

**Features:** ai_assistant.view

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

### Responses

**200** – Providers and curated models available for the agent picker. Empty `providers` array when `allowRuntimeOverride` is false.

Content-Type: `application/json`

**404** – Unknown agent id.

Content-Type: `application/json`

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/ai_assistant/ai/agents/:agentId/models" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## DELETE `/ai_assistant/ai/agents/{agentId}/mutation-policy`

Remove the tenant-scoped mutationPolicy override for this agent.

Deletes the override row if it exists; subsequent calls fall back to the agent's code-declared policy. Idempotent — returns 200 even when no override exists. Requires `ai_assistant.settings.manage`.

Requires features: ai_assistant.settings.manage

**Tags:** AI Assistant

**Requires authentication.**

**Features:** ai_assistant.settings.manage

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

### Responses

**200** – Override cleared (or already absent).

Content-Type: `application/json`

**400** – Invalid agent id.

Content-Type: `application/json`

**404** – Unknown agent id.

Content-Type: `application/json`

### Example

```bash
curl -X DELETE "https://crm.nf-hotel.pl/ai_assistant/ai/agents/:agentId/mutation-policy" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/ai_assistant/ai/agents/{agentId}/mutation-policy`

Read the effective mutationPolicy for an agent — code-declared value plus any tenant override.

Returns `{ agentId, codeDeclared, override }` where `codeDeclared` is the agent's compiled-in `mutationPolicy` and `override` is the persisted tenant-scoped override (or `null`). Requires `ai_assistant.view`.

Requires features: ai_assistant.view

**Tags:** AI Assistant

**Requires authentication.**

**Features:** ai_assistant.view

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

### Responses

**200** – Effective mutationPolicy payload.

Content-Type: `application/json`

**400** – Invalid agent id.

Content-Type: `application/json`

**404** – Unknown agent id.

Content-Type: `application/json`

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/ai_assistant/ai/agents/:agentId/mutation-policy" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/ai_assistant/ai/agents/{agentId}/mutation-policy`

Set (or replace) the tenant-scoped mutationPolicy override for this agent.

Body: `{ mutationPolicy: "read-only" | "confirm-required" | "destructive-confirm-required", notes? }`. The override MUST NOT escalate beyond the agent's code-declared policy. Escalation attempts are rejected with 400 + `code: "escalation_not_allowed"`. Requires `ai_assistant.settings.manage`.

Requires features: ai_assistant.settings.manage

**Tags:** AI Assistant

**Requires authentication.**

**Features:** ai_assistant.settings.manage

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

### Request Body

Content-Type: `application/json`

```json
{
  "mutationPolicy": "read-only"
}
```

### Responses

**200** – Override persisted.

Content-Type: `application/json`

**400** – Invalid agent id, malformed body, or escalation attempt.

Content-Type: `application/json`

**404** – Unknown agent id.

Content-Type: `application/json`

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/ai_assistant/ai/agents/:agentId/mutation-policy" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"mutationPolicy\": \"read-only\"
}"
```

## GET `/ai_assistant/ai/agents/{agentId}/prompt-override`

Read the latest prompt-section override for an agent plus recent version history.

Returns `{ agentId, override, versions }` where `override` is the latest persisted row (or `null`) and `versions` is the newest-first history capped at 10 rows. Tenant-scoped; requires `ai_assistant.settings.manage`.

**Tags:** AI Assistant

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

### Responses

**200** – Latest override + recent version history.

Content-Type: `application/json`

**400** – Invalid agent id.

Content-Type: `application/json`

**401** – Unauthenticated caller.

Content-Type: `application/json`

**403** – Caller lacks `ai_assistant.settings.manage`.

Content-Type: `application/json`

**404** – Unknown agent id.

Content-Type: `application/json`

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/ai_assistant/ai/agents/:agentId/prompt-override" \
  -H "Accept: application/json"
```

## POST `/ai_assistant/ai/agents/{agentId}/prompt-override`

Save a new prompt-section override version for the agent.

Persists an additive `{ sections: Record<sectionId, text>, notes? }` override, allocating the next monotonic version for `(tenant, org, agent)`. Reserved policy keys (`mutationPolicy`, `readOnly`, `allowedTools`, `acceptedMediaTypes`) are rejected with 400 / `reserved_key`. Requires `ai_assistant.settings.manage`.

**Tags:** AI Assistant

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

### Request Body

Content-Type: `application/json`

```json
{}
```

### Responses

**200** – Override persisted. Returns `{ ok: true, version, updatedAt }`.

Content-Type: `application/json`

**400** – Invalid agent id, malformed body, or reserved policy key.

Content-Type: `application/json`

**401** – Unauthenticated caller.

Content-Type: `application/json`

**403** – Caller lacks `ai_assistant.settings.manage`.

Content-Type: `application/json`

**404** – Unknown agent id.

Content-Type: `application/json`

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/ai_assistant/ai/agents/:agentId/prompt-override" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d "{}"
```

## POST `/ai_assistant/ai/chat`

Stream a chat turn for a registered AI agent

Dispatches a chat turn to the focused AI agent identified by `?agent=<module>.<agent>`. Enforces agent-level `requiredFeatures`, tool whitelisting, read-only / mutationPolicy, execution-mode compatibility, and attachment media-type policy. The streaming response body uses an AI SDK-compatible `text/event-stream` transport. Optional `?provider=`, `?model=`, and `?baseUrl=` query params let callers override the resolved provider/model/base-URL for this turn (Phase 4a). Provider must be registered and configured; baseUrl must match `AI_RUNTIME_BASEURL_ALLOWLIST` when set. Both are suppressed when the agent declares `allowRuntimeOverride: false`.

Requires features: ai_assistant.view

**Tags:** AI Assistant

**Requires authentication.**

**Features:** ai_assistant.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| agent | query | any | Required |
| provider | query | any | Optional |
| model | query | any | Optional |
| baseUrl | query | any | Optional |
| loopBudget | query | any | Optional |

### Request Body

Content-Type: `application/json`

```json
{
  "messages": [
    {
      "role": "user",
      "content": "string"
    }
  ]
}
```

### Responses

**200** – Streaming text/event-stream response compatible with AI SDK chat transports.

Content-Type: `text/event-stream`

**400** – Invalid query param, malformed payload, or message count above the cap. Typed codes: `runtime_override_disabled` (agent has allowRuntimeOverride:false), `provider_unknown` (provider id not registered), `provider_not_configured` (provider registered but no API key in env), `baseurl_not_allowlisted` (baseUrl not in AI_RUNTIME_BASEURL_ALLOWLIST).

Content-Type: `application/json`

**404** – Unknown agent id.

Content-Type: `application/json`

**409** – Agent/tool/execution-mode policy violation.

Content-Type: `application/json`

**500** – Internal runtime failure.

Content-Type: `application/json`

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/ai_assistant/ai/chat?agent=string" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"messages\": [
    {
      \"role\": \"user\",
      \"content\": \"string\"
    }
  ]
}"
```

## GET `/ai_assistant/ai/conversations`

List AI chat conversations visible to the caller.

Returns `{ items, nextCursor }` for the authenticated caller, ordered by `lastMessageAt` descending. View-only callers receive only their own conversations. Callers with `ai_assistant.conversations.manage` may list conversations across users in the same tenant/organization. The `agent` and `status` filters are optional; `cursor` is the ISO timestamp returned by a previous response.

Requires features: ai_assistant.view

**Tags:** AI Assistant

**Requires authentication.**

**Features:** ai_assistant.view

### Responses

**200** – Caller-owned conversation summaries.

Content-Type: `application/json`

**400** – Invalid query parameters.

Content-Type: `application/json`

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/ai_assistant/ai/conversations" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/ai_assistant/ai/conversations`

Idempotently create a new AI chat conversation.

If a non-deleted conversation already exists with the supplied `conversationId` for the authenticated caller in this tenant/org, returns the existing summary. Otherwise creates a fresh row and writes the owner-participant row in the same transaction.

Requires features: ai_assistant.view

**Tags:** AI Assistant

**Requires authentication.**

**Features:** ai_assistant.view

### Responses

**200** – Existing conversation (idempotent path).

Content-Type: `application/json`

**201** – Newly created conversation.

Content-Type: `application/json`

**400** – Invalid request body.

Content-Type: `application/json`

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/ai_assistant/ai/conversations" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## DELETE `/ai_assistant/ai/conversations/{conversationId}`

Soft-delete a conversation and its messages.

View-only callers can delete only their own conversations. Callers with `ai_assistant.conversations.manage` can delete conversations in the same tenant/organization. Marks the conversation row and every undeleted message row with a `deleted_at` timestamp in one transaction. The transcript remains in the database for audit/restore until a future retention worker hard-deletes it.

Requires features: ai_assistant.view

**Tags:** AI Assistant

**Requires authentication.**

**Features:** ai_assistant.view

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

### Responses

**200** – Soft-delete acknowledgment.

Content-Type: `application/json`

**404** – No conversation accessible to the caller.

Content-Type: `application/json`

### Example

```bash
curl -X DELETE "https://crm.nf-hotel.pl/ai_assistant/ai/conversations/:conversationId" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/ai_assistant/ai/conversations/{conversationId}`

Fetch a conversation summary and recent transcript.

Returns `{ conversation, messages, nextCursor }` for the supplied `conversationId`. View-only callers can load only their own conversations. Callers with `ai_assistant.conversations.manage` can load conversations across users in the same tenant/organization. Messages are ordered ascending by `createdAt`. The `before` cursor returns the next older page when paging back through long transcripts.

Requires features: ai_assistant.view

**Tags:** AI Assistant

**Requires authentication.**

**Features:** ai_assistant.view

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

### Responses

**200** – Conversation transcript page for the authenticated owner.

Content-Type: `application/json`

**400** – Invalid path or query parameters.

Content-Type: `application/json`

**404** – No conversation accessible to the caller.

Content-Type: `application/json`

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/ai_assistant/ai/conversations/:conversationId" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## PATCH `/ai_assistant/ai/conversations/{conversationId}`

Update an existing conversation.

Accepts a partial body containing any of `title`, `status`, `pageContext`. Setting `status` to `closed` archives the conversation while keeping its transcript intact. View-only callers can update only their own conversations; conversation managers can update conversations in the same tenant/organization.

Requires features: ai_assistant.view

**Tags:** AI Assistant

**Requires authentication.**

**Features:** ai_assistant.view

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

### Responses

**200** – Updated conversation summary.

Content-Type: `application/json`

**400** – Invalid request body.

Content-Type: `application/json`

**404** – No conversation accessible to the caller.

Content-Type: `application/json`

### Example

```bash
curl -X PATCH "https://crm.nf-hotel.pl/ai_assistant/ai/conversations/:conversationId" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/ai_assistant/ai/conversations/{conversationId}/participants`

List active participants of a conversation.

Returns the list of active (non-revoked) participants for the conversation. Only the conversation owner or a caller with `ai_assistant.conversations.manage` can call this endpoint.

Requires features: ai_assistant.view

**Tags:** AI Assistant

**Requires authentication.**

**Features:** ai_assistant.view

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

### Responses

**200** – List of active participants.

Content-Type: `application/json`

**404** – Conversation not found or not accessible.

Content-Type: `application/json`

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/ai_assistant/ai/conversations/:conversationId/participants" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/ai_assistant/ai/conversations/{conversationId}/participants`

Add a participant to a conversation.

Grants a named user read access to the conversation. Requires `ai_assistant.conversations.share`. Only the conversation owner may add participants. If the user was previously revoked, the soft-deleted row is restored.

Requires features: ai_assistant.view

**Tags:** AI Assistant

**Requires authentication.**

**Features:** ai_assistant.view

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

### Responses

**201** – Participant added; conversation visibility updated to "shared".

Content-Type: `application/json`

**400** – Invalid request body.

Content-Type: `application/json`

**404** – Conversation not found.

Content-Type: `application/json`

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/ai_assistant/ai/conversations/:conversationId/participants" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## DELETE `/ai_assistant/ai/conversations/{conversationId}/participants/{userId}`

Revoke a participant from a conversation (soft-delete).

Soft-deletes the participant row. If no active non-owner participants remain, the conversation visibility is reset to "private". Only the conversation owner or a manager may revoke participants.

Requires features: ai_assistant.view

**Tags:** AI Assistant

**Requires authentication.**

**Features:** ai_assistant.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| conversationId | path | any | Required |
| userId | path | any | Required |

### Responses

**204** – Participant revoked.

**400** – Invalid path parameters.

Content-Type: `application/json`

**404** – Conversation not found.

Content-Type: `application/json`

### Example

```bash
curl -X DELETE "https://crm.nf-hotel.pl/ai_assistant/ai/conversations/:conversationId/participants/:userId" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/ai_assistant/ai/conversations/import`

Import a conversation that previously lived only in browser localStorage.

Idempotent: messages with `clientMessageId` already present in the server transcript are skipped and counted in `skippedMessageCount`. New messages are appended with the original `clientMessageId` so subsequent retries continue to dedupe. Up to 100 messages per request. Attachment previews stored as `data:` URLs in the source localStorage record MUST NOT be forwarded to this endpoint; the UI strips them before upload.

Requires features: ai_assistant.view

**Tags:** AI Assistant

**Requires authentication.**

**Features:** ai_assistant.view

### Responses

**200** – Import result including imported/skipped counters.

Content-Type: `application/json`

**400** – Invalid request body.

Content-Type: `application/json`

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/ai_assistant/ai/conversations/import" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/ai_assistant/ai/run-object`

Run an object-mode AI agent and return the generated object

Invokes `runAiAgentObject` server-side for the registered AI agent identified by `agent` (matching "<module>.<agent>"). Enforces the same `requiredFeatures`, tool whitelisting, mutationPolicy, and attachment media-type policy as the chat dispatcher, but additionally requires the agent to declare `executionMode: "object"`. Returns the generated object in a single JSON response (no streaming).

Requires features: ai_assistant.view

**Tags:** AI Assistant

**Requires authentication.**

**Features:** ai_assistant.view

### Request Body

Content-Type: `application/json`

```json
{
  "agent": "string",
  "messages": [
    {
      "role": "user",
      "content": "string"
    }
  ]
}
```

### Responses

**200** – Object-mode run completed; response body contains `{ object, usage?, finishReason? }`.

Content-Type: `application/json`

**400** – Malformed payload or message cap exceeded.

Content-Type: `application/json`

**404** – Unknown agent id.

Content-Type: `application/json`

**409** – Agent/tool/execution-mode policy violation.

Content-Type: `application/json`

**422** – Agent does not support object-mode execution.

Content-Type: `application/json`

**500** – Internal runtime failure.

Content-Type: `application/json`

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/ai_assistant/ai/run-object" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"agent\": \"string\",
  \"messages\": [
    {
      \"role\": \"user\",
      \"content\": \"string\"
    }
  ]
}"
```

## POST `/ai_assistant/chat`

Send message to AI agent via SSE stream

Requires features: ai_assistant.view

**Tags:** AI Assistant

**Requires authentication.**

**Features:** ai_assistant.view

### Responses

**201** – Success response

Content-Type: `application/json`

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/ai_assistant/chat" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/ai_assistant/health`

Check OpenCode and MCP connection status

Requires features: ai_assistant.view

**Tags:** AI Assistant

**Requires authentication.**

**Features:** ai_assistant.view

### Responses

**200** – Success response

Content-Type: `application/json`

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/ai_assistant/health" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/ai_assistant/mcp-key`

Generate MCP API key

Generates a persistent `omk_` API key scoped to the calling user's tenant/organization and carrying the caller's own roles, so the key has the same ACL as the user.

Requires features: api_keys.create

**Tags:** AI Assistant

**Requires authentication.**

**Features:** api_keys.create

### Responses

**200** – API key created

Content-Type: `application/json`

```json
{
  "id": "string",
  "name": "string",
  "keyPrefix": "string",
  "secret": "string",
  "tenantId": null,
  "organizationId": null,
  "roles": [
    "string"
  ]
}
```

**500** – Failed to create MCP API key

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/ai_assistant/mcp-key" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/ai_assistant/route`

Route user query to appropriate AI handler

Requires features: ai_assistant.view

**Tags:** AI Assistant

**Requires authentication.**

**Features:** ai_assistant.view

### Responses

**201** – Success response

Content-Type: `application/json`

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/ai_assistant/route" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/ai_assistant/session-key`

Generate session key

Generates a new session token that can be included in MCP tool calls via the _sessionToken parameter. The token inherits the calling user's roles and organization context.

Requires features: ai_assistant.view

**Tags:** AI Assistant

**Requires authentication.**

**Features:** ai_assistant.view

### Responses

**200** – Session key created successfully

Content-Type: `application/json`

```json
{
  "sessionToken": "string",
  "expiresAt": "string"
}
```

**500** – Failed to create session key

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/ai_assistant/session-key" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## DELETE `/ai_assistant/settings`

Clear per-tenant AI runtime override

Soft-deletes the active per-tenant runtime override. Pass `agentId` to clear only the agent-specific row; omit to clear the tenant-wide default. Gated by `ai_assistant.settings.manage`. Idempotent — returns 200 with `cleared: false` when no active row existed.

Requires features: ai_assistant.settings.manage

**Tags:** AI Assistant

**Requires authentication.**

**Features:** ai_assistant.settings.manage

### Request Body

Content-Type: `application/json`

```json
{
  "agentId": null
}
```

### Responses

**200** – Returns `{ cleared: boolean }` indicating whether a row was found and removed.

Content-Type: `application/json`

### Example

```bash
curl -X DELETE "https://crm.nf-hotel.pl/ai_assistant/settings" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"agentId\": null
}"
```

## GET `/ai_assistant/settings`

Get AI provider configuration

Requires features: ai_assistant.view

**Tags:** AI Assistant

**Requires authentication.**

**Features:** ai_assistant.view

### Responses

**200** – Success response

Content-Type: `application/json`

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/ai_assistant/settings" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## PUT `/ai_assistant/settings`

Upsert per-tenant AI runtime override

Creates or updates the per-tenant AI runtime override (provider, model, baseURL). Optionally scoped to a specific agent via `agentId`. Gated by `ai_assistant.settings.manage`. baseURL must match AI_RUNTIME_BASEURL_ALLOWLIST when set.

Requires features: ai_assistant.settings.manage

**Tags:** AI Assistant

**Requires authentication.**

**Features:** ai_assistant.settings.manage

### Request Body

Content-Type: `application/json`

```json
{
  "providerId": null,
  "modelId": null,
  "baseURL": null,
  "agentId": null,
  "allowedOverrideProviders": null
}
```

### Responses

**200** – Override saved. Returns the saved row.

Content-Type: `application/json`

**400** – Validation error: unknown provider, invalid URL, or baseURL not allowlisted.

Content-Type: `application/json`

### Example

```bash
curl -X PUT "https://crm.nf-hotel.pl/ai_assistant/settings" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"providerId\": null,
  \"modelId\": null,
  \"baseURL\": null,
  \"agentId\": null,
  \"allowedOverrideProviders\": null
}"
```

## DELETE `/ai_assistant/settings/allowlist`

Clear per-tenant AI provider/model allowlist

Soft-deletes the tenant allowlist row. Tenant overrides revert to env-only enforcement. Idempotent — returns `{ cleared: false }` when no active row existed.

Requires features: ai_assistant.settings.manage

**Tags:** AI Assistant

**Requires authentication.**

**Features:** ai_assistant.settings.manage

### Responses

**200** – Returns `{ cleared: boolean }`.

Content-Type: `application/json`

### Example

```bash
curl -X DELETE "https://crm.nf-hotel.pl/ai_assistant/settings/allowlist" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## PUT `/ai_assistant/settings/allowlist`

Upsert per-tenant AI provider/model allowlist

Persists the per-tenant allowlist of providers and models. The runtime intersects this with the env allowlist (`OM_AI_AVAILABLE_*`) at resolution time. Tenant values that fall outside the env allowlist are rejected with `provider_not_in_env_allowlist` / `model_not_in_env_allowlist` 400 codes. Gated by `ai_assistant.settings.manage`.

Requires features: ai_assistant.settings.manage

**Tags:** AI Assistant

**Requires authentication.**

**Features:** ai_assistant.settings.manage

### Request Body

Content-Type: `application/json`

```json
{
  "allowedProviders": null
}
```

### Responses

**200** – Allowlist saved. Returns the saved snapshot.

Content-Type: `application/json`

**400** – Validation error or values outside env allowlist.

Content-Type: `application/json`

### Example

```bash
curl -X PUT "https://crm.nf-hotel.pl/ai_assistant/settings/allowlist" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"allowedProviders\": null
}"
```

## GET `/ai_assistant/tools`

List available MCP tools filtered by user permissions

Requires features: ai_assistant.view

**Tags:** AI Assistant

**Requires authentication.**

**Features:** ai_assistant.view

### Responses

**200** – Success response

Content-Type: `application/json`

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/ai_assistant/tools" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/ai_assistant/tools/execute`

Execute a specific MCP tool by name

Requires features: ai_assistant.view

**Tags:** AI Assistant

**Requires authentication.**

**Features:** ai_assistant.view

### Responses

**201** – Success response

Content-Type: `application/json`

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/ai_assistant/tools/execute" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/ai_assistant/usage/daily`

Fetch daily token-usage rollup rows for a date window.

Returns aggregated token-usage data from `ai_token_usage_daily` for the given date window. Tenant-scoped. Optionally filtered by `agentId` and/or `modelId`. Requires `ai_assistant.settings.manage`.

Requires features: ai_assistant.settings.manage

**Tags:** AI Assistant

**Requires authentication.**

**Features:** ai_assistant.settings.manage

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| from | query | any | Required |
| to | query | any | Required |
| agentId | query | any | Optional |
| modelId | query | any | Optional |

### Responses

**200** – Array of daily rollup rows.

Content-Type: `application/json`

**400** – Invalid query parameters.

Content-Type: `application/json`

**500** – Internal failure.

Content-Type: `application/json`

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/ai_assistant/usage/daily?from=string&to=string" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/ai_assistant/usage/sessions`

List per-session token usage totals for a date window.

Returns aggregated token-usage data grouped by `session_id` from `ai_token_usage_events` for the given date window. Tenant-scoped. Optionally filtered by `agentId`. Paginated via `limit` / `offset`. Requires `ai_assistant.settings.manage`.

Requires features: ai_assistant.settings.manage

**Tags:** AI Assistant

**Requires authentication.**

**Features:** ai_assistant.settings.manage

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| from | query | any | Required |
| to | query | any | Required |
| agentId | query | any | Optional |
| limit | query | any | Required |
| offset | query | any | Required |

### Responses

**200** – Array of session-level usage summaries plus pagination metadata.

Content-Type: `application/json`

**400** – Invalid query parameters.

Content-Type: `application/json`

**500** – Internal failure.

Content-Type: `application/json`

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/ai_assistant/usage/sessions?from=string&to=string" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/ai_assistant/usage/sessions/{sessionId}`

Fetch per-step token usage event rows for a single session.

Returns up to 200 raw `ai_token_usage_events` rows for the given `sessionId`, ordered by `created_at ASC, step_index ASC`. Tenant-scoped. Requires `ai_assistant.settings.manage`.

Requires features: ai_assistant.settings.manage

**Tags:** AI Assistant

**Requires authentication.**

**Features:** ai_assistant.settings.manage

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

### Responses

**200** – Array of per-step event rows for the session.

Content-Type: `application/json`

**400** – Invalid session id (must be a UUID).

Content-Type: `application/json`

**404** – No events found for the given session id in the caller's tenant.

Content-Type: `application/json`

**500** – Internal failure.

Content-Type: `application/json`

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/ai_assistant/usage/sessions/:sessionId" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## DELETE `/api_keys/keys`

Delete API key

Removes an API key by identifier. The key must belong to the current tenant and fall within the requester organization scope.

Requires features: api_keys.delete

**Tags:** API Keys

**Requires authentication.**

**Features:** api_keys.delete

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| id | query | any | Required. API key identifier to delete |

### Responses

**200** – Key deleted successfully

Content-Type: `application/json`

```json
{
  "success": true
}
```

**400** – Missing or invalid identifier

Content-Type: `application/json`

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

**404** – Key not found within scope

Content-Type: `application/json`

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

### Example

```bash
curl -X DELETE "https://crm.nf-hotel.pl/api_keys/keys?id=00000000-0000-4000-8000-000000000000" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/api_keys/keys`

List API keys

Returns paginated API keys visible to the current user, including per-key role assignments and organization context.

Requires features: api_keys.view

**Tags:** API Keys

**Requires authentication.**

**Features:** api_keys.view

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

### Responses

**200** – Collection of API keys

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "string",
      "name": "string",
      "description": null,
      "keyPrefix": "string",
      "organizationId": null,
      "organizationName": null,
      "createdAt": "string",
      "lastUsedAt": null,
      "expiresAt": null,
      "roles": [
        {
          "id": "string",
          "name": null
        }
      ]
    }
  ],
  "total": 1,
  "page": 1,
  "pageSize": 1,
  "totalPages": 1
}
```

**400** – Tenant context missing

Content-Type: `application/json`

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

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/api_keys/keys" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/api_keys/keys`

Create API key

Creates a new API key, returning the one-time secret value together with the generated key prefix and scope details.

Requires features: api_keys.create

**Tags:** API Keys

**Requires authentication.**

**Features:** api_keys.create

### Request Body

Content-Type: `application/json`

```json
{
  "name": "string",
  "description": null,
  "tenantId": null,
  "organizationId": null,
  "roles": [],
  "expiresAt": null
}
```

### Responses

**201** – API key created successfully

Content-Type: `application/json`

```json
{
  "id": "string",
  "name": "string",
  "keyPrefix": "string",
  "tenantId": null,
  "organizationId": null,
  "roles": [
    {
      "id": "string",
      "name": null
    }
  ]
}
```

**400** – Invalid payload or missing tenant context

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/api_keys/keys" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"name\": \"string\",
  \"description\": null,
  \"tenantId\": null,
  \"organizationId\": null,
  \"roles\": [],
  \"expiresAt\": null
}"
```

## 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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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 `/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/auth/users/resend-invite" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## PUT `/call_transcriptions/action-items`

Toggle an action item

Marks one HermesAI-produced follow-up as done or not done, stamping who did it and when.

Requires features: call_transcriptions.view

**Tags:** Call transcriptions

**Requires authentication.**

**Features:** call_transcriptions.view

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – Updated item.

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "isDone": true,
  "doneAt": null,
  "doneByUserId": null
}
```

**400** – Invalid payload.

Content-Type: `application/json`

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

**404** – No such item in the caller scope.

Content-Type: `application/json`

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

### Example

```bash
curl -X PUT "https://crm.nf-hotel.pl/call_transcriptions/action-items" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\",
  \"isDone\": true
}"
```

## DELETE `/call_transcriptions/agent-numbers`

Remove an agent number

Neither auth.users nor staff team members carry a phone number, so this is the only place the "which employee owns this number" fact lives. Numbers are normalized on write so their lookup hashes match the ones computed during call matching.

Requires features: call_transcriptions.manage

**Tags:** Call transcriptions

**Requires authentication.**

**Features:** call_transcriptions.manage

### Responses

**200** – Removed.

Content-Type: `application/json`

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

### Example

```bash
curl -X DELETE "https://crm.nf-hotel.pl/call_transcriptions/agent-numbers" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/call_transcriptions/agent-numbers`

List agent numbers

Neither auth.users nor staff team members carry a phone number, so this is the only place the "which employee owns this number" fact lives. Numbers are normalized on write so their lookup hashes match the ones computed during call matching.

Requires features: call_transcriptions.manage

**Tags:** Call transcriptions

**Requires authentication.**

**Features:** call_transcriptions.manage

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

### Responses

**200** – Agent numbers.

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "msisdn": "string",
      "userId": null,
      "userName": null,
      "label": null,
      "source": "string",
      "isActive": true,
      "claimedByCustomField": true
    }
  ],
  "total": 1
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/call_transcriptions/agent-numbers?page=1&pageSize=50&assignment=all" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/call_transcriptions/agent-numbers`

Add an agent number

Neither auth.users nor staff team members carry a phone number, so this is the only place the "which employee owns this number" fact lives. Numbers are normalized on write so their lookup hashes match the ones computed during call matching.

Requires features: call_transcriptions.manage

**Tags:** Call transcriptions

**Requires authentication.**

**Features:** call_transcriptions.manage

### Request Body

Content-Type: `application/json`

```json
{
  "msisdn": "string",
  "userId": null,
  "label": null
}
```

### Responses

**200** – Created.

Content-Type: `application/json`

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

**400** – Invalid or duplicate number.

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/call_transcriptions/agent-numbers" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"msisdn\": \"string\",
  \"userId\": null,
  \"label\": null
}"
```

## PUT `/call_transcriptions/agent-numbers`

Update an agent number

Neither auth.users nor staff team members carry a phone number, so this is the only place the "which employee owns this number" fact lives. Numbers are normalized on write so their lookup hashes match the ones computed during call matching.

Requires features: call_transcriptions.manage

**Tags:** Call transcriptions

**Requires authentication.**

**Features:** call_transcriptions.manage

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – Updated.

Content-Type: `application/json`

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

**400** – Invalid or duplicate number.

Content-Type: `application/json`

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

### Example

```bash
curl -X PUT "https://crm.nf-hotel.pl/call_transcriptions/agent-numbers" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"userId\": null,
  \"label\": null,
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/call_transcriptions/calls`

Read calls changed since a cursor

Machine-facing read feed for external systems. Each item carries everything the operator modal shows — parties, property match, HermesAI feedback, summary, action items and the transcript. Every feedback item also carries `comments[]`: what operators wrote about THAT remark, oldest first, with author and timestamps, capped at 50 per remark with the true count in `commentsTotal` (deactivated comments are omitted). Comments are never pushed anywhere when written — writing, editing or withdrawing one only bumps the call's `updated_at`, so it returns on the cursor and is delivered on your next poll. Paged with an opaque keyset cursor ordered by `(updated_at, id)`: pass `updatedSince` on the first run, then keep echoing the latest `nextCursor` — it is returned on every page that had rows and echoed back unchanged when a page is empty, so an empty page means "nothing new", never "start over". A call becomes visible a couple of seconds after it changes; that settling delay is what keeps a slow-committing write from landing behind a cursor already handed out. Speaker segments are opt-in via `include=segments`. Audio is not exposed here — playback stays behind the separate `call_transcriptions.listen` grant.

Requires features: call_transcriptions.integrate

**Tags:** Call transcriptions

**Requires authentication.**

**Features:** call_transcriptions.integrate

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| cursor | query | any | Optional |
| updatedSince | query | any | Optional |
| pageSize | query | any | Optional |
| include | query | any | Required |
| propertyId | query | any | Optional |
| transcriptionStatus | query | any | Optional |
| direction | query | any | Optional |
| callStatus | query | any | Optional |
| assignment | query | any | Optional |

### Responses

**200** – A page of calls.

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "occurredAt": "string",
      "endedAt": null,
      "updatedAt": "string",
      "direction": "MO",
      "callStatus": "ESTABLISHED",
      "durationSeconds": null,
      "counterpartyName": null,
      "counterpartyNumber": null,
      "agentNumber": null,
      "agentUserId": null,
      "agentName": null,
      "propertyId": null,
      "propertyName": null,
      "matchSource": null,
      "matchConfidence": null,
      "transcriptionStatus": "pending",
      "transcript": null
    }
  ],
  "nextCursor": null,
  "pageSize": 1
}
```

**400** – Invalid query, an unreadable cursor, `cursor` together with `updatedSince`, or `assignment=unassigned` together with `propertyId`.

Content-Type: `application/json`

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

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/call_transcriptions/calls?pageSize=25&assignment=all" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/call_transcriptions/directory`

List properties, their contact numbers and agent numbers (paginated)

Properties with the phone numbers of their linked contacts, plus the mapping from Wirtualna Centralka numbers to Open Mercato user ids and their Slack ids. Phone numbers are returned in full (normalized digits, no plus sign) because matching is the endpoint's only purpose — grant `call_transcriptions.integrate` to a dedicated integration role.

**PAGINATED — one call is not the whole directory.** `properties` is a single page (default 50, max 100). Keep requesting `page=nextPage` while `hasMore` is true, or follow the `Link: <…>; rel="next"` response header. Skipping this fails silently: each page is a valid, complete-looking response, and the missing properties only show up later as calls that match nothing. `agents` and `feedbackTags` are NOT paginated — they repeat identically on every page, so read them once.

**`agents[].slackUserId`** is the Slack member ID to send that employee's call feedback to. `null` means Open Mercato does not know it — in that case do NOT send anything and do NOT guess a recipient from the name or email; a message delivered to the wrong colleague is worse than no message. One entry per NUMBER, so an employee with two extensions appears twice with the same `slackUserId`.

**`agents[].label` is now always `null`.** Numbers moved to a multi-value custom field on the user, which has no room for a per-number description. The field is kept so the change stays backwards compatible.

Requires features: call_transcriptions.integrate

**Tags:** Call transcriptions

**Requires authentication.**

**Features:** call_transcriptions.integrate

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

### Responses

**200** – Directory page.

Content-Type: `application/json`

```json
{
  "properties": [
    {
      "propertyId": "00000000-0000-4000-8000-000000000000",
      "name": "string",
      "code": null,
      "city": null,
      "updatedAt": null,
      "contacts": [
        {
          "personEntityId": "00000000-0000-4000-8000-000000000000",
          "displayName": null,
          "phone": null,
          "role": null,
          "isPrimary": true
        }
      ]
    }
  ],
  "agents": [
    {
      "userId": null,
      "name": null,
      "msisdn": "string",
      "label": null,
      "slackUserId": null
    }
  ],
  "feedbackTags": [
    {
      "slug": "string",
      "label": "string",
      "color": null
    }
  ],
  "total": 1,
  "page": 1,
  "pageSize": 1,
  "totalPages": 1,
  "hasMore": true,
  "nextPage": null
}
```

**400** – Invalid query.

Content-Type: `application/json`

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

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/call_transcriptions/directory?page=1&pageSize=50" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/call_transcriptions/pending`

Fetch (and optionally claim) pending transcription jobs

Calls whose encrypted audio is stored and which still need a transcript. With claim=1 the rows are atomically marked as claimed (FOR UPDATE SKIP LOCKED), so concurrent workers never receive the same call. Claims older than 30 minutes are offered again, which makes a crashed worker self-healing.

Requires features: call_transcriptions.integrate

**Tags:** Call transcriptions

**Requires authentication.**

**Features:** call_transcriptions.integrate

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| limit | query | any | Optional |
| claim | query | any | Optional |
| workerId | query | any | Optional |

### Responses

**200** – Pending jobs.

Content-Type: `application/json`

```json
{
  "items": [
    {
      "callId": "00000000-0000-4000-8000-000000000000",
      "occurredAt": "string",
      "direction": "string",
      "callStatus": "string",
      "durationSeconds": null,
      "encryptionKeyName": null,
      "audioUrl": "string",
      "propertyHint": null,
      "attempts": 1
    }
  ],
  "claimed": true
}
```

**400** – Invalid query.

Content-Type: `application/json`

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

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/call_transcriptions/pending?limit=10&claim=0" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/call_transcriptions/phone-index/refresh`

Rebuild the phone index (inline for one organization, queued otherwise)

Rebuilds the phone→person index. With `organizationId` the rebuild runs synchronously and the response means it is done — that is the path the UI uses after adding a contact. Without it the caller visible scope is rebuilt through the queue. The index is also rebuilt hourly.

Requires features: call_transcriptions.manage

**Tags:** Call transcriptions

**Requires authentication.**

**Features:** call_transcriptions.manage

### Request Body

Content-Type: `application/json`

```json
{}
```

### Responses

**200** – Rebuilt or queued.

Content-Type: `application/json`

```json
{
  "ok": true,
  "applied": true,
  "indexed": null,
  "queued": 1,
  "failed": 1
}
```

**400** – Invalid payload.

Content-Type: `application/json`

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

**404** – Organization outside the caller scope.

Content-Type: `application/json`

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

**503** – Queue unavailable; the hourly schedule still applies.

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/call_transcriptions/phone-index/refresh" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{}"
```

## GET `/call_transcriptions/recordings`

List calls

Lists calls with their match state. The `unassigned` filter is the triage bucket: those calls appear on no property timeline, so this is the only place they are visible.

Requires features: call_transcriptions.view

**Tags:** Call transcriptions

**Requires authentication.**

**Features:** call_transcriptions.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| page | query | any | Optional |
| pageSize | query | any | Optional |
| sort | query | any | Optional |
| order | query | any | Optional |
| propertyId | query | any | Optional |
| assignment | query | any | Optional |
| transcriptionStatus | query | any | Optional |
| direction | query | any | Optional |
| callStatus | query | any | Optional |
| skipped | query | any | Optional |
| from | query | any | Optional |
| to | query | any | Optional |
| search | query | any | Optional |

### Responses

**200** – Calls.

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "occurredAt": "string",
      "direction": "MO",
      "callStatus": "ESTABLISHED",
      "durationSeconds": null,
      "counterpartyName": null,
      "counterpartyNumber": null,
      "agentName": null,
      "propertyId": null,
      "propertyName": null,
      "matchSource": null,
      "transcriptionStatus": "pending",
      "audioStatus": "string",
      "legCount": 1,
      "agentAmbiguous": true,
      "agentCandidates": [
        {
          "userId": "00000000-0000-4000-8000-000000000000",
          "name": "string"
        }
      ],
      "agentUserId": null,
      "isSkipped": true
    }
  ],
  "total": 1,
  "totalPages": 1,
  "page": 1,
  "pageSize": 1
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/call_transcriptions/recordings?page=1&pageSize=50&sort=occurredAt&order=desc&assignment=all&skipped=hide" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## PUT `/call_transcriptions/recordings`

Assign a call to a property by hand

Lists calls with their match state. The `unassigned` filter is the triage bucket: those calls appear on no property timeline, so this is the only place they are visible.

Requires features: call_transcriptions.manage

**Tags:** Call transcriptions

**Requires authentication.**

**Features:** call_transcriptions.manage

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – Assigned. `propertyOrganizationId` lets the caller create related records in the property own organization.

Content-Type: `application/json`

```json
{
  "ok": true,
  "id": "00000000-0000-4000-8000-000000000000",
  "propertyId": null,
  "propertyOrganizationId": null
}
```

**400** – Invalid payload.

Content-Type: `application/json`

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

**404** – No such call or property in scope.

Content-Type: `application/json`

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

### Example

```bash
curl -X PUT "https://crm.nf-hotel.pl/call_transcriptions/recordings" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\",
  \"propertyId\": null,
  \"agentUserId\": null
}"
```

## GET `/call_transcriptions/recordings/{id}`

Get one call with its transcription

Everything the timeline modal shows for one call: parties, timing, HermesAI feedback, summary, action items and the full transcript with speaker segments. Phone numbers are returned masked.

Requires features: call_transcriptions.view

**Tags:** Call transcriptions

**Requires authentication.**

**Features:** call_transcriptions.view

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

### Responses

**200** – Call detail.

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "occurredAt": "string",
  "direction": "MO",
  "callStatus": "ESTABLISHED",
  "durationSeconds": null,
  "counterpartyName": null,
  "counterpartyNumber": null,
  "agentNumber": null,
  "agentUserId": null,
  "agentName": null,
  "propertyId": null,
  "matchSource": null,
  "matchConfidence": null,
  "transcriptionStatus": "pending",
  "lastError": null,
  "hasAudio": true,
  "canListen": true,
  "audioUrl": null,
  "recordingWebUrl": null,
  "transcript": null
}
```

**400** – Invalid identifier.

Content-Type: `application/json`

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

**404** – No such call in the caller scope.

Content-Type: `application/json`

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

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/call_transcriptions/recordings/00000000-0000-4000-8000-000000000000" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/call_transcriptions/recordings/{id}/audio`

Stream a recording (CMS envelope, or decrypted for playback)

Streams the recording. `variant=encrypted` (default) returns the CMS envelope exactly as Play delivered it — the transcription provider decrypts it with its own key, which never reaches this endpoint. `variant=decoded` decrypts in memory for in-app playback and supports `Range`; it requires `call_transcriptions.listen`, while `encrypted` requires `call_transcriptions.integrate`.

Requires features: call_transcriptions.view

**Tags:** Call transcriptions

**Requires authentication.**

**Features:** call_transcriptions.view

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

### Responses

**200** – The recording, encrypted or decrypted per `variant`.

Content-Type: `application/json`

**206** – A byte range of the decrypted recording (`variant=decoded`).

Content-Type: `application/json`

**400** – Invalid identifier or variant.

Content-Type: `application/json`

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

**404** – No such call, or its audio is not stored.

Content-Type: `application/json`

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

**409** – The recording was sealed with a different key than the one configured.

Content-Type: `application/json`

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

**416** – The requested range lies outside the recording.

Content-Type: `application/json`

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

**503** – No private key configured for this organization.

Content-Type: `application/json`

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

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/call_transcriptions/recordings/00000000-0000-4000-8000-000000000000/audio?variant=encrypted" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/call_transcriptions/transcripts`

Upsert the transcription for one call

Ingest contract v1. HermesAI posts the result of one transcription job identified by the callId it received from GET /api/call_transcriptions/pending. Idempotent per callId.

Requires features: call_transcriptions.integrate

**Tags:** Call transcriptions

**Requires authentication.**

**Features:** call_transcriptions.integrate

### Request Body

Content-Type: `application/json`

```json
{
  "callId": "00000000-0000-4000-8000-000000000000",
  "status": "done"
}
```

### Responses

**200** – Transcript stored and the call matched.

Content-Type: `application/json`

```json
{
  "ok": true,
  "callId": "00000000-0000-4000-8000-000000000000",
  "transcriptionStatus": "string",
  "propertyId": null,
  "matchSource": null,
  "matchConfidence": null
}
```

**400** – Payload failed validation.

Content-Type: `application/json`

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

**404** – No such call in the caller scope.

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/call_transcriptions/transcripts" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"callId\": \"00000000-0000-4000-8000-000000000000\",
  \"status\": \"done\"
}"
```

## GET `/call_transcriptions/webhooks/play/{secret}`

Connection test used by the Play panel before it will save the address

Receives the CallModel notification Play sends when a call ends. The unguessable path segment identifies the tenant; HTTP Basic authenticates Play itself. Idempotent per callSessionId. GET on the same address is the panel connection test and writes nothing. An authenticated POST whose body is not a call (the panel also probes this way) is acknowledged with `ignored: true` rather than rejected, because a non-2xx there blocks saving the configuration.

**Tags:** Call transcriptions

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

### Responses

**200** – Address and credentials are valid.

Content-Type: `application/json`

**401** – Basic credentials did not match.

Content-Type: `application/json`

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

**404** – Unknown endpoint secret.

Content-Type: `application/json`

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

**503** – Webhook credentials are not configured.

Content-Type: `application/json`

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

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/call_transcriptions/webhooks/play/:secret" \
  -H "Accept: application/json"
```

## POST `/call_transcriptions/webhooks/play/{secret}`

Receive a Play call notification

Receives the CallModel notification Play sends when a call ends. The unguessable path segment identifies the tenant; HTTP Basic authenticates Play itself. Idempotent per callSessionId. GET on the same address is the panel connection test and writes nothing. An authenticated POST whose body is not a call (the panel also probes this way) is acknowledged with `ignored: true` rather than rejected, because a non-2xx there blocks saving the configuration.

**Tags:** Call transcriptions

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

### Responses

**200** – Accepted (possibly as a duplicate).

Content-Type: `application/json`

```json
{
  "ok": true,
  "callId": null,
  "duplicate": true,
  "ignored": true
}
```

**400** – Payload could not be interpreted.

Content-Type: `application/json`

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

**401** – Basic credentials did not match.

Content-Type: `application/json`

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

**404** – Unknown endpoint secret.

Content-Type: `application/json`

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

**503** – Webhook credentials are not configured.

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/call_transcriptions/webhooks/play/:secret" \
  -H "Accept: application/json"
```

## POST `/catalog/bulk-delete`

Start bulk deleting catalog products

Requires features: catalog.products.manage

**Tags:** Product Catalog

**Requires authentication.**

**Features:** catalog.products.manage

### Responses

**201** – Success response

Content-Type: `application/json`

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/catalog/bulk-delete" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## DELETE `/catalog/categories`

Delete category

Deletes a category by id.

Requires features: catalog.categories.manage

**Tags:** Catalog

**Requires authentication.**

**Features:** catalog.categories.manage

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – Category deleted

Content-Type: `application/json`

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

### Example

```bash
curl -X DELETE "https://crm.nf-hotel.pl/catalog/categories" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/catalog/categories`

List categories

Returns a paginated collection of categories scoped to the authenticated organization.

Requires features: catalog.categories.view

**Tags:** Catalog

**Requires authentication.**

**Features:** catalog.categories.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| view | query | any | Optional |
| page | query | any | Optional |
| pageSize | query | any | Optional |
| search | query | any | Optional |
| status | query | any | Optional |
| ids | query | any | Optional. Comma-separated list of record UUIDs to filter by (max 200). |

### Responses

**200** – Paginated categories

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "name": "string",
      "slug": null,
      "description": null,
      "parentId": null,
      "parentName": null,
      "depth": 1,
      "treePath": "string",
      "pathLabel": "string",
      "childCount": 1,
      "descendantCount": 1,
      "isActive": true,
      "updatedAt": null,
      "organizationId": null,
      "tenantId": "00000000-0000-4000-8000-000000000000"
    }
  ],
  "total": 1,
  "totalPages": 1
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/catalog/categories?view=manage&page=1&pageSize=50" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/catalog/categories`

Create category

Creates a new product category.

Requires features: catalog.categories.manage

**Tags:** Catalog

**Requires authentication.**

**Features:** catalog.categories.manage

### Request Body

Content-Type: `application/json`

```json
{
  "organizationId": "00000000-0000-4000-8000-000000000000",
  "tenantId": "00000000-0000-4000-8000-000000000000",
  "name": "string",
  "slug": null,
  "parentId": null
}
```

### Responses

**201** – Category created

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/catalog/categories" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"organizationId\": \"00000000-0000-4000-8000-000000000000\",
  \"tenantId\": \"00000000-0000-4000-8000-000000000000\",
  \"name\": \"string\",
  \"slug\": null,
  \"parentId\": null
}"
```

## PUT `/catalog/categories`

Update category

Updates an existing category by id.

Requires features: catalog.categories.manage

**Tags:** Catalog

**Requires authentication.**

**Features:** catalog.categories.manage

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – Category updated

Content-Type: `application/json`

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

### Example

```bash
curl -X PUT "https://crm.nf-hotel.pl/catalog/categories" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\",
  \"slug\": null,
  \"parentId\": null
}"
```

## GET `/catalog/dictionaries/{key}`

Get dictionary entries by key

Returns dictionary entries for a specific key (e.g., currency, unit).

Requires features: catalog.products.manage

**Tags:** Catalog

**Requires authentication.**

**Features:** catalog.products.manage

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

### Responses

**200** – Dictionary entries

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "entries": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "value": "string",
      "label": "string",
      "color": null,
      "icon": null
    }
  ]
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/catalog/dictionaries/:key" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## DELETE `/catalog/offers`

Delete offer

Deletes an offer by id.

Requires features: sales.channels.manage

**Tags:** Catalog

**Requires authentication.**

**Features:** sales.channels.manage

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – Offer deleted

Content-Type: `application/json`

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

### Example

```bash
curl -X DELETE "https://crm.nf-hotel.pl/catalog/offers" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/catalog/offers`

List offers

Returns a paginated collection of offers scoped to the authenticated organization.

Requires features: sales.channels.manage

**Tags:** Catalog

**Requires authentication.**

**Features:** sales.channels.manage

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| page | query | any | Optional |
| pageSize | query | any | Optional |
| productId | query | any | Optional |
| channelId | query | any | Optional |
| channelIds | query | any | Optional |
| id | query | any | Optional |
| search | query | any | Optional |
| isActive | query | any | Optional |
| withDeleted | query | any | Optional |
| sortField | query | any | Optional |
| sortDir | query | any | Optional |
| ids | query | any | Optional. Comma-separated list of record UUIDs to filter by (max 200). |

### Responses

**200** – Paginated offers

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "productId": null,
      "organizationId": null,
      "tenantId": null,
      "channelId": null,
      "title": "string",
      "description": null,
      "defaultMediaId": null,
      "defaultMediaUrl": null,
      "metadata": null,
      "isActive": null,
      "createdAt": null,
      "updatedAt": null,
      "product": null,
      "productChannelPrice": null
    }
  ],
  "total": 1,
  "totalPages": 1
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/catalog/offers?page=1&pageSize=50" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/catalog/offers`

Create offer

Creates a new offer linking a product to a sales channel.

Requires features: sales.channels.manage

**Tags:** Catalog

**Requires authentication.**

**Features:** sales.channels.manage

### Request Body

Content-Type: `application/json`

```json
{
  "organizationId": "00000000-0000-4000-8000-000000000000",
  "tenantId": "00000000-0000-4000-8000-000000000000",
  "channelId": "00000000-0000-4000-8000-000000000000",
  "title": "string",
  "defaultMediaId": null,
  "defaultMediaUrl": null,
  "productId": "00000000-0000-4000-8000-000000000000"
}
```

### Responses

**201** – Offer created

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/catalog/offers" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"organizationId\": \"00000000-0000-4000-8000-000000000000\",
  \"tenantId\": \"00000000-0000-4000-8000-000000000000\",
  \"channelId\": \"00000000-0000-4000-8000-000000000000\",
  \"title\": \"string\",
  \"defaultMediaId\": null,
  \"defaultMediaUrl\": null,
  \"productId\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## PUT `/catalog/offers`

Update offer

Updates an existing offer by id.

Requires features: sales.channels.manage

**Tags:** Catalog

**Requires authentication.**

**Features:** sales.channels.manage

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – Offer updated

Content-Type: `application/json`

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

### Example

```bash
curl -X PUT "https://crm.nf-hotel.pl/catalog/offers" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\",
  \"defaultMediaId\": null,
  \"defaultMediaUrl\": null
}"
```

## DELETE `/catalog/option-schemas`

Delete option schema

Deletes an option schema by id.

Requires features: catalog.settings.manage

**Tags:** Catalog

**Requires authentication.**

**Features:** catalog.settings.manage

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – Option Schema deleted

Content-Type: `application/json`

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

### Example

```bash
curl -X DELETE "https://crm.nf-hotel.pl/catalog/option-schemas" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/catalog/option-schemas`

List option schemas

Returns a paginated collection of option schemas scoped to the authenticated organization.

Requires features: catalog.products.view

**Tags:** Catalog

**Requires authentication.**

**Features:** catalog.products.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| page | query | any | Optional |
| pageSize | query | any | Optional |
| id | query | any | Optional |
| search | query | any | Optional |
| isActive | query | any | Optional |
| withDeleted | query | any | Optional |
| ids | query | any | Optional. Comma-separated list of record UUIDs to filter by (max 200). |

### Responses

**200** – Paginated option schemas

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "name": "string",
      "code": null,
      "description": null,
      "schema": null,
      "metadata": null,
      "is_active": null,
      "created_at": null,
      "updated_at": null
    }
  ],
  "total": 1,
  "totalPages": 1
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/catalog/option-schemas?page=1&pageSize=50" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/catalog/option-schemas`

Create option schema

Creates a new option schema template for product configurations.

Requires features: catalog.settings.manage

**Tags:** Catalog

**Requires authentication.**

**Features:** catalog.settings.manage

### Request Body

Content-Type: `application/json`

```json
{
  "organizationId": "00000000-0000-4000-8000-000000000000",
  "tenantId": "00000000-0000-4000-8000-000000000000",
  "name": "string",
  "schema": {
    "options": [
      {
        "code": "string",
        "label": "string",
        "inputType": "select"
      }
    ]
  }
}
```

### Responses

**201** – Option Schema created

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/catalog/option-schemas" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"organizationId\": \"00000000-0000-4000-8000-000000000000\",
  \"tenantId\": \"00000000-0000-4000-8000-000000000000\",
  \"name\": \"string\",
  \"schema\": {
    \"options\": [
      {
        \"code\": \"string\",
        \"label\": \"string\",
        \"inputType\": \"select\"
      }
    ]
  }
}"
```

## PUT `/catalog/option-schemas`

Update option schema

Updates an existing option schema by id.

Requires features: catalog.settings.manage

**Tags:** Catalog

**Requires authentication.**

**Features:** catalog.settings.manage

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – Option Schema updated

Content-Type: `application/json`

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

### Example

```bash
curl -X PUT "https://crm.nf-hotel.pl/catalog/option-schemas" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## DELETE `/catalog/price-kinds`

Delete price kind

Deletes a price kind by id.

Requires features: catalog.settings.manage

**Tags:** Catalog

**Requires authentication.**

**Features:** catalog.settings.manage

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – Price Kind deleted

Content-Type: `application/json`

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

### Example

```bash
curl -X DELETE "https://crm.nf-hotel.pl/catalog/price-kinds" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/catalog/price-kinds`

List price kinds

Returns a paginated collection of price kinds scoped to the authenticated organization.

Requires features: catalog.settings.manage

**Tags:** Catalog

**Requires authentication.**

**Features:** catalog.settings.manage

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| page | query | any | Optional |
| pageSize | query | any | Optional |
| search | query | any | Optional |
| isPromotion | query | any | Optional |
| isActive | query | any | Optional |
| sortField | query | any | Optional |
| sortDir | query | any | Optional |
| ids | query | any | Optional. Comma-separated list of record UUIDs to filter by (max 200). |

### Responses

**200** – Paginated price kinds

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "organization_id": null,
      "tenant_id": null,
      "code": "string",
      "title": "string",
      "display_mode": null,
      "currency_code": null,
      "is_promotion": null,
      "is_active": null,
      "created_at": null,
      "updated_at": null
    }
  ],
  "total": 1,
  "totalPages": 1
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/catalog/price-kinds?page=1&pageSize=50" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/catalog/price-kinds`

Create price kind

Creates a new price kind for categorizing product prices.

Requires features: catalog.settings.manage

**Tags:** Catalog

**Requires authentication.**

**Features:** catalog.settings.manage

### Request Body

Content-Type: `application/json`

```json
{
  "tenantId": "00000000-0000-4000-8000-000000000000",
  "code": "string",
  "title": "string",
  "displayMode": "excluding-tax"
}
```

### Responses

**201** – Price Kind created

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/catalog/price-kinds" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"tenantId\": \"00000000-0000-4000-8000-000000000000\",
  \"code\": \"string\",
  \"title\": \"string\",
  \"displayMode\": \"excluding-tax\"
}"
```

## PUT `/catalog/price-kinds`

Update price kind

Updates an existing price kind by id.

Requires features: catalog.settings.manage

**Tags:** Catalog

**Requires authentication.**

**Features:** catalog.settings.manage

### Request Body

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "displayMode": "excluding-tax"
}
```

### Responses

**200** – Price Kind updated

Content-Type: `application/json`

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

### Example

```bash
curl -X PUT "https://crm.nf-hotel.pl/catalog/price-kinds" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\",
  \"displayMode\": \"excluding-tax\"
}"
```

## DELETE `/catalog/prices`

Delete price

Deletes a price by id.

Requires features: catalog.pricing.manage

**Tags:** Catalog

**Requires authentication.**

**Features:** catalog.pricing.manage

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – Price deleted

Content-Type: `application/json`

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

### Example

```bash
curl -X DELETE "https://crm.nf-hotel.pl/catalog/prices" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/catalog/prices`

List prices

Returns a paginated collection of prices scoped to the authenticated organization.

Requires features: catalog.products.view

**Tags:** Catalog

**Requires authentication.**

**Features:** catalog.products.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| page | query | any | Optional |
| pageSize | query | any | Optional |
| productId | query | any | Optional |
| variantId | query | any | Optional |
| offerId | query | any | Optional |
| channelId | query | any | Optional |
| currencyCode | query | any | Optional |
| priceKindId | query | any | Optional |
| kind | query | any | Optional |
| userId | query | any | Optional |
| userGroupId | query | any | Optional |
| customerId | query | any | Optional |
| customerGroupId | query | any | Optional |
| quantity | query | any | Optional |
| quantityUnit | query | any | Optional |
| withDeleted | query | any | Optional |
| sortField | query | any | Optional |
| sortDir | query | any | Optional |
| ids | query | any | Optional. Comma-separated list of record UUIDs to filter by (max 200). |

### Responses

**200** – Paginated prices

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "product_id": null,
      "variant_id": null,
      "offer_id": null,
      "currency_code": null,
      "price_kind_id": null,
      "kind": null,
      "min_quantity": null,
      "max_quantity": null,
      "unit_price_net": null,
      "unit_price_gross": null,
      "tax_rate": null,
      "tax_amount": null,
      "channel_id": null,
      "user_id": null,
      "user_group_id": null,
      "customer_id": null,
      "customer_group_id": null,
      "metadata": null,
      "starts_at": null,
      "ends_at": null,
      "created_at": null,
      "updated_at": null
    }
  ],
  "total": 1,
  "totalPages": 1
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/catalog/prices?page=1&pageSize=50" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/catalog/prices`

Create price

Creates a new price entry for a product or variant.

Requires features: catalog.pricing.manage

**Tags:** Catalog

**Requires authentication.**

**Features:** catalog.pricing.manage

### Request Body

Content-Type: `application/json`

```json
{
  "organizationId": "00000000-0000-4000-8000-000000000000",
  "tenantId": "00000000-0000-4000-8000-000000000000",
  "currencyCode": "string",
  "priceKindId": "00000000-0000-4000-8000-000000000000",
  "taxRateId": null
}
```

### Responses

**201** – Price created

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/catalog/prices" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"organizationId\": \"00000000-0000-4000-8000-000000000000\",
  \"tenantId\": \"00000000-0000-4000-8000-000000000000\",
  \"currencyCode\": \"string\",
  \"priceKindId\": \"00000000-0000-4000-8000-000000000000\",
  \"taxRateId\": null
}"
```

## PUT `/catalog/prices`

Update price

Updates an existing price by id.

Requires features: catalog.pricing.manage

**Tags:** Catalog

**Requires authentication.**

**Features:** catalog.pricing.manage

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – Price updated

Content-Type: `application/json`

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

### Example

```bash
curl -X PUT "https://crm.nf-hotel.pl/catalog/prices" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\",
  \"taxRateId\": null
}"
```

## GET `/catalog/product-media`

List product media

Returns a list of media attachments for a specific product.

Requires features: catalog.products.view

**Tags:** Catalog

**Requires authentication.**

**Features:** catalog.products.view

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

### Responses

**200** – List of product media

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "fileName": "string",
      "url": "string",
      "thumbnailUrl": "string"
    }
  ]
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/catalog/product-media?productId=00000000-0000-4000-8000-000000000000" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## DELETE `/catalog/product-unit-conversions`

Delete product unit conversion

Deletes a product unit conversion by id.

Requires features: catalog.products.manage

**Tags:** Catalog

**Requires authentication.**

**Features:** catalog.products.manage

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – Product unit conversion deleted

Content-Type: `application/json`

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

### Example

```bash
curl -X DELETE "https://crm.nf-hotel.pl/catalog/product-unit-conversions" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"organizationId\": \"00000000-0000-4000-8000-000000000000\",
  \"tenantId\": \"00000000-0000-4000-8000-000000000000\",
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/catalog/product-unit-conversions`

List product unit conversions

Returns a paginated collection of product unit conversions scoped to the authenticated organization.

Requires features: catalog.products.view

**Tags:** Catalog

**Requires authentication.**

**Features:** catalog.products.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| page | query | any | Optional |
| pageSize | query | any | Optional |
| id | query | any | Optional |
| productId | query | any | Optional |
| unitCode | query | any | Optional |
| isActive | query | any | Optional |
| sortField | query | any | Optional |
| sortDir | query | any | Optional |
| ids | query | any | Optional. Comma-separated list of record UUIDs to filter by (max 200). |

### Responses

**200** – Paginated product unit conversions

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "product_id": "00000000-0000-4000-8000-000000000000",
      "unit_code": "string",
      "to_base_factor": 1,
      "sort_order": null,
      "is_active": null,
      "metadata": null,
      "created_at": null,
      "updated_at": null
    }
  ],
  "total": 1,
  "totalPages": 1
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/catalog/product-unit-conversions?page=1&pageSize=50" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/catalog/product-unit-conversions`

Create product unit conversion

Creates a product unit conversion.

Requires features: catalog.products.manage

**Tags:** Catalog

**Requires authentication.**

**Features:** catalog.products.manage

### Request Body

Content-Type: `application/json`

```json
{
  "organizationId": "00000000-0000-4000-8000-000000000000",
  "tenantId": "00000000-0000-4000-8000-000000000000",
  "productId": "00000000-0000-4000-8000-000000000000",
  "unitCode": "string",
  "toBaseFactor": 1
}
```

### Responses

**201** – Product unit conversion created

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/catalog/product-unit-conversions" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"organizationId\": \"00000000-0000-4000-8000-000000000000\",
  \"tenantId\": \"00000000-0000-4000-8000-000000000000\",
  \"productId\": \"00000000-0000-4000-8000-000000000000\",
  \"unitCode\": \"string\",
  \"toBaseFactor\": 1
}"
```

## PUT `/catalog/product-unit-conversions`

Update product unit conversion

Updates an existing product unit conversion by id.

Requires features: catalog.products.manage

**Tags:** Catalog

**Requires authentication.**

**Features:** catalog.products.manage

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – Product unit conversion updated

Content-Type: `application/json`

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

### Example

```bash
curl -X PUT "https://crm.nf-hotel.pl/catalog/product-unit-conversions" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## DELETE `/catalog/products`

Delete product

Deletes a product by id.

Requires features: catalog.products.manage

**Tags:** Catalog

**Requires authentication.**

**Features:** catalog.products.manage

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – Product deleted

Content-Type: `application/json`

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

### Example

```bash
curl -X DELETE "https://crm.nf-hotel.pl/catalog/products" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/catalog/products`

List products

Returns a paginated collection of products scoped to the authenticated organization.

Requires features: catalog.products.view

**Tags:** Catalog

**Requires authentication.**

**Features:** catalog.products.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| page | query | any | Optional |
| pageSize | query | any | Optional |
| id | query | any | Optional |
| search | query | any | Optional |
| status | query | any | Optional |
| isActive | query | any | Optional |
| configurable | query | any | Optional |
| productType | query | any | Optional |
| channelIds | query | any | Optional |
| channelId | query | any | Optional |
| categoryIds | query | any | Optional |
| tagIds | query | any | Optional |
| offerId | query | any | Optional |
| userId | query | any | Optional |
| userGroupId | query | any | Optional |
| customerId | query | any | Optional |
| customerGroupId | query | any | Optional |
| quantity | query | any | Optional |
| quantityUnit | query | any | Optional |
| priceDate | query | any | Optional |
| sortField | query | any | Optional |
| sortDir | query | any | Optional |
| withDeleted | query | any | Optional |
| customFieldset | query | any | Optional |
| ids | query | any | Optional. Comma-separated list of record UUIDs to filter by (max 200). |

### Responses

**200** – Paginated products

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "title": null,
      "subtitle": null,
      "description": null,
      "sku": null,
      "handle": null,
      "product_type": null,
      "status_entry_id": null,
      "primary_currency_code": null,
      "default_unit": null,
      "default_sales_unit": null,
      "default_sales_unit_quantity": null,
      "uom_rounding_scale": null,
      "uom_rounding_mode": null,
      "unit_price_enabled": null,
      "unit_price_reference_unit": null,
      "unit_price_base_quantity": null,
      "default_media_id": null,
      "default_media_url": null,
      "weight_value": null,
      "weight_unit": null,
      "dimensions": null,
      "is_configurable": null,
      "is_active": null,
      "country_of_origin_code": null,
      "pkwiu_code": null,
      "cn_code": null,
      "hs_code": null,
      "tax_classification_code": null,
      "gtu_codes": null,
      "age_min": null,
      "is_excise_good": null,
      "excise_category": null,
      "requires_prescription": null,
      "hazmat_class": null,
      "un_number": null,
      "hazmat_packing_group": null,
      "contains_lithium_battery": null,
      "launch_at": null,
      "end_of_life_at": null,
      "available_from": null,
      "available_until": null,
      "min_order_qty": null,
      "max_order_qty": null,
      "order_qty_increment": null,
      "requires_shipping": null,
      "is_quote_only": null,
      "seo_title": null,
      "seo_description": null,
      "canonical_url": null,
      "metadata": null,
      "custom_fieldset_code": null,
      "option_schema_id": null,
      "created_at": null,
      "updated_at": null,
      "pricing": null
    }
  ],
  "total": 1,
  "totalPages": 1
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/catalog/products?page=1&pageSize=50" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/catalog/products`

Create product

Creates a new product in the catalog.

Requires features: catalog.products.manage

**Tags:** Catalog

**Requires authentication.**

**Features:** catalog.products.manage

### Request Body

Content-Type: `application/json`

```json
{
  "organizationId": "00000000-0000-4000-8000-000000000000",
  "tenantId": "00000000-0000-4000-8000-000000000000",
  "title": "string",
  "sku": null,
  "taxRateId": null,
  "taxRate": null,
  "productType": "simple",
  "defaultUnit": null,
  "defaultSalesUnit": null,
  "unitPriceReferenceUnit": null,
  "defaultMediaId": null,
  "defaultMediaUrl": null,
  "weightValue": null,
  "weightUnit": null,
  "dimensions": null,
  "optionSchemaId": null,
  "customFieldsetCode": null,
  "countryOfOriginCode": null,
  "pkwiuCode": null,
  "cnCode": null,
  "hsCode": null,
  "taxClassificationCode": null,
  "gtuCodes": null,
  "ageMin": null,
  "exciseCategory": null,
  "hazmatClass": null,
  "unNumber": null,
  "hazmatPackingGroup": null,
  "launchAt": null,
  "endOfLifeAt": null,
  "availableFrom": null,
  "availableUntil": null,
  "minOrderQty": null,
  "maxOrderQty": null,
  "orderQtyIncrement": null,
  "seoTitle": null,
  "seoDescription": null,
  "canonicalUrl": null
}
```

### Responses

**201** – Product created

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/catalog/products" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"organizationId\": \"00000000-0000-4000-8000-000000000000\",
  \"tenantId\": \"00000000-0000-4000-8000-000000000000\",
  \"title\": \"string\",
  \"sku\": null,
  \"taxRateId\": null,
  \"taxRate\": null,
  \"productType\": \"simple\",
  \"defaultUnit\": null,
  \"defaultSalesUnit\": null,
  \"unitPriceReferenceUnit\": null,
  \"defaultMediaId\": null,
  \"defaultMediaUrl\": null,
  \"weightValue\": null,
  \"weightUnit\": null,
  \"dimensions\": null,
  \"optionSchemaId\": null,
  \"customFieldsetCode\": null,
  \"countryOfOriginCode\": null,
  \"pkwiuCode\": null,
  \"cnCode\": null,
  \"hsCode\": null,
  \"taxClassificationCode\": null,
  \"gtuCodes\": null,
  \"ageMin\": null,
  \"exciseCategory\": null,
  \"hazmatClass\": null,
  \"unNumber\": null,
  \"hazmatPackingGroup\": null,
  \"launchAt\": null,
  \"endOfLifeAt\": null,
  \"availableFrom\": null,
  \"availableUntil\": null,
  \"minOrderQty\": null,
  \"maxOrderQty\": null,
  \"orderQtyIncrement\": null,
  \"seoTitle\": null,
  \"seoDescription\": null,
  \"canonicalUrl\": null
}"
```

## PUT `/catalog/products`

Update product

Updates an existing product by id.

Requires features: catalog.products.manage

**Tags:** Catalog

**Requires authentication.**

**Features:** catalog.products.manage

### Request Body

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "sku": null,
  "taxRateId": null,
  "taxRate": null,
  "defaultUnit": null,
  "defaultSalesUnit": null,
  "unitPriceReferenceUnit": null,
  "defaultMediaId": null,
  "defaultMediaUrl": null,
  "weightValue": null,
  "weightUnit": null,
  "dimensions": null,
  "optionSchemaId": null,
  "customFieldsetCode": null,
  "countryOfOriginCode": null,
  "pkwiuCode": null,
  "cnCode": null,
  "hsCode": null,
  "taxClassificationCode": null,
  "gtuCodes": null,
  "ageMin": null,
  "exciseCategory": null,
  "hazmatClass": null,
  "unNumber": null,
  "hazmatPackingGroup": null,
  "launchAt": null,
  "endOfLifeAt": null,
  "availableFrom": null,
  "availableUntil": null,
  "minOrderQty": null,
  "maxOrderQty": null,
  "orderQtyIncrement": null,
  "seoTitle": null,
  "seoDescription": null,
  "canonicalUrl": null
}
```

### Responses

**200** – Product updated

Content-Type: `application/json`

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

### Example

```bash
curl -X PUT "https://crm.nf-hotel.pl/catalog/products" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\",
  \"sku\": null,
  \"taxRateId\": null,
  \"taxRate\": null,
  \"defaultUnit\": null,
  \"defaultSalesUnit\": null,
  \"unitPriceReferenceUnit\": null,
  \"defaultMediaId\": null,
  \"defaultMediaUrl\": null,
  \"weightValue\": null,
  \"weightUnit\": null,
  \"dimensions\": null,
  \"optionSchemaId\": null,
  \"customFieldsetCode\": null,
  \"countryOfOriginCode\": null,
  \"pkwiuCode\": null,
  \"cnCode\": null,
  \"hsCode\": null,
  \"taxClassificationCode\": null,
  \"gtuCodes\": null,
  \"ageMin\": null,
  \"exciseCategory\": null,
  \"hazmatClass\": null,
  \"unNumber\": null,
  \"hazmatPackingGroup\": null,
  \"launchAt\": null,
  \"endOfLifeAt\": null,
  \"availableFrom\": null,
  \"availableUntil\": null,
  \"minOrderQty\": null,
  \"maxOrderQty\": null,
  \"orderQtyIncrement\": null,
  \"seoTitle\": null,
  \"seoDescription\": null,
  \"canonicalUrl\": null
}"
```

## GET `/catalog/settings`

Read catalog settings

Requires features: catalog.products.view

**Tags:** Catalog

**Requires authentication.**

**Features:** catalog.products.view

### Responses

**200** – Catalog settings

Content-Type: `application/json`

```json
{
  "unitPriceDisplayEnabled": true
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/catalog/settings" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## PUT `/catalog/settings`

Update catalog settings

Requires features: catalog.settings.manage

**Tags:** Catalog

**Requires authentication.**

**Features:** catalog.settings.manage

### Request Body

Content-Type: `application/json`

```json
{
  "unitPriceDisplayEnabled": true
}
```

### Responses

**200** – Updated catalog settings

Content-Type: `application/json`

```json
{
  "unitPriceDisplayEnabled": true
}
```

**400** – Invalid request body

Content-Type: `application/json`

### Example

```bash
curl -X PUT "https://crm.nf-hotel.pl/catalog/settings" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"unitPriceDisplayEnabled\": true
}"
```

## GET `/catalog/tags`

List product tags

Returns a paginated collection of product tags scoped to the authenticated organization.

Requires features: catalog.products.view

**Tags:** Catalog

**Requires authentication.**

**Features:** catalog.products.view

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

### Responses

**200** – Paginated product tags

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "label": "string",
      "slug": null,
      "createdAt": "string",
      "updatedAt": "string"
    }
  ],
  "total": 1,
  "totalPages": 1
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/catalog/tags?page=1&pageSize=50" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## DELETE `/catalog/variants`

Delete variant

Deletes a variant by id.

Requires features: catalog.variants.manage

**Tags:** Catalog

**Requires authentication.**

**Features:** catalog.variants.manage

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – Variant deleted

Content-Type: `application/json`

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

### Example

```bash
curl -X DELETE "https://crm.nf-hotel.pl/catalog/variants" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/catalog/variants`

List variants

Returns a paginated collection of variants scoped to the authenticated organization.

Requires features: catalog.products.view

**Tags:** Catalog

**Requires authentication.**

**Features:** catalog.products.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| page | query | any | Optional |
| pageSize | query | any | Optional |
| id | query | any | Optional |
| search | query | any | Optional |
| productId | query | any | Optional |
| sku | query | any | Optional |
| isActive | query | any | Optional |
| isDefault | query | any | Optional |
| withDeleted | query | any | Optional |
| sortField | query | any | Optional |
| sortDir | query | any | Optional |
| ids | query | any | Optional. Comma-separated list of record UUIDs to filter by (max 200). |

### Responses

**200** – Paginated variants

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "product_id": null,
      "name": null,
      "sku": null,
      "barcode": null,
      "gtin_type": null,
      "hs_code": null,
      "status_entry_id": null,
      "is_default": null,
      "is_active": null,
      "weight_value": null,
      "weight_unit": null,
      "dimensions": null,
      "metadata": null,
      "option_values": null,
      "custom_fieldset_code": null,
      "default_media_id": null,
      "default_media_url": null,
      "created_at": null,
      "updated_at": null
    }
  ],
  "total": 1,
  "totalPages": 1
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/catalog/variants?page=1&pageSize=50" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/catalog/variants`

Create variant

Creates a new product variant.

Requires features: catalog.variants.manage

**Tags:** Catalog

**Requires authentication.**

**Features:** catalog.variants.manage

### Request Body

Content-Type: `application/json`

```json
{
  "organizationId": "00000000-0000-4000-8000-000000000000",
  "tenantId": "00000000-0000-4000-8000-000000000000",
  "productId": "00000000-0000-4000-8000-000000000000",
  "gtinType": null,
  "hsCode": null,
  "defaultMediaId": null,
  "defaultMediaUrl": null,
  "taxRateId": null,
  "taxRate": null,
  "customFieldsetCode": null
}
```

### Responses

**201** – Variant created

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/catalog/variants" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"organizationId\": \"00000000-0000-4000-8000-000000000000\",
  \"tenantId\": \"00000000-0000-4000-8000-000000000000\",
  \"productId\": \"00000000-0000-4000-8000-000000000000\",
  \"gtinType\": null,
  \"hsCode\": null,
  \"defaultMediaId\": null,
  \"defaultMediaUrl\": null,
  \"taxRateId\": null,
  \"taxRate\": null,
  \"customFieldsetCode\": null
}"
```

## PUT `/catalog/variants`

Update variant

Updates an existing variant by id.

Requires features: catalog.variants.manage

**Tags:** Catalog

**Requires authentication.**

**Features:** catalog.variants.manage

### Request Body

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "gtinType": null,
  "hsCode": null,
  "defaultMediaId": null,
  "defaultMediaUrl": null,
  "taxRateId": null,
  "taxRate": null,
  "customFieldsetCode": null
}
```

### Responses

**200** – Variant updated

Content-Type: `application/json`

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

### Example

```bash
curl -X PUT "https://crm.nf-hotel.pl/catalog/variants" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\",
  \"gtinType\": null,
  \"hsCode\": null,
  \"defaultMediaId\": null,
  \"defaultMediaUrl\": null,
  \"taxRateId\": null,
  \"taxRate\": null,
  \"customFieldsetCode\": null
}"
```

## DELETE `/comments`

Deactivate a comment

CRUD for polymorphic, threaded comments (invoices, properties). Edits are versioned; deletes deactivate (soft delete).

Requires features: comments.edit

**Tags:** Comments

**Requires authentication.**

**Features:** comments.edit

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

### Responses

**200** – Deactivated.

Content-Type: `application/json`

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

**404** – Not found.

Content-Type: `application/json`

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

### Example

```bash
curl -X DELETE "https://crm.nf-hotel.pl/comments?id=00000000-0000-4000-8000-000000000000" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/comments`

List comments for a record

Comments attached to (entityType, entityId). Supplying the four pinned* parameters additionally returns comments pinned to a broader target whose pin window overlaps [pinnedWindowStart, pinnedWindowEnd) — used by a wFirma service to show the notes pinned to its contract alongside its own. The four travel together; a partial set is rejected.

Requires features: comments.view

**Tags:** Comments

**Requires authentication.**

**Features:** comments.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| page | query | any | Optional |
| pageSize | query | any | Optional |
| entityType | query | any | Required |
| entityId | query | any | Required |
| authorUserId | query | any | Optional |
| threads | query | any | Optional |
| withDeleted | query | any | Optional |
| pinnedEntityType | query | any | Optional |
| pinnedEntityId | query | any | Optional |
| pinnedWindowStart | query | any | Optional |
| pinnedWindowEnd | query | any | Optional |

### Responses

**200** – Comments.

Content-Type: `application/json`

```json
{
  "items": [],
  "total": 1
}
```

**400** – Incomplete pinned scope.

Content-Type: `application/json`

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

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/comments?page=1&pageSize=100&entityType=wfirma%3Awfirma_invoice&entityId=00000000-0000-4000-8000-000000000000" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/comments`

Add a comment or reply

CRUD for polymorphic, threaded comments (invoices, properties). Edits are versioned; deletes deactivate (soft delete).

Requires features: comments.create

**Tags:** Comments

**Requires authentication.**

**Features:** comments.create

### Request Body

Content-Type: `application/json`

```json
{
  "entityType": "wfirma:wfirma_invoice",
  "entityId": "00000000-0000-4000-8000-000000000000",
  "parentId": null,
  "body": "string"
}
```

### Responses

**201** – Created.

Content-Type: `application/json`

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

**400** – Invalid parent.

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/comments" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"entityType\": \"wfirma:wfirma_invoice\",
  \"entityId\": \"00000000-0000-4000-8000-000000000000\",
  \"parentId\": null,
  \"body\": \"string\"
}"
```

## PUT `/comments`

Edit a comment (versioned)

CRUD for polymorphic, threaded comments (invoices, properties). Edits are versioned; deletes deactivate (soft delete).

Requires features: comments.edit

**Tags:** Comments

**Requires authentication.**

**Features:** comments.edit

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – Updated.

Content-Type: `application/json`

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

**404** – Not found.

Content-Type: `application/json`

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

### Example

```bash
curl -X PUT "https://crm.nf-hotel.pl/comments" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\",
  \"body\": \"string\"
}"
```

## GET `/comments/counts`

Active comment counts for a batch of records

Given an entityType and comma-separated record ids, returns the number of active comments (incl. replies) per id in one grouped query. Used to render comment badges on list rows without N+1 requests.

Requires features: comments.view

**Tags:** Comments

**Requires authentication.**

**Features:** comments.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| entityType | query | any | Required |
| ids | query | any | Optional |

### Responses

**200** – Counts keyed by entity id.

Content-Type: `application/json`

```json
{
  "counts": {
    "key": 1
  }
}
```

**400** – Invalid entityType or ids.

Content-Type: `application/json`

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

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/comments/counts?entityType=wfirma%3Awfirma_invoice" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/comments/versions`

List a comment's prior versions

Read-only prior versions of a comment (versioning).

Requires features: comments.view

**Tags:** Comments

**Requires authentication.**

**Features:** comments.view

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

### Responses

**200** – Versions.

Content-Type: `application/json`

```json
{
  "items": [],
  "total": 1
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/comments/versions?page=1&pageSize=100&commentId=00000000-0000-4000-8000-000000000000" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## 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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/configs/upgrade-actions" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"actionId\": \"string\"
}"
```

## DELETE `/customers/activities`

Delete activity

DEPRECATED (sunset 2026-06-30): Deletes an activity. Use DELETE /api/customers/interactions instead.

Requires features: customers.activities.manage

**Tags:** Customers

**Requires authentication.**

**Features:** customers.activities.manage

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – Activity deleted

Content-Type: `application/json`

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

### Example

```bash
curl -X DELETE "https://crm.nf-hotel.pl/customers/activities" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/customers/activities`

List activitys

Returns a paginated collection of activitys scoped to the authenticated organization.

Requires features: customers.activities.view

**Tags:** Customers

**Requires authentication.**

**Features:** customers.activities.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| page | query | any | Optional |
| pageSize | query | any | Optional |
| entityId | query | any | Optional |
| dealId | query | any | Optional |
| activityType | query | any | Optional |
| sortField | query | any | Optional |
| sortDir | query | any | Optional |
| ids | query | any | Optional. Comma-separated list of record UUIDs to filter by (max 200). |

### Responses

**200** – Paginated activitys

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "activityType": "string",
      "subject": null,
      "body": null,
      "occurredAt": null,
      "createdAt": "string",
      "appearanceIcon": null,
      "appearanceColor": null,
      "entityId": null,
      "authorUserId": null,
      "authorName": null,
      "authorEmail": null,
      "dealId": null,
      "dealTitle": null,
      "customValues": null,
      "activityTypeLabel": null
    }
  ],
  "total": 1,
  "totalPages": 1
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/customers/activities?page=1&pageSize=50" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/customers/activities`

Create activity

DEPRECATED (sunset 2026-06-30): Creates a timeline activity. Use POST /api/customers/interactions instead.

Requires features: customers.activities.manage

**Tags:** Customers

**Requires authentication.**

**Features:** customers.activities.manage

### Request Body

Content-Type: `application/json`

```json
{
  "entityId": "00000000-0000-4000-8000-000000000000",
  "activityType": "string",
  "phoneNumber": null,
  "appearanceIcon": null,
  "appearanceColor": null
}
```

### Responses

**201** – Activity created

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/customers/activities" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"entityId\": \"00000000-0000-4000-8000-000000000000\",
  \"activityType\": \"string\",
  \"phoneNumber\": null,
  \"appearanceIcon\": null,
  \"appearanceColor\": null
}"
```

## PUT `/customers/activities`

Update activity

DEPRECATED (sunset 2026-06-30): Updates an activity. Use PUT /api/customers/interactions instead.

Requires features: customers.activities.manage

**Tags:** Customers

**Requires authentication.**

**Features:** customers.activities.manage

### Request Body

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "phoneNumber": null,
  "appearanceIcon": null,
  "appearanceColor": null
}
```

### Responses

**200** – Activity updated

Content-Type: `application/json`

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

### Example

```bash
curl -X PUT "https://crm.nf-hotel.pl/customers/activities" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\",
  \"phoneNumber\": null,
  \"appearanceIcon\": null,
  \"appearanceColor\": null
}"
```

## DELETE `/customers/addresses`

Delete address

Deletes an address by id. The identifier may be included in the body or query.

Requires features: customers.activities.manage

**Tags:** Customers

**Requires authentication.**

**Features:** customers.activities.manage

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – Address deleted

Content-Type: `application/json`

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

### Example

```bash
curl -X DELETE "https://crm.nf-hotel.pl/customers/addresses" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/customers/addresses`

List addresss

Returns a paginated collection of addresss scoped to the authenticated organization.

Requires features: customers.activities.view

**Tags:** Customers

**Requires authentication.**

**Features:** customers.activities.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| page | query | any | Optional |
| pageSize | query | any | Optional |
| entityId | query | any | Optional |
| id | query | any | Optional |
| sortField | query | any | Optional |
| sortDir | query | any | Optional |
| ids | query | any | Optional. Comma-separated list of record UUIDs to filter by (max 200). |

### Responses

**200** – Paginated addresss

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "entity_id": "00000000-0000-4000-8000-000000000000",
      "name": null,
      "purpose": null,
      "company_name": null,
      "address_line1": null,
      "address_line2": null,
      "building_number": null,
      "flat_number": null,
      "city": null,
      "region": null,
      "postal_code": null,
      "country": null,
      "latitude": null,
      "longitude": null,
      "is_primary": null,
      "organization_id": null,
      "tenant_id": null,
      "updated_at": null
    }
  ],
  "total": 1,
  "totalPages": 1
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/customers/addresses?page=1&pageSize=50" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/customers/addresses`

Create address

Creates a customer address record and associates it with the referenced entity.

Requires features: customers.activities.manage

**Tags:** Customers

**Requires authentication.**

**Features:** customers.activities.manage

### Request Body

Content-Type: `application/json`

```json
{
  "organizationId": "00000000-0000-4000-8000-000000000000",
  "tenantId": "00000000-0000-4000-8000-000000000000",
  "entityId": "00000000-0000-4000-8000-000000000000",
  "addressLine1": "string",
  "latitude": null,
  "longitude": null
}
```

### Responses

**201** – Address created

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/customers/addresses" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"organizationId\": \"00000000-0000-4000-8000-000000000000\",
  \"tenantId\": \"00000000-0000-4000-8000-000000000000\",
  \"entityId\": \"00000000-0000-4000-8000-000000000000\",
  \"addressLine1\": \"string\",
  \"latitude\": null,
  \"longitude\": null
}"
```

## PUT `/customers/addresses`

Update address

Updates fields on an existing customer address.

Requires features: customers.activities.manage

**Tags:** Customers

**Requires authentication.**

**Features:** customers.activities.manage

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – Address updated

Content-Type: `application/json`

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

### Example

```bash
curl -X PUT "https://crm.nf-hotel.pl/customers/addresses" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\",
  \"latitude\": null,
  \"longitude\": null
}"
```

## GET `/customers/assignable-staff`

DEPRECATED: use GET /api/staff/team-members/assignable instead.

Deprecated. Returns 308 Permanent Redirect to /api/staff/team-members/assignable preserving the query string. Will be removed no earlier than the next major release.

Requires features: customers.roles.view

**Tags:** Customers

**Requires authentication.**

**Features:** customers.roles.view

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

### Responses

**200** – Assignable staff members (only reachable by following the redirect).

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "teamMemberId": "00000000-0000-4000-8000-000000000000",
      "userId": "00000000-0000-4000-8000-000000000000",
      "displayName": "string",
      "email": null,
      "teamName": null,
      "user": null,
      "team": null
    }
  ],
  "total": 1,
  "totalPages": 1
}
```

**308** – Permanent redirect to /api/staff/team-members/assignable.

Content-Type: `application/json`

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

**400** – Invalid request

Content-Type: `application/json`

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

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/customers/assignable-staff?page=1&pageSize=24" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## DELETE `/customers/comments`

Delete comment

Deletes a comment identified by `id` supplied via body or query string.

Requires features: customers.activities.manage

**Tags:** Customers

**Requires authentication.**

**Features:** customers.activities.manage

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – Comment deleted

Content-Type: `application/json`

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

### Example

```bash
curl -X DELETE "https://crm.nf-hotel.pl/customers/comments" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/customers/comments`

List comments

Returns a paginated collection of comments scoped to the authenticated organization.

Requires features: customers.activities.view

**Tags:** Customers

**Requires authentication.**

**Features:** customers.activities.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| page | query | any | Optional |
| pageSize | query | any | Optional |
| entityId | query | any | Optional |
| dealId | query | any | Optional |
| sortField | query | any | Optional |
| sortDir | query | any | Optional |
| ids | query | any | Optional. Comma-separated list of record UUIDs to filter by (max 200). |

### Responses

**200** – Paginated comments

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "entity_id": null,
      "deal_id": null,
      "body": null,
      "author_user_id": null,
      "appearance_icon": null,
      "appearance_color": null,
      "organization_id": null,
      "tenant_id": null,
      "created_at": null,
      "updated_at": null
    }
  ],
  "total": 1,
  "totalPages": 1
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/customers/comments?page=1&pageSize=50" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/customers/comments`

Create comment

Adds a comment to a customer timeline.

Requires features: customers.activities.manage

**Tags:** Customers

**Requires authentication.**

**Features:** customers.activities.manage

### Request Body

Content-Type: `application/json`

```json
{
  "organizationId": "00000000-0000-4000-8000-000000000000",
  "tenantId": "00000000-0000-4000-8000-000000000000",
  "entityId": "00000000-0000-4000-8000-000000000000",
  "body": "string",
  "appearanceIcon": null,
  "appearanceColor": null
}
```

### Responses

**201** – Comment created

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/customers/comments" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"organizationId\": \"00000000-0000-4000-8000-000000000000\",
  \"tenantId\": \"00000000-0000-4000-8000-000000000000\",
  \"entityId\": \"00000000-0000-4000-8000-000000000000\",
  \"body\": \"string\",
  \"appearanceIcon\": null,
  \"appearanceColor\": null
}"
```

## PUT `/customers/comments`

Update comment

Updates an existing timeline comment.

Requires features: customers.activities.manage

**Tags:** Customers

**Requires authentication.**

**Features:** customers.activities.manage

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – Comment updated

Content-Type: `application/json`

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

### Example

```bash
curl -X PUT "https://crm.nf-hotel.pl/customers/comments" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\",
  \"appearanceIcon\": null,
  \"appearanceColor\": null
}"
```

## DELETE `/customers/companies`

Delete company

Deletes a company by id. The identifier can be provided via body or query.

Requires features: customers.companies.manage

**Tags:** Customers

**Requires authentication.**

**Features:** customers.companies.manage

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – Company deleted

Content-Type: `application/json`

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

**422** – Company has dependent records (people, deals, or direct staff); unlink or reassign before delete.

Content-Type: `application/json`

```json
{
  "error": "string",
  "code": "COMPANY_HAS_DEPENDENTS"
}
```

### Example

```bash
curl -X DELETE "https://crm.nf-hotel.pl/customers/companies" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/customers/companies`

List companies

Returns a paginated collection of companies scoped to the authenticated organization.

Requires features: customers.companies.view

**Tags:** Customers

**Requires authentication.**

**Features:** customers.companies.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| page | query | any | Optional |
| pageSize | query | any | Optional |
| search | query | any | Optional |
| email | query | any | Optional |
| emailStartsWith | query | any | Optional |
| emailContains | query | any | Optional |
| sortField | query | any | Optional |
| sortDir | query | any | Optional |
| status | query | any | Optional |
| lifecycleStage | query | any | Optional |
| source | query | any | Optional |
| hasEmail | query | any | Optional |
| hasPhone | query | any | Optional |
| hasNextInteraction | query | any | Optional |
| createdFrom | query | any | Optional |
| createdTo | query | any | Optional |
| id | query | any | Optional |
| tagIds | query | any | Optional |
| tagIdsEmpty | query | any | Optional |
| excludeIds | query | any | Optional |
| excludeLinkedPersonId | query | any | Optional |
| excludeLinkedCompanyId | query | any | Optional |
| excludeLinkedDealId | query | any | Optional |
| ids | query | any | Optional. Comma-separated list of record UUIDs to filter by (max 200). |

### Responses

**200** – Paginated companies

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "description": null,
      "owner_user_id": null,
      "primary_email": null,
      "primary_phone": null,
      "status": null,
      "lifecycle_stage": null,
      "source": null,
      "next_interaction_at": null,
      "next_interaction_name": null,
      "next_interaction_ref_id": null,
      "next_interaction_icon": null,
      "next_interaction_color": null,
      "organization_id": null,
      "tenant_id": null,
      "created_at": null
    }
  ],
  "total": 1,
  "totalPages": 1
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/customers/companies?page=1&pageSize=50" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/customers/companies`

Create company

Creates a company record and associated profile data.

Requires features: customers.companies.manage

**Tags:** Customers

**Requires authentication.**

**Features:** customers.companies.manage

### Request Body

Content-Type: `application/json`

```json
{
  "organizationId": "00000000-0000-4000-8000-000000000000",
  "tenantId": "00000000-0000-4000-8000-000000000000",
  "displayName": "string",
  "description": null,
  "primaryEmail": null,
  "primaryPhone": null,
  "nextInteraction": null,
  "brandName": null,
  "domain": null,
  "websiteUrl": null,
  "sizeBucket": null,
  "annualRevenue": null
}
```

### Responses

**201** – Company created

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/customers/companies" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"organizationId\": \"00000000-0000-4000-8000-000000000000\",
  \"tenantId\": \"00000000-0000-4000-8000-000000000000\",
  \"displayName\": \"string\",
  \"description\": null,
  \"primaryEmail\": null,
  \"primaryPhone\": null,
  \"nextInteraction\": null,
  \"brandName\": null,
  \"domain\": null,
  \"websiteUrl\": null,
  \"sizeBucket\": null,
  \"annualRevenue\": null
}"
```

## PUT `/customers/companies`

Update company

Updates company profile fields, tags, or custom attributes.

Requires features: customers.companies.manage

**Tags:** Customers

**Requires authentication.**

**Features:** customers.companies.manage

### Request Body

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "description": null,
  "primaryEmail": null,
  "primaryPhone": null,
  "nextInteraction": null,
  "brandName": null,
  "domain": null,
  "websiteUrl": null,
  "sizeBucket": null,
  "annualRevenue": null
}
```

### Responses

**200** – Company updated

Content-Type: `application/json`

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

### Example

```bash
curl -X PUT "https://crm.nf-hotel.pl/customers/companies" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\",
  \"description\": null,
  \"primaryEmail\": null,
  \"primaryPhone\": null,
  \"nextInteraction\": null,
  \"brandName\": null,
  \"domain\": null,
  \"websiteUrl\": null,
  \"sizeBucket\": null,
  \"annualRevenue\": null
}"
```

## GET `/customers/companies/{id}`

Fetch company with related data

Returns a company customer record with optional related resources such as addresses, comments, activities, interactions, deals, todos, and linked people.

Requires features: customers.companies.view

**Tags:** Customers

**Requires authentication.**

**Features:** customers.companies.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| id | path | any | Required |
| include | query | any | Optional. Comma-separated list of relations to include (addresses, comments, activities, interactions, deals, todos, people). |

### Responses

**200** – Company detail payload

Content-Type: `application/json`

```json
{
  "interactionMode": "canonical",
  "company": {
    "id": "00000000-0000-4000-8000-000000000000",
    "displayName": null,
    "description": null,
    "ownerUserId": null,
    "primaryEmail": null,
    "primaryPhone": null,
    "status": null,
    "lifecycleStage": null,
    "source": null,
    "nextInteractionAt": null,
    "nextInteractionName": null,
    "nextInteractionRefId": null,
    "nextInteractionIcon": null,
    "nextInteractionColor": null,
    "organizationId": null,
    "tenantId": null,
    "temperature": null,
    "renewalQuarter": null,
    "createdAt": "string",
    "updatedAt": "string"
  },
  "profile": null,
  "customFields": {},
  "tags": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "label": "string",
      "color": null
    }
  ],
  "addresses": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "name": null,
      "purpose": null,
      "addressLine1": null,
      "addressLine2": null,
      "buildingNumber": null,
      "flatNumber": null,
      "city": null,
      "region": null,
      "postalCode": null,
      "country": null,
      "latitude": null,
      "longitude": null,
      "isPrimary": null,
      "createdAt": "string"
    }
  ],
  "comments": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "body": null,
      "authorUserId": null,
      "authorName": null,
      "authorEmail": null,
      "dealId": null,
      "createdAt": "string",
      "appearanceIcon": null,
      "appearanceColor": null
    }
  ],
  "activities": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "activityType": "string",
      "subject": null,
      "body": null,
      "occurredAt": null,
      "dealId": null,
      "authorUserId": null,
      "authorName": null,
      "authorEmail": null,
      "createdAt": "string",
      "appearanceIcon": null,
      "appearanceColor": null
    }
  ],
  "interactions": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "entityId": null,
      "interactionType": "string",
      "title": null,
      "body": null,
      "status": "string",
      "scheduledAt": null,
      "occurredAt": null,
      "priority": null,
      "authorUserId": null,
      "ownerUserId": null,
      "dealId": null,
      "organizationId": null,
      "tenantId": null,
      "authorName": null,
      "authorEmail": null,
      "dealTitle": null,
      "customValues": null,
      "appearanceIcon": null,
      "appearanceColor": null,
      "source": null,
      "createdAt": "string",
      "updatedAt": "string"
    }
  ],
  "deals": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "title": null,
      "status": null,
      "pipelineStage": null,
      "valueAmount": null,
      "valueCurrency": null,
      "probability": null,
      "expectedCloseAt": null,
      "ownerUserId": null,
      "source": null,
      "createdAt": "string",
      "updatedAt": "string"
    }
  ],
  "todos": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "todoId": "00000000-0000-4000-8000-000000000000",
      "todoSource": "string",
      "createdAt": "string",
      "createdByUserId": null,
      "title": null,
      "isDone": null,
      "priority": null,
      "severity": null,
      "description": null,
      "dueAt": null,
      "todoOrganizationId": null,
      "customValues": null
    }
  ],
  "people": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "displayName": null,
      "primaryEmail": null,
      "primaryPhone": null,
      "status": null,
      "lifecycleStage": null,
      "jobTitle": null,
      "department": null,
      "createdAt": "string",
      "organizationId": null,
      "source": null,
      "temperature": null,
      "linkedAt": null
    }
  ],
  "viewer": {
    "userId": null,
    "name": null,
    "email": null
  }
}
```

**400** – Invalid identifier

Content-Type: `application/json`

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

**404** – Company not found

Content-Type: `application/json`

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

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/customers/companies/:id" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/customers/companies/{id}/people`

List linked people for a company

Requires features: customers.companies.view

**Tags:** Customers

**Requires authentication.**

**Features:** customers.companies.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| id | path | any | Required |
| page | query | any | Optional |
| pageSize | query | any | Optional |
| search | query | any | Optional |
| sort | query | any | Optional |

### Responses

**200** – Paginated linked people

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "displayName": "string",
      "primaryEmail": null,
      "primaryPhone": null,
      "status": null,
      "lifecycleStage": null,
      "jobTitle": null,
      "department": null,
      "createdAt": "string",
      "organizationId": null,
      "temperature": null,
      "source": null,
      "linkedAt": null
    }
  ],
  "total": 1,
  "page": 1,
  "pageSize": 1,
  "totalPages": 1
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/customers/companies/:id/people?page=1&pageSize=20&sort=name-asc" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## DELETE `/customers/companies/{id}/roles`

Remove a company role assignment

Requires features: customers.roles.manage

**Tags:** Customers

**Requires authentication.**

**Features:** customers.roles.manage

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

### Responses

**200** – Role deleted

Content-Type: `application/json`

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

**400** – Invalid request

Content-Type: `application/json`

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

**404** – Role not found

Content-Type: `application/json`

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

### Example

```bash
curl -X DELETE "https://crm.nf-hotel.pl/customers/companies/00000000-0000-4000-8000-000000000000/roles?roleId=00000000-0000-4000-8000-000000000000" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/customers/companies/{id}/roles`

List roles for a company

Requires features: customers.roles.view

**Tags:** Customers

**Requires authentication.**

**Features:** customers.roles.view

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

### Responses

**200** – Role assignments

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "entityType": "company",
      "entityId": "00000000-0000-4000-8000-000000000000",
      "userId": "00000000-0000-4000-8000-000000000000",
      "userName": null,
      "userEmail": null,
      "userPhone": null,
      "roleType": "string",
      "createdAt": "string",
      "updatedAt": "string"
    }
  ]
}
```

**400** – Invalid request

Content-Type: `application/json`

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

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/customers/companies/00000000-0000-4000-8000-000000000000/roles" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/customers/companies/{id}/roles`

Assign a role to a company

Requires features: customers.roles.manage

**Tags:** Customers

**Requires authentication.**

**Features:** customers.roles.manage

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

### Request Body

Content-Type: `application/json`

```json
{
  "roleType": "string",
  "userId": "00000000-0000-4000-8000-000000000000"
}
```

### Responses

**201** – Role created

Content-Type: `application/json`

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

**400** – Invalid request

Content-Type: `application/json`

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

**409** – Role already assigned

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/customers/companies/00000000-0000-4000-8000-000000000000/roles" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"roleType\": \"string\",
  \"userId\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## PUT `/customers/companies/{id}/roles`

Update a company role assignment

Requires features: customers.roles.manage

**Tags:** Customers

**Requires authentication.**

**Features:** customers.roles.manage

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

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – Role updated

Content-Type: `application/json`

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

**400** – Invalid request

Content-Type: `application/json`

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

**404** – Role not found

Content-Type: `application/json`

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

### Example

```bash
curl -X PUT "https://crm.nf-hotel.pl/customers/companies/00000000-0000-4000-8000-000000000000/roles?roleId=00000000-0000-4000-8000-000000000000" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"userId\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/customers/dashboard/widgets/customer-todos`

Fetch recent customer tasks

Returns the most recent customer tasks for display on dashboards, including legacy compatibility rows when needed.

Requires features: dashboards.view, customers.widgets.todos

**Tags:** Customers

**Requires authentication.**

**Features:** dashboards.view, customers.widgets.todos

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| limit | query | any | Optional |
| tenantId | query | any | Optional |
| organizationId | query | any | Optional |

### Responses

**200** – Widget payload

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "todoId": "00000000-0000-4000-8000-000000000000",
      "todoSource": "string",
      "todoTitle": null,
      "createdAt": "string",
      "organizationId": null,
      "entity": {
        "id": null,
        "displayName": null,
        "kind": null,
        "ownerUserId": null
      }
    }
  ]
}
```

**400** – Invalid query parameters

Content-Type: `application/json`

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

**500** – Widget failed to load

Content-Type: `application/json`

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

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/customers/dashboard/widgets/customer-todos?limit=5" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/customers/dashboard/widgets/new-customers`

Fetch recently created customers

Returns the latest customers created within the scoped tenant/organization for dashboard display.

Requires features: dashboards.view, customers.widgets.new-customers

**Tags:** Customers

**Requires authentication.**

**Features:** dashboards.view, customers.widgets.new-customers

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| limit | query | any | Optional |
| tenantId | query | any | Optional |
| organizationId | query | any | Optional |
| kind | query | any | Optional |

### Responses

**200** – Widget payload

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "displayName": null,
      "kind": null,
      "organizationId": null,
      "createdAt": "string",
      "ownerUserId": null
    }
  ]
}
```

**400** – Invalid query parameters

Content-Type: `application/json`

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

**500** – Widget failed to load

Content-Type: `application/json`

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

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/customers/dashboard/widgets/new-customers?limit=5" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/customers/dashboard/widgets/new-deals`

Fetch recently created deals

Returns the latest deals created within the scoped tenant/organization for dashboard display.

Requires features: dashboards.view, customers.widgets.new-deals

**Tags:** Customers

**Requires authentication.**

**Features:** dashboards.view, customers.widgets.new-deals

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| limit | query | any | Optional |
| tenantId | query | any | Optional |
| organizationId | query | any | Optional |

### Responses

**200** – Widget payload

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "title": null,
      "status": null,
      "organizationId": null,
      "createdAt": "string",
      "ownerUserId": null,
      "valueAmount": null,
      "valueCurrency": null
    }
  ]
}
```

**400** – Invalid query parameters

Content-Type: `application/json`

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

**500** – Widget failed to load

Content-Type: `application/json`

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

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/customers/dashboard/widgets/new-deals?limit=5" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/customers/dashboard/widgets/next-interactions`

Fetch upcoming customer interactions

Lists upcoming (or optionally past) customer interaction reminders ordered by interaction date.

Requires features: dashboards.view, customers.widgets.next-interactions

**Tags:** Customers

**Requires authentication.**

**Features:** dashboards.view, customers.widgets.next-interactions

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| limit | query | any | Optional |
| tenantId | query | any | Optional |
| organizationId | query | any | Optional |
| includePast | query | any | Optional |

### Responses

**200** – Widget payload

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "displayName": null,
      "kind": null,
      "organizationId": null,
      "nextInteractionAt": null,
      "nextInteractionName": null,
      "nextInteractionIcon": null,
      "nextInteractionColor": null,
      "ownerUserId": null
    }
  ],
  "now": "string"
}
```

**400** – Invalid query parameters

Content-Type: `application/json`

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

**500** – Widget failed to load

Content-Type: `application/json`

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

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/customers/dashboard/widgets/next-interactions?limit=5" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## DELETE `/customers/deals`

Delete deal

Deletes a deal by `id`. The identifier may be provided in the body or query parameters.

Requires features: customers.deals.manage

**Tags:** Customers

**Requires authentication.**

**Features:** customers.deals.manage

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – Deal deleted

Content-Type: `application/json`

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

### Example

```bash
curl -X DELETE "https://crm.nf-hotel.pl/customers/deals" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/customers/deals`

List deals

Returns a paginated collection of deals scoped to the authenticated organization.

Requires features: customers.deals.view

**Tags:** Customers

**Requires authentication.**

**Features:** customers.deals.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| page | query | any | Optional |
| pageSize | query | any | Optional |
| id | query | any | Optional |
| search | query | any | Optional |
| status | query | any | Optional |
| pipelineStage | query | any | Optional |
| pipelineId | query | any | Optional |
| pipelineStageId | query | any | Optional |
| ownerUserId | query | any | Optional |
| expectedCloseAtFrom | query | any | Optional |
| expectedCloseAtTo | query | any | Optional |
| isStuck | query | any | Optional |
| isOverdue | query | any | Optional |
| needsAttention | query | any | Optional |
| valueCurrency | query | any | Optional |
| sortField | query | any | Optional |
| sortDir | query | any | Optional |
| personEntityId | query | any | Optional. Deprecated; use personId |
| companyEntityId | query | any | Optional. Deprecated; use companyId |
| personId | query | any | Optional |
| companyId | query | any | Optional |
| ids | query | any | Optional. Comma-separated list of record UUIDs to filter by (max 200). |

### Responses

**200** – Paginated deals

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "title": null,
      "description": null,
      "status": null,
      "pipeline_stage": null,
      "pipeline_id": null,
      "pipeline_stage_id": null,
      "value_amount": null,
      "value_currency": null,
      "probability": null,
      "expected_close_at": null,
      "owner_user_id": null,
      "source": null,
      "organization_id": null,
      "tenant_id": null,
      "created_at": null,
      "updated_at": null,
      "organizationId": null,
      "tenantId": null
    }
  ],
  "total": 1,
  "totalPages": 1
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/customers/deals?page=1&pageSize=50" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/customers/deals`

Create deal

Creates a sales deal, optionally associating people and companies.

Requires features: customers.deals.manage

**Tags:** Customers

**Requires authentication.**

**Features:** customers.deals.manage

### Request Body

Content-Type: `application/json`

```json
{
  "organizationId": "00000000-0000-4000-8000-000000000000",
  "tenantId": "00000000-0000-4000-8000-000000000000",
  "title": "string",
  "ownerUserId": null
}
```

### Responses

**201** – Deal created

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/customers/deals" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"organizationId\": \"00000000-0000-4000-8000-000000000000\",
  \"tenantId\": \"00000000-0000-4000-8000-000000000000\",
  \"title\": \"string\",
  \"ownerUserId\": null
}"
```

## PUT `/customers/deals`

Update deal

Updates pipeline position, metadata, or associations for an existing deal.

Requires features: customers.deals.manage

**Tags:** Customers

**Requires authentication.**

**Features:** customers.deals.manage

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – Deal updated

Content-Type: `application/json`

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

### Example

```bash
curl -X PUT "https://crm.nf-hotel.pl/customers/deals" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\",
  \"ownerUserId\": null
}"
```

## GET `/customers/deals/{id}`

Fetch deal with associations and pipeline context

Returns a deal with linked people, companies, closure fields, optional pipeline history, custom fields, and viewer context.

Requires features: customers.deals.view

**Tags:** Customers

**Requires authentication.**

**Features:** customers.deals.view

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

### Responses

**200** – Deal detail payload

Content-Type: `application/json`

```json
{
  "deal": {
    "id": "00000000-0000-4000-8000-000000000000",
    "title": null,
    "description": null,
    "status": null,
    "pipelineStage": null,
    "pipelineId": null,
    "pipelineStageId": null,
    "valueAmount": null,
    "valueCurrency": null,
    "probability": null,
    "expectedCloseAt": null,
    "ownerUserId": null,
    "source": null,
    "closureOutcome": null,
    "lossReasonId": null,
    "lossNotes": null,
    "organizationId": null,
    "tenantId": null,
    "createdAt": "string",
    "updatedAt": "string"
  },
  "people": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "label": "string",
      "subtitle": null,
      "kind": "person"
    }
  ],
  "companies": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "label": "string",
      "subtitle": null,
      "kind": "company"
    }
  ],
  "customFields": {},
  "viewer": {
    "userId": null,
    "name": null,
    "email": null
  },
  "pipelineStages": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "label": "string",
      "order": 1,
      "color": null,
      "icon": null
    }
  ],
  "stageTransitions": [
    {
      "stageId": "00000000-0000-4000-8000-000000000000",
      "stageLabel": "string",
      "stageOrder": 1,
      "transitionedAt": "string"
    }
  ],
  "owner": null
}
```

**404** – Deal not found

Content-Type: `application/json`

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

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/customers/deals/:id" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/customers/deals/{id}/companies`

List linked companies for a deal

Requires features: customers.deals.view

**Tags:** Customers

**Requires authentication.**

**Features:** customers.deals.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| id | path | any | Required |
| page | query | any | Optional |
| pageSize | query | any | Optional |
| search | query | any | Optional |
| sort | query | any | Optional |

### Responses

**200** – Paginated linked companies

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "label": "string",
      "subtitle": null,
      "kind": "company",
      "linkedAt": "string"
    }
  ],
  "total": 1,
  "page": 1,
  "pageSize": 1,
  "totalPages": 1
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/customers/deals/:id/companies?page=1&pageSize=20&sort=label-asc" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/customers/deals/{id}/people`

List linked people for a deal

Requires features: customers.deals.view

**Tags:** Customers

**Requires authentication.**

**Features:** customers.deals.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| id | path | any | Required |
| page | query | any | Optional |
| pageSize | query | any | Optional |
| search | query | any | Optional |
| sort | query | any | Optional |

### Responses

**200** – Paginated linked people

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "label": "string",
      "subtitle": null,
      "kind": "person",
      "linkedAt": "string"
    }
  ],
  "total": 1,
  "page": 1,
  "pageSize": 1,
  "totalPages": 1
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/customers/deals/:id/people?page=1&pageSize=20&sort=label-asc" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/customers/deals/{id}/stats`

Fetch analytics for a closed deal

Returns week-to-date closure counts, sales cycle length, quarter ranking, and loss reason context for a closed deal.

Requires features: customers.deals.view

**Tags:** Customers

**Requires authentication.**

**Features:** customers.deals.view

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

### Responses

**200** – Deal closure stats payload

Content-Type: `application/json`

```json
{
  "dealValue": null,
  "dealCurrency": null,
  "closureOutcome": "won",
  "closedAt": "string",
  "pipelineName": null,
  "dealsClosedThisPeriod": 1,
  "salesCycleDays": null,
  "dealRankInQuarter": null,
  "lossReason": null
}
```

**400** – Deal is not closed

Content-Type: `application/json`

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

**404** – Deal not found

Content-Type: `application/json`

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

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/customers/deals/:id/stats" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/customers/deals/aggregate`

Per-stage counts and currency totals for kanban lane headers

Returns per-stage counts and totals for deals, with values converted to the tenant base currency where rates are available. Used to power kanban lane headers without loading every deal.

Requires features: customers.deals.view

**Tags:** Customers

**Requires authentication.**

**Features:** customers.deals.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| pipelineId | query | any | Optional |
| search | query | any | Optional |
| status | query | any | Optional |
| ownerUserId | query | any | Optional |
| personId | query | any | Optional |
| companyId | query | any | Optional |
| isStuck | query | any | Optional |
| isOverdue | query | any | Optional |
| expectedCloseAtFrom | query | any | Optional |
| expectedCloseAtTo | query | any | Optional |

### Responses

**200** – Per-stage aggregate payload

Content-Type: `application/json`

```json
{
  "baseCurrencyCode": null,
  "perStage": [
    {
      "stageId": "string",
      "count": 1,
      "openCount": 1,
      "totalInBaseCurrency": 1,
      "byCurrency": [
        {
          "currency": "string",
          "total": 1,
          "count": 1
        }
      ],
      "convertedAll": true,
      "missingRateCurrencies": [
        "string"
      ]
    }
  ]
}
```

**400** – Invalid query parameters

Content-Type: `application/json`

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

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/customers/deals/aggregate" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/customers/deals/bulk-update-owner`

Bulk reassign deal owner

Queues a background job that reassigns the listed deals to a new owner (or clears the owner when null).

Requires features: customers.deals.manage

**Tags:** Customer Relationship Management

**Requires authentication.**

**Features:** customers.deals.manage

### Responses

**201** – Success response

Content-Type: `application/json`

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/customers/deals/bulk-update-owner" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/customers/deals/bulk-update-stage`

Bulk update deal pipeline stage

Queues a background job that moves the listed deals to the same pipeline stage. Returns a progress job id to poll for completion.

Requires features: customers.deals.manage

**Tags:** Customer Relationship Management

**Requires authentication.**

**Features:** customers.deals.manage

### Responses

**201** – Success response

Content-Type: `application/json`

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/customers/deals/bulk-update-stage" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/customers/deals/map`

Paginated deals that have a resolvable map location

Returns a page of deals that have a coordinate-bearing linked company/person address, each enriched with one resolved location (company primary first, then earliest created; person addresses as fallback). Deals with no coordinate-bearing address are excluded entirely, so every item carries a non-null location in normal operation; the schema keeps location nullable only for the rare case where the address is deleted between the located-deal resolution and the page fetch.

Requires features: customers.deals.view, customers.activities.view

**Tags:** Customers

**Requires authentication.**

**Features:** customers.deals.view, customers.activities.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| page | query | any | Optional |
| pageSize | query | any | Optional |
| id | query | any | Optional |
| search | query | any | Optional |
| status | query | any | Optional |
| pipelineStage | query | any | Optional |
| pipelineId | query | any | Optional |
| pipelineStageId | query | any | Optional |
| ownerUserId | query | any | Optional |
| expectedCloseAtFrom | query | any | Optional |
| expectedCloseAtTo | query | any | Optional |
| isStuck | query | any | Optional |
| isOverdue | query | any | Optional |
| needsAttention | query | any | Optional |
| valueCurrency | query | any | Optional |
| sortField | query | any | Optional |
| sortDir | query | any | Optional |
| personEntityId | query | any | Optional. Deprecated; use personId |
| companyEntityId | query | any | Optional. Deprecated; use companyId |
| personId | query | any | Optional |
| companyId | query | any | Optional |

### Responses

**200** – Paged located deals

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "title": null,
      "status": null,
      "pipelineId": null,
      "pipelineStageId": null,
      "pipelineStage": null,
      "valueAmount": null,
      "valueCurrency": null,
      "probability": null,
      "expectedCloseAt": null,
      "ownerUserId": null,
      "updatedAt": null,
      "companies": [
        {
          "id": "00000000-0000-4000-8000-000000000000",
          "label": null
        }
      ],
      "people": [
        {
          "id": "00000000-0000-4000-8000-000000000000",
          "label": null
        }
      ],
      "location": null
    }
  ],
  "total": 1,
  "totalPages": 1
}
```

**400** – Invalid query parameters

Content-Type: `application/json`

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

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/customers/deals/map?page=1&pageSize=100" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/customers/deals/summary`

Pipeline KPI metrics with period-over-period deltas for the deals list

Returns the four list-level KPI cards (pipeline value, active deals, won this quarter, win rate) with quarter-over-quarter deltas, per-stage open-pipeline breakdown, top owners, and a 6-month win-rate series. Values are converted to the tenant base currency where rates are available; partial conversions are disclosed via convertedAll/missingRateCurrencies.

Requires features: customers.deals.view

**Tags:** Customers

**Requires authentication.**

**Features:** customers.deals.view

### Responses

**200** – Deals KPI summary payload

Content-Type: `application/json`

```json
{
  "baseCurrencyCode": null,
  "convertedAll": true,
  "missingRateCurrencies": [
    "string"
  ],
  "pipelineValue": {
    "value": 1,
    "delta": {
      "value": 1,
      "direction": "up"
    },
    "stages": [
      {
        "stage": null,
        "count": 1,
        "value": 1
      }
    ]
  },
  "activeDeals": {
    "value": 1,
    "delta": {
      "value": 1,
      "direction": "up"
    },
    "ownersCount": 1,
    "needAttention": 1,
    "owners": [
      {
        "id": "string",
        "count": 1
      }
    ],
    "ownersOverflow": 1
  },
  "wonThisQuarter": {
    "value": 1,
    "delta": {
      "value": 1,
      "direction": "up"
    },
    "dealsClosed": 1,
    "avgDeal": 1
  },
  "winRate": {
    "value": 1,
    "deltaPp": 1,
    "direction": "up",
    "previousValue": 1,
    "series": [
      {
        "period": "string",
        "rate": 1
      }
    ]
  }
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/customers/deals/summary" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/customers/dictionaries/{kind}`

List dictionary entries

Returns dictionary entries for the requested kind within the currently selected organization.

Requires features: customers.people.view

**Tags:** Customers

**Requires authentication.**

**Features:** customers.people.view

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

### Responses

**200** – Dictionary entries

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "value": "string",
      "label": null,
      "color": null,
      "icon": null,
      "organizationId": null
    }
  ]
}
```

**400** – Failed to resolve dictionary context

Content-Type: `application/json`

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

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/customers/dictionaries/:kind" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/customers/dictionaries/{kind}`

Create or override dictionary entry

Creates a dictionary entry (or updates the existing entry for the same value) within the current organization scope.

Requires features: customers.settings.manage

**Tags:** Customers

**Requires authentication.**

**Features:** customers.settings.manage

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

### Request Body

Content-Type: `application/json`

```json
{
  "value": "string"
}
```

### Responses

**200** – Dictionary entry updated

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "value": "string",
  "label": null,
  "color": null,
  "icon": null,
  "organizationId": null
}
```

**201** – Dictionary entry created

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "value": "string",
  "label": null,
  "color": null,
  "icon": null,
  "organizationId": null
}
```

**400** – Invalid payload

Content-Type: `application/json`

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

**409** – Duplicate value conflict

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/customers/dictionaries/:kind" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"value\": \"string\"
}"
```

## DELETE `/customers/dictionaries/{kind}/{id}`

Delete dictionary entry

Removes a customer dictionary entry by identifier.

Requires features: customers.settings.manage

**Tags:** Customers

**Requires authentication.**

**Features:** customers.settings.manage

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

### Responses

**200** – Entry deleted

Content-Type: `application/json`

```json
{
  "success": true
}
```

**404** – Entry not found

Content-Type: `application/json`

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

**409** – Entry is in use and cannot be deleted

Content-Type: `application/json`

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

### Example

```bash
curl -X DELETE "https://crm.nf-hotel.pl/customers/dictionaries/:kind/:id" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## PATCH `/customers/dictionaries/{kind}/{id}`

Update dictionary entry

Updates value, label, color, or icon for an existing customer dictionary entry.

Requires features: customers.settings.manage

**Tags:** Customers

**Requires authentication.**

**Features:** customers.settings.manage

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

### Request Body

Content-Type: `application/json`

```json
{}
```

### Responses

**200** – Updated dictionary entry

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "value": "string",
  "label": null,
  "color": null,
  "icon": null,
  "organizationId": null
}
```

**400** – Invalid payload

Content-Type: `application/json`

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

**404** – Entry not found

Content-Type: `application/json`

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

**409** – Duplicate value conflict

Content-Type: `application/json`

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

### Example

```bash
curl -X PATCH "https://crm.nf-hotel.pl/customers/dictionaries/:kind/:id" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{}"
```

## GET `/customers/dictionaries/currency`

Resolve currency dictionary

Returns the active currency dictionary for the current organization scope, falling back to shared entries when required.

Requires features: customers.people.view

**Tags:** Customers

**Requires authentication.**

**Features:** customers.people.view

### Responses

**200** – Currency dictionary entries

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "entries": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "value": "string",
      "label": null
    }
  ]
}
```

**404** – Currency dictionary missing

Content-Type: `application/json`

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

**500** – Unexpected error

Content-Type: `application/json`

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

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/customers/dictionaries/currency" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/customers/dictionaries/kind-settings`

List kind settings

Returns selection mode and visibility settings for each dictionary kind.

Requires features: customers.people.view

**Tags:** Customers

**Requires authentication.**

**Features:** customers.people.view

### Responses

**200** – Kind settings

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "kind": "string",
      "selectionMode": "single",
      "visibleInTags": true,
      "sortOrder": 1
    }
  ]
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/customers/dictionaries/kind-settings" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## PATCH `/customers/dictionaries/kind-settings`

Update kind setting

Creates or updates settings for a specific dictionary kind.

Requires features: customers.settings.manage

**Tags:** Customers

**Requires authentication.**

**Features:** customers.settings.manage

### Request Body

Content-Type: `application/json`

```json
{
  "kind": "string"
}
```

### Responses

**200** – Setting updated

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "kind": "string",
  "selectionMode": "single",
  "visibleInTags": true,
  "sortOrder": 1
}
```

**400** – Invalid payload

Content-Type: `application/json`

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

### Example

```bash
curl -X PATCH "https://crm.nf-hotel.pl/customers/dictionaries/kind-settings" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"kind\": \"string\"
}"
```

## DELETE `/customers/interactions`

Delete interaction

Soft-deletes an interaction identified by `id`. Accepts id via body or query string.

Requires features: customers.interactions.manage

**Tags:** Customers

**Requires authentication.**

**Features:** customers.interactions.manage

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – Interaction deleted

Content-Type: `application/json`

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

### Example

```bash
curl -X DELETE "https://crm.nf-hotel.pl/customers/interactions" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/customers/interactions`

List interactions

Returns a paginated collection of interactions scoped to the authenticated organization.

Requires features: customers.interactions.view

**Tags:** Customers

**Requires authentication.**

**Features:** customers.interactions.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| limit | query | any | Optional |
| cursor | query | any | Optional |
| entityId | query | any | Optional |
| dealId | query | any | Optional |
| status | query | any | Optional |
| interactionType | query | any | Optional |
| type | query | any | Optional |
| excludeInteractionType | query | any | Optional |
| search | query | any | Optional |
| from | query | any | Optional |
| to | query | any | Optional |
| pinned | query | any | Optional |
| sortField | query | any | Optional |
| sortDir | query | any | Optional |
| ids | query | any | Optional. Comma-separated list of record UUIDs to filter by (max 200). |

### Responses

**200** – Paginated interactions

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "entityId": null,
      "dealId": null,
      "interactionType": "string",
      "title": null,
      "body": null,
      "status": "string",
      "scheduledAt": null,
      "occurredAt": null,
      "priority": null,
      "authorUserId": null,
      "ownerUserId": null,
      "appearanceIcon": null,
      "appearanceColor": null,
      "source": null,
      "duration": null,
      "durationMinutes": null,
      "location": null,
      "allDay": null,
      "recurrenceRule": null,
      "recurrenceEnd": null,
      "participants": null,
      "reminderMinutes": null,
      "visibility": null,
      "linkedEntities": null,
      "guestPermissions": null,
      "organizationId": null,
      "tenantId": null,
      "createdAt": null,
      "updatedAt": null,
      "authorName": null,
      "authorEmail": null,
      "dealTitle": null,
      "customValues": null
    }
  ]
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/customers/interactions?limit=25" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/customers/interactions`

Create interaction

Creates a new interaction linked to a customer entity or deal.

Requires features: customers.interactions.manage

**Tags:** Customers

**Requires authentication.**

**Features:** customers.interactions.manage

### Request Body

Content-Type: `application/json`

No example available for this content type.

### Responses

**201** – Interaction created

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/customers/interactions" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## PUT `/customers/interactions`

Update interaction

Updates fields for an existing interaction.

Requires features: customers.interactions.manage

**Tags:** Customers

**Requires authentication.**

**Features:** customers.interactions.manage

### Request Body

Content-Type: `application/json`

No example available for this content type.

### Responses

**200** – Interaction updated

Content-Type: `application/json`

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

### Example

```bash
curl -X PUT "https://crm.nf-hotel.pl/customers/interactions" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## PATCH `/customers/interactions/{id}/visibility`

Flip an email interaction visibility (private ↔ shared)

Requires features: customers.email.compose

**Tags:** Customers, Email

**Requires authentication.**

**Features:** customers.email.compose

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

### Responses

**200** – Updated

Content-Type: `application/json`

**400** – Invalid id

Content-Type: `application/json`

**404** – Email not found or not visible to caller

Content-Type: `application/json`

**422** – Invalid body

Content-Type: `application/json`

### Example

```bash
curl -X PATCH "https://crm.nf-hotel.pl/customers/interactions/:id/visibility" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/customers/interactions/cancel`

Cancel an interaction

Marks an interaction as canceled.

Requires features: customers.interactions.manage

**Tags:** Customers

**Requires authentication.**

**Features:** customers.interactions.manage

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – Interaction canceled

Content-Type: `application/json`

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

**400** – Validation failed

Content-Type: `application/json`

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

**404** – Interaction not found

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/customers/interactions/cancel" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## POST `/customers/interactions/complete`

Complete an interaction

Marks an interaction as done and sets occurredAt to current time (or a provided timestamp).

Requires features: customers.interactions.manage

**Tags:** Customers

**Requires authentication.**

**Features:** customers.interactions.manage

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – Interaction completed

Content-Type: `application/json`

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

**400** – Validation failed

Content-Type: `application/json`

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

**404** – Interaction not found

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/customers/interactions/complete" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/customers/interactions/conflicts`

Detect scheduling conflicts

Checks for overlapping planned interactions within the requested time window.

Requires features: customers.interactions.view

**Tags:** Customers

**Requires authentication.**

**Features:** customers.interactions.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| date | query | any | Required |
| startTime | query | any | Required |
| duration | query | any | Required |
| excludeId | query | any | Optional |
| userId | query | any | Optional |
| timezoneOffsetMinutes | query | any | Optional |

### Responses

**200** – Conflict detection result

Content-Type: `application/json`

```json
{
  "ok": true,
  "result": {
    "hasConflicts": true,
    "conflicts": [
      {
        "id": "string",
        "title": null,
        "startTime": "string",
        "endTime": "string",
        "type": "string"
      }
    ]
  }
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/customers/interactions/conflicts?date=string&startTime=string&duration=1" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/customers/interactions/counts`

Get interaction counts by type

Returns per-type interaction counts scoped to an entity.

Requires features: customers.interactions.view

**Tags:** Customers

**Requires authentication.**

**Features:** customers.interactions.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| entityId | query | any | Required |
| status | query | any | Optional |

### Responses

**200** – Counts by interaction type

Content-Type: `application/json`

```json
{
  "ok": true,
  "result": {
    "call": 1,
    "email": 1,
    "meeting": 1,
    "note": 1,
    "task": 1,
    "total": 1
  }
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/customers/interactions/counts?entityId=00000000-0000-4000-8000-000000000000" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/customers/interactions/tasks`

List customertasks

Returns a paginated collection of customertasks scoped to the authenticated organization.

Requires features: customers.interactions.view

**Tags:** Customers

**Requires authentication.**

**Features:** customers.interactions.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| page | query | any | Optional |
| pageSize | query | any | Optional |
| search | query | any | Optional |
| all | query | any | Optional |
| entityId | query | any | Optional |
| ids | query | any | Optional. Comma-separated list of record UUIDs to filter by (max 200). |

### Responses

**200** – Paginated customertasks

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "string",
      "todoId": "string",
      "todoSource": "string",
      "todoTitle": null,
      "todoIsDone": null,
      "todoPriority": null,
      "todoSeverity": null,
      "todoDescription": null,
      "todoDueAt": null,
      "todoCustomValues": null,
      "todoOrganizationId": null,
      "organizationId": "string",
      "tenantId": "string",
      "createdAt": "string",
      "externalHref": null,
      "customer": {
        "id": null,
        "displayName": null,
        "kind": null
      }
    }
  ],
  "total": 1,
  "totalPages": 1
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/customers/interactions/tasks?page=1&pageSize=50" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/customers/labels`

List labels

Returns labels for the current user within the selected organization. Optionally includes assignment status for a specific entity.

Requires features: customers.people.view

**Tags:** Customers

**Requires authentication.**

**Features:** customers.people.view

### Responses

**200** – Labels list

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "slug": "string",
      "label": "string"
    }
  ],
  "assignedIds": [
    "00000000-0000-4000-8000-000000000000"
  ],
  "total": 1,
  "page": 1,
  "pageSize": 1,
  "totalPages": 1
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/customers/labels" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/customers/labels`

Create label

Creates a new label scoped to the current user and selected organization.

Requires features: customers.people.manage

**Tags:** Customers

**Requires authentication.**

**Features:** customers.people.manage

### Request Body

Content-Type: `application/json`

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

### Responses

**201** – Label created

Content-Type: `application/json`

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

**409** – Duplicate slug

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/customers/labels" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"label\": \"string\"
}"
```

## POST `/customers/labels/assign`

Assign label

**Tags:** Customers

**Requires authentication.**

### Request Body

Content-Type: `application/json`

```json
{
  "labelId": "00000000-0000-4000-8000-000000000000",
  "entityId": "00000000-0000-4000-8000-000000000000"
}
```

### Responses

**200** – Already assigned

Content-Type: `application/json`

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

**201** – Assigned

Content-Type: `application/json`

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

**404** – Label or entity not found

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/customers/labels/assign" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"labelId\": \"00000000-0000-4000-8000-000000000000\",
  \"entityId\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## POST `/customers/labels/unassign`

Unassign label

**Tags:** Customers

**Requires authentication.**

### Request Body

Content-Type: `application/json`

```json
{
  "labelId": "00000000-0000-4000-8000-000000000000",
  "entityId": "00000000-0000-4000-8000-000000000000"
}
```

### Responses

**200** – Unassigned

Content-Type: `application/json`

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

**404** – Label or entity not found

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/customers/labels/unassign" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"labelId\": \"00000000-0000-4000-8000-000000000000\",
  \"entityId\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## DELETE `/customers/people`

Delete person

Deletes a person by id. Request body or query may provide the identifier.

Requires features: customers.people.manage

**Tags:** Customers

**Requires authentication.**

**Features:** customers.people.manage

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – Person deleted

Content-Type: `application/json`

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

**422** – Person has dependent records (e.g. linked deals); unlink or reassign before delete.

Content-Type: `application/json`

```json
{
  "error": "string",
  "code": "PERSON_HAS_DEPENDENTS"
}
```

### Example

```bash
curl -X DELETE "https://crm.nf-hotel.pl/customers/people" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/customers/people`

List people

Returns a paginated collection of people scoped to the authenticated organization.

Requires features: customers.people.view

**Tags:** Customers

**Requires authentication.**

**Features:** customers.people.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| page | query | any | Optional |
| pageSize | query | any | Optional |
| search | query | any | Optional |
| email | query | any | Optional |
| emailStartsWith | query | any | Optional |
| emailContains | query | any | Optional |
| status | query | any | Optional |
| lifecycleStage | query | any | Optional |
| source | query | any | Optional |
| hasEmail | query | any | Optional |
| hasPhone | query | any | Optional |
| hasNextInteraction | query | any | Optional |
| createdFrom | query | any | Optional |
| createdTo | query | any | Optional |
| sortField | query | any | Optional |
| sortDir | query | any | Optional |
| id | query | any | Optional |
| tagIds | query | any | Optional |
| tagIdsEmpty | query | any | Optional |
| excludeIds | query | any | Optional |
| excludeLinkedCompanyId | query | any | Optional |
| excludeLinkedDealId | query | any | Optional |
| ids | query | any | Optional. Comma-separated list of record UUIDs to filter by (max 200). |

### Responses

**200** – Paginated people

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "description": null,
      "owner_user_id": null,
      "primary_email": null,
      "primary_phone": null,
      "status": null,
      "lifecycle_stage": null,
      "source": null,
      "next_interaction_at": null,
      "next_interaction_name": null,
      "next_interaction_ref_id": null,
      "next_interaction_icon": null,
      "next_interaction_color": null,
      "organization_id": null,
      "tenant_id": null,
      "created_at": null
    }
  ],
  "total": 1,
  "totalPages": 1
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/customers/people?page=1&pageSize=50" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/customers/people`

Create person

Creates a person contact using scoped organization and tenant identifiers.

Requires features: customers.people.manage

**Tags:** Customers

**Requires authentication.**

**Features:** customers.people.manage

### Request Body

Content-Type: `application/json`

```json
{
  "organizationId": "00000000-0000-4000-8000-000000000000",
  "tenantId": "00000000-0000-4000-8000-000000000000",
  "description": null,
  "primaryEmail": null,
  "primaryPhone": null,
  "nextInteraction": null,
  "firstName": "string",
  "lastName": "string",
  "linkedInUrl": null,
  "twitterUrl": null,
  "companyEntityId": null
}
```

### Responses

**201** – Person created

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/customers/people" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"organizationId\": \"00000000-0000-4000-8000-000000000000\",
  \"tenantId\": \"00000000-0000-4000-8000-000000000000\",
  \"description\": null,
  \"primaryEmail\": null,
  \"primaryPhone\": null,
  \"nextInteraction\": null,
  \"firstName\": \"string\",
  \"lastName\": \"string\",
  \"linkedInUrl\": null,
  \"twitterUrl\": null,
  \"companyEntityId\": null
}"
```

## PUT `/customers/people`

Update person

Updates contact details or custom fields for a person.

Requires features: customers.people.manage

**Tags:** Customers

**Requires authentication.**

**Features:** customers.people.manage

### Request Body

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "description": null,
  "primaryEmail": null,
  "primaryPhone": null,
  "nextInteraction": null,
  "linkedInUrl": null,
  "twitterUrl": null,
  "companyEntityId": null
}
```

### Responses

**200** – Person updated

Content-Type: `application/json`

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

### Example

```bash
curl -X PUT "https://crm.nf-hotel.pl/customers/people" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\",
  \"description\": null,
  \"primaryEmail\": null,
  \"primaryPhone\": null,
  \"nextInteraction\": null,
  \"linkedInUrl\": null,
  \"twitterUrl\": null,
  \"companyEntityId\": null
}"
```

## GET `/customers/people/{id}`

Fetch person with related data

Returns a person customer record with optional related resources such as addresses, comments, activities, interactions, deals, and todos.

Requires features: customers.people.view

**Tags:** Customers

**Requires authentication.**

**Features:** customers.people.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| id | path | any | Required |
| include | query | any | Optional. Comma-separated list of relations to include (addresses, comments, activities, interactions, deals, todos). |

### Responses

**200** – Person detail payload

Content-Type: `application/json`

```json
{
  "interactionMode": "canonical",
  "person": {
    "id": "00000000-0000-4000-8000-000000000000",
    "displayName": null,
    "description": null,
    "ownerUserId": null,
    "primaryEmail": null,
    "primaryPhone": null,
    "status": null,
    "lifecycleStage": null,
    "source": null,
    "nextInteractionAt": null,
    "nextInteractionName": null,
    "nextInteractionRefId": null,
    "nextInteractionIcon": null,
    "nextInteractionColor": null,
    "organizationId": null,
    "tenantId": null,
    "createdAt": "string",
    "updatedAt": "string"
  },
  "profile": null,
  "customFields": {},
  "tags": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "label": "string",
      "color": null
    }
  ],
  "addresses": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "name": null,
      "purpose": null,
      "addressLine1": null,
      "addressLine2": null,
      "buildingNumber": null,
      "flatNumber": null,
      "city": null,
      "region": null,
      "postalCode": null,
      "country": null,
      "latitude": null,
      "longitude": null,
      "isPrimary": null,
      "createdAt": "string"
    }
  ],
  "comments": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "body": null,
      "authorUserId": null,
      "authorName": null,
      "authorEmail": null,
      "dealId": null,
      "createdAt": "string",
      "appearanceIcon": null,
      "appearanceColor": null
    }
  ],
  "activities": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "activityType": "string",
      "subject": null,
      "body": null,
      "occurredAt": null,
      "dealId": null,
      "authorUserId": null,
      "authorName": null,
      "authorEmail": null,
      "createdAt": "string",
      "appearanceIcon": null,
      "appearanceColor": null
    }
  ],
  "interactions": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "entityId": null,
      "interactionType": "string",
      "title": null,
      "body": null,
      "status": "string",
      "scheduledAt": null,
      "occurredAt": null,
      "priority": null,
      "authorUserId": null,
      "ownerUserId": null,
      "dealId": null,
      "organizationId": null,
      "tenantId": null,
      "authorName": null,
      "authorEmail": null,
      "dealTitle": null,
      "customValues": null,
      "appearanceIcon": null,
      "appearanceColor": null,
      "source": null,
      "createdAt": "string",
      "updatedAt": "string"
    }
  ],
  "deals": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "title": null,
      "status": null,
      "pipelineStage": null,
      "valueAmount": null,
      "valueCurrency": null,
      "probability": null,
      "expectedCloseAt": null,
      "ownerUserId": null,
      "source": null,
      "closureOutcome": null,
      "lossReasonId": null,
      "lossNotes": null,
      "createdAt": "string",
      "updatedAt": "string"
    }
  ],
  "todos": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "todoId": "00000000-0000-4000-8000-000000000000",
      "todoSource": "string",
      "createdAt": "string",
      "createdByUserId": null,
      "title": null,
      "isDone": null,
      "priority": null,
      "severity": null,
      "description": null,
      "dueAt": null,
      "todoOrganizationId": null,
      "customValues": null
    }
  ],
  "isPrimary": true,
  "companies": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "displayName": "string",
      "isPrimary": true
    }
  ],
  "viewer": {
    "userId": null,
    "name": null,
    "email": null
  }
}
```

**400** – Invalid identifier

Content-Type: `application/json`

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

**404** – Person not found

Content-Type: `application/json`

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

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/customers/people/:id" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/customers/people/{id}/companies`

List linked companies for a person

Requires features: customers.people.view

**Tags:** Customers

**Requires authentication.**

**Features:** customers.people.view

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

### Responses

**200** – Linked company rows

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "companyId": "00000000-0000-4000-8000-000000000000",
      "displayName": "string",
      "isPrimary": true
    }
  ]
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/customers/people/:id/companies" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/customers/people/{id}/companies`

Link a company to a person

Requires features: customers.people.manage

**Tags:** Customers

**Requires authentication.**

**Features:** customers.people.manage

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

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – Linked company row

Content-Type: `application/json`

```json
{
  "ok": true,
  "result": {
    "id": "00000000-0000-4000-8000-000000000000",
    "companyId": "00000000-0000-4000-8000-000000000000",
    "displayName": "string",
    "isPrimary": true
  }
}
```

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/customers/people/:id/companies" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"companyId\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## DELETE `/customers/people/{id}/companies/{linkId}`

Remove a linked company from a person

Requires features: customers.people.manage

**Tags:** Customers

**Requires authentication.**

**Features:** customers.people.manage

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

### Responses

**200** – Deletion result

Content-Type: `application/json`

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

### Example

```bash
curl -X DELETE "https://crm.nf-hotel.pl/customers/people/:id/companies/:linkId" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## PATCH `/customers/people/{id}/companies/{linkId}`

Update a linked company for a person

Requires features: customers.people.manage

**Tags:** Customers

**Requires authentication.**

**Features:** customers.people.manage

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

### Request Body

Content-Type: `application/json`

```json
{}
```

### Responses

**200** – Updated company link

Content-Type: `application/json`

```json
{
  "ok": true,
  "result": null
}
```

### Example

```bash
curl -X PATCH "https://crm.nf-hotel.pl/customers/people/:id/companies/:linkId" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{}"
```

## GET `/customers/people/{id}/companies/enriched`

Get enriched company data for a person's linked companies

Requires features: customers.people.view

**Tags:** Customers

**Requires authentication.**

**Features:** customers.people.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| id | path | any | Required |
| page | query | any | Optional |
| pageSize | query | any | Optional |
| search | query | any | Optional |
| sort | query | any | Optional |

### Responses

**200** – Enriched company rows with profile, billing, tags, deals and more

Content-Type: `application/json`

```json
{
  "items": [
    {
      "linkId": "00000000-0000-4000-8000-000000000000",
      "companyId": "00000000-0000-4000-8000-000000000000",
      "displayName": "string",
      "isPrimary": true,
      "subtitle": null,
      "profile": null,
      "billing": null,
      "primaryAddress": null,
      "tags": [
        {
          "id": "00000000-0000-4000-8000-000000000000",
          "label": "string",
          "color": null
        }
      ],
      "roles": [
        {
          "id": "00000000-0000-4000-8000-000000000000",
          "roleValue": "string"
        }
      ],
      "activeDeal": null,
      "lastContactAt": null,
      "clv": null,
      "status": null,
      "lifecycleStage": null,
      "temperature": null,
      "renewalQuarter": null
    }
  ],
  "total": 1,
  "page": 1,
  "pageSize": 1,
  "totalPages": 1
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/customers/people/:id/companies/enriched?page=1&pageSize=20&sort=name-asc" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/customers/people/{id}/email-threads`

List a Person's email threads (Gmail-style conversation grouping)

Requires features: customers.people.view

**Tags:** Customers, Email

**Requires authentication.**

**Features:** customers.people.view

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

### Responses

**200** – Email threads for the person, grouped by conversation

Content-Type: `application/json`

**400** – Invalid person id

Content-Type: `application/json`

**404** – Person not found

Content-Type: `application/json`

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/customers/people/:id/email-threads" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/customers/people/{id}/emails`

Compose + send an email anchored to a Person

Requires features: customers.email.compose

**Tags:** Customers, Email

**Requires authentication.**

**Features:** customers.email.compose

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

### Responses

**200** – Email queued for send

Content-Type: `application/json`

**400** – Invalid person id

Content-Type: `application/json`

**404** – Person or channel not found

Content-Type: `application/json`

**409** – Channel not connected

Content-Type: `application/json`

**422** – Invalid request body

Content-Type: `application/json`

**500** – Send failed

Content-Type: `application/json`

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/customers/people/:id/emails" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## DELETE `/customers/people/{id}/roles`

Remove a person role assignment

Requires features: customers.roles.manage

**Tags:** Customers

**Requires authentication.**

**Features:** customers.roles.manage

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

### Responses

**200** – Role deleted

Content-Type: `application/json`

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

**400** – Invalid request

Content-Type: `application/json`

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

**404** – Role not found

Content-Type: `application/json`

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

### Example

```bash
curl -X DELETE "https://crm.nf-hotel.pl/customers/people/00000000-0000-4000-8000-000000000000/roles?roleId=00000000-0000-4000-8000-000000000000" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/customers/people/{id}/roles`

List roles for a person

Requires features: customers.roles.view

**Tags:** Customers

**Requires authentication.**

**Features:** customers.roles.view

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

### Responses

**200** – Role assignments

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "entityType": "company",
      "entityId": "00000000-0000-4000-8000-000000000000",
      "userId": "00000000-0000-4000-8000-000000000000",
      "userName": null,
      "userEmail": null,
      "userPhone": null,
      "roleType": "string",
      "createdAt": "string",
      "updatedAt": "string"
    }
  ]
}
```

**400** – Invalid request

Content-Type: `application/json`

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

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/customers/people/00000000-0000-4000-8000-000000000000/roles" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/customers/people/{id}/roles`

Assign a role to a person

Requires features: customers.roles.manage

**Tags:** Customers

**Requires authentication.**

**Features:** customers.roles.manage

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

### Request Body

Content-Type: `application/json`

```json
{
  "roleType": "string",
  "userId": "00000000-0000-4000-8000-000000000000"
}
```

### Responses

**201** – Role created

Content-Type: `application/json`

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

**400** – Invalid request

Content-Type: `application/json`

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

**409** – Role already assigned

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/customers/people/00000000-0000-4000-8000-000000000000/roles" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"roleType\": \"string\",
  \"userId\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## PUT `/customers/people/{id}/roles`

Update a person role assignment

Requires features: customers.roles.manage

**Tags:** Customers

**Requires authentication.**

**Features:** customers.roles.manage

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

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – Role updated

Content-Type: `application/json`

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

**400** – Invalid request

Content-Type: `application/json`

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

**404** – Role not found

Content-Type: `application/json`

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

### Example

```bash
curl -X PUT "https://crm.nf-hotel.pl/customers/people/00000000-0000-4000-8000-000000000000/roles?roleId=00000000-0000-4000-8000-000000000000" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"userId\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/customers/people/check-phone`

Find person by phone digits

Performs an exact digits comparison (stripping non-numeric characters) to determine whether a customer contact matches the provided phone fragment.

Requires features: customers.people.view

**Tags:** Customers

**Requires authentication.**

**Features:** customers.people.view

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

### Responses

**200** – Matching contact (if any)

Content-Type: `application/json`

```json
{
  "match": null
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/customers/people/check-phone" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## DELETE `/customers/pipeline-stages`

Delete pipeline stage

Deletes a pipeline stage. Returns 409 if active deals use this stage.

Requires features: customers.pipelines.manage

**Tags:** Customers

**Requires authentication.**

**Features:** customers.pipelines.manage

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – Stage deleted

Content-Type: `application/json`

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

**404** – Stage not found

Content-Type: `application/json`

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

**409** – Stage has active deals

Content-Type: `application/json`

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

### Example

```bash
curl -X DELETE "https://crm.nf-hotel.pl/customers/pipeline-stages" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/customers/pipeline-stages`

List pipeline stages

Returns pipeline stages for the authenticated organization, optionally filtered by pipelineId.

Requires features: customers.pipelines.view

**Tags:** Customers

**Requires authentication.**

**Features:** customers.pipelines.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| pipelineId | query | any | Optional |

### Responses

**200** – Stage list

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "pipelineId": "00000000-0000-4000-8000-000000000000",
      "label": "string",
      "order": 1,
      "color": null,
      "icon": null,
      "organizationId": "00000000-0000-4000-8000-000000000000",
      "tenantId": "00000000-0000-4000-8000-000000000000",
      "createdAt": "2025-01-01T00:00:00.000Z",
      "updatedAt": "2025-01-01T00:00:00.000Z"
    }
  ],
  "total": 1
}
```

**400** – Invalid request

Content-Type: `application/json`

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

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/customers/pipeline-stages" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/customers/pipeline-stages`

Create pipeline stage

Creates a new pipeline stage.

Requires features: customers.pipelines.manage

**Tags:** Customers

**Requires authentication.**

**Features:** customers.pipelines.manage

### Request Body

Content-Type: `application/json`

```json
{
  "organizationId": "00000000-0000-4000-8000-000000000000",
  "tenantId": "00000000-0000-4000-8000-000000000000",
  "pipelineId": "00000000-0000-4000-8000-000000000000",
  "label": "string",
  "color": null,
  "icon": null
}
```

### Responses

**201** – Stage created

Content-Type: `application/json`

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

**400** – Validation failed

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/customers/pipeline-stages" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"organizationId\": \"00000000-0000-4000-8000-000000000000\",
  \"tenantId\": \"00000000-0000-4000-8000-000000000000\",
  \"pipelineId\": \"00000000-0000-4000-8000-000000000000\",
  \"label\": \"string\",
  \"color\": null,
  \"icon\": null
}"
```

## PUT `/customers/pipeline-stages`

Update pipeline stage

Updates an existing pipeline stage.

Requires features: customers.pipelines.manage

**Tags:** Customers

**Requires authentication.**

**Features:** customers.pipelines.manage

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – Stage updated

Content-Type: `application/json`

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

**400** – Validation failed

Content-Type: `application/json`

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

**404** – Stage not found

Content-Type: `application/json`

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

### Example

```bash
curl -X PUT "https://crm.nf-hotel.pl/customers/pipeline-stages" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\",
  \"color\": null,
  \"icon\": null
}"
```

## POST `/customers/pipeline-stages/reorder`

Reorder pipeline stages

Updates the order of pipeline stages in bulk.

Requires features: customers.pipelines.manage

**Tags:** Customers

**Requires authentication.**

**Features:** customers.pipelines.manage

### Request Body

Content-Type: `application/json`

```json
{
  "organizationId": "00000000-0000-4000-8000-000000000000",
  "tenantId": "00000000-0000-4000-8000-000000000000",
  "stages": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "order": 1
    }
  ]
}
```

### Responses

**200** – Stages reordered

Content-Type: `application/json`

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

**400** – Validation failed

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/customers/pipeline-stages/reorder" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"organizationId\": \"00000000-0000-4000-8000-000000000000\",
  \"tenantId\": \"00000000-0000-4000-8000-000000000000\",
  \"stages\": [
    {
      \"id\": \"00000000-0000-4000-8000-000000000000\",
      \"order\": 1
    }
  ]
}"
```

## DELETE `/customers/pipelines`

Delete pipeline

Deletes a pipeline. Returns 409 if active deals exist.

Requires features: customers.pipelines.manage

**Tags:** Customers

**Requires authentication.**

**Features:** customers.pipelines.manage

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – Pipeline deleted

Content-Type: `application/json`

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

**404** – Pipeline not found

Content-Type: `application/json`

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

**409** – Pipeline has active deals

Content-Type: `application/json`

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

### Example

```bash
curl -X DELETE "https://crm.nf-hotel.pl/customers/pipelines" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/customers/pipelines`

List pipelines

Returns a list of pipelines scoped to the authenticated organization.

Requires features: customers.pipelines.view

**Tags:** Customers

**Requires authentication.**

**Features:** customers.pipelines.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| isDefault | query | any | Optional |

### Responses

**200** – Pipeline list

Content-Type: `application/json`

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

**400** – Invalid request

Content-Type: `application/json`

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

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/customers/pipelines" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/customers/pipelines`

Create pipeline

Creates a new pipeline within the authenticated organization.

Requires features: customers.pipelines.manage

**Tags:** Customers

**Requires authentication.**

**Features:** customers.pipelines.manage

### Request Body

Content-Type: `application/json`

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

### Responses

**201** – Pipeline created

Content-Type: `application/json`

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

**400** – Validation failed

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/customers/pipelines" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"organizationId\": \"00000000-0000-4000-8000-000000000000\",
  \"tenantId\": \"00000000-0000-4000-8000-000000000000\",
  \"name\": \"string\"
}"
```

## PUT `/customers/pipelines`

Update pipeline

Updates an existing pipeline.

Requires features: customers.pipelines.manage

**Tags:** Customers

**Requires authentication.**

**Features:** customers.pipelines.manage

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – Pipeline updated

Content-Type: `application/json`

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

**400** – Validation failed

Content-Type: `application/json`

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

**404** – Pipeline not found

Content-Type: `application/json`

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

### Example

```bash
curl -X PUT "https://crm.nf-hotel.pl/customers/pipelines" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/customers/settings/address-format`

Retrieve address format

Returns the current address formatting preference for the selected organization.

Requires features: customers.settings.manage

**Tags:** Customers

**Requires authentication.**

**Features:** customers.settings.manage

### Responses

**200** – Current address format

Content-Type: `application/json`

```json
{
  "addressFormat": "string"
}
```

**400** – Organization context missing

Content-Type: `application/json`

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

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/customers/settings/address-format" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## PUT `/customers/settings/address-format`

Update address format

Updates the address format preference for the selected organization.

Requires features: customers.settings.manage

**Tags:** Customers

**Requires authentication.**

**Features:** customers.settings.manage

### Request Body

Content-Type: `application/json`

```json
{
  "organizationId": "00000000-0000-4000-8000-000000000000",
  "tenantId": "00000000-0000-4000-8000-000000000000",
  "addressFormat": "line_first"
}
```

### Responses

**200** – Updated address format

Content-Type: `application/json`

```json
{
  "addressFormat": "string"
}
```

**400** – Invalid payload or organization context

Content-Type: `application/json`

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

### Example

```bash
curl -X PUT "https://crm.nf-hotel.pl/customers/settings/address-format" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"organizationId\": \"00000000-0000-4000-8000-000000000000\",
  \"tenantId\": \"00000000-0000-4000-8000-000000000000\",
  \"addressFormat\": \"line_first\"
}"
```

## GET `/customers/settings/dictionary-sort-modes`

Retrieve dictionary sort modes

Returns entry sort preferences for customer dictionaries in the selected organization.

Requires features: customers.settings.manage

**Tags:** Customers

**Requires authentication.**

**Features:** customers.settings.manage

### Responses

**200** – Current dictionary sort modes

Content-Type: `application/json`

```json
{
  "dictionarySortModes": {
    "key": "label_asc"
  }
}
```

**400** – Organization context missing

Content-Type: `application/json`

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

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/customers/settings/dictionary-sort-modes" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## PATCH `/customers/settings/dictionary-sort-modes`

Update dictionary sort modes

Updates entry sort preferences for customer dictionaries in the selected organization.

Requires features: customers.settings.manage

**Tags:** Customers

**Requires authentication.**

**Features:** customers.settings.manage

### Request Body

Content-Type: `application/json`

```json
{
  "dictionarySortModes": {
    "key": "label_asc"
  }
}
```

### Responses

**200** – Updated dictionary sort modes

Content-Type: `application/json`

```json
{
  "dictionarySortModes": {
    "key": "label_asc"
  }
}
```

**400** – Invalid payload or organization context

Content-Type: `application/json`

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

### Example

```bash
curl -X PATCH "https://crm.nf-hotel.pl/customers/settings/dictionary-sort-modes" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"dictionarySortModes\": {
    \"key\": \"label_asc\"
  }
}"
```

## GET `/customers/settings/stuck-threshold`

Retrieve stuck-threshold days

Returns the current stuck-deal threshold (in days) for the selected organization.

Requires features: customers.deals.manage

**Tags:** Customers

**Requires authentication.**

**Features:** customers.deals.manage

### Responses

**200** – Current threshold

Content-Type: `application/json`

```json
{
  "stuckThresholdDays": 1
}
```

**400** – Organization context missing

Content-Type: `application/json`

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

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/customers/settings/stuck-threshold" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## PUT `/customers/settings/stuck-threshold`

Update stuck-threshold days

Updates the stuck-deal threshold for the selected organization.

Requires features: customers.deals.manage

**Tags:** Customers

**Requires authentication.**

**Features:** customers.deals.manage

### Request Body

Content-Type: `application/json`

```json
{
  "organizationId": "00000000-0000-4000-8000-000000000000",
  "tenantId": "00000000-0000-4000-8000-000000000000",
  "stuckThresholdDays": 1
}
```

### Responses

**200** – Updated threshold

Content-Type: `application/json`

```json
{
  "stuckThresholdDays": 1
}
```

**400** – Invalid payload

Content-Type: `application/json`

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

### Example

```bash
curl -X PUT "https://crm.nf-hotel.pl/customers/settings/stuck-threshold" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"organizationId\": \"00000000-0000-4000-8000-000000000000\",
  \"tenantId\": \"00000000-0000-4000-8000-000000000000\",
  \"stuckThresholdDays\": 1
}"
```

## DELETE `/customers/tags`

Delete tag

Deletes a tag identified by `id`. The identifier may be provided via body or query string.

Requires features: customers.activities.manage

**Tags:** Customers

**Requires authentication.**

**Features:** customers.activities.manage

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – Tag deleted

Content-Type: `application/json`

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

### Example

```bash
curl -X DELETE "https://crm.nf-hotel.pl/customers/tags" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/customers/tags`

List tags

Returns a paginated collection of tags scoped to the authenticated organization.

Requires features: customers.activities.view

**Tags:** Customers

**Requires authentication.**

**Features:** customers.activities.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| page | query | any | Optional |
| pageSize | query | any | Optional |
| search | query | any | Optional |
| sortField | query | any | Optional |
| sortDir | query | any | Optional |
| ids | query | any | Optional. Comma-separated list of record UUIDs to filter by (max 200). |

### Responses

**200** – Paginated tags

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "slug": "string",
      "label": "string",
      "color": null,
      "description": null,
      "organization_id": null,
      "tenant_id": null,
      "updated_at": null
    }
  ],
  "total": 1,
  "totalPages": 1
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/customers/tags?page=1&pageSize=100" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/customers/tags`

Create tag

Creates a tag scoped to the current tenant and organization.

Requires features: customers.activities.manage

**Tags:** Customers

**Requires authentication.**

**Features:** customers.activities.manage

### Request Body

Content-Type: `application/json`

```json
{
  "organizationId": "00000000-0000-4000-8000-000000000000",
  "tenantId": "00000000-0000-4000-8000-000000000000",
  "slug": "string",
  "label": "string"
}
```

### Responses

**201** – Tag created

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/customers/tags" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"organizationId\": \"00000000-0000-4000-8000-000000000000\",
  \"tenantId\": \"00000000-0000-4000-8000-000000000000\",
  \"slug\": \"string\",
  \"label\": \"string\"
}"
```

## PUT `/customers/tags`

Update tag

Updates label, color, or description for an existing tag.

Requires features: customers.activities.manage

**Tags:** Customers

**Requires authentication.**

**Features:** customers.activities.manage

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – Tag updated

Content-Type: `application/json`

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

### Example

```bash
curl -X PUT "https://crm.nf-hotel.pl/customers/tags" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## POST `/customers/tags/assign`

Assign tag to customer entity

Links a tag to a customer entity within the validated tenant / organization scope.

Requires features: customers.activities.manage

**Tags:** Customers

**Requires authentication.**

**Features:** customers.activities.manage

### Request Body

Content-Type: `application/json`

```json
{
  "organizationId": "00000000-0000-4000-8000-000000000000",
  "tenantId": "00000000-0000-4000-8000-000000000000",
  "tagId": "00000000-0000-4000-8000-000000000000",
  "entityId": "00000000-0000-4000-8000-000000000000"
}
```

### Responses

**201** – Tag assigned to customer

Content-Type: `application/json`

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

**400** – Validation or assignment failed

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/customers/tags/assign" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"organizationId\": \"00000000-0000-4000-8000-000000000000\",
  \"tenantId\": \"00000000-0000-4000-8000-000000000000\",
  \"tagId\": \"00000000-0000-4000-8000-000000000000\",
  \"entityId\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## POST `/customers/tags/unassign`

Remove tag from customer entity

Detaches a tag from a customer entity within the validated tenant / organization scope.

Requires features: customers.activities.manage

**Tags:** Customers

**Requires authentication.**

**Features:** customers.activities.manage

### Request Body

Content-Type: `application/json`

```json
{
  "organizationId": "00000000-0000-4000-8000-000000000000",
  "tenantId": "00000000-0000-4000-8000-000000000000",
  "tagId": "00000000-0000-4000-8000-000000000000",
  "entityId": "00000000-0000-4000-8000-000000000000"
}
```

### Responses

**200** – Tag unassigned from customer

Content-Type: `application/json`

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

**400** – Validation or unassignment failed

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/customers/tags/unassign" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"organizationId\": \"00000000-0000-4000-8000-000000000000\",
  \"tenantId\": \"00000000-0000-4000-8000-000000000000\",
  \"tagId\": \"00000000-0000-4000-8000-000000000000\",
  \"entityId\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## DELETE `/customers/todos`

Delete customertodo

DEPRECATED (sunset 2026-06-30): Deletes a customer task. Use DELETE /api/customers/interactions instead.

Requires features: customers.interactions.manage

**Tags:** Customers

**Requires authentication.**

**Features:** customers.interactions.manage

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – CustomerTodo deleted

Content-Type: `application/json`

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

### Example

```bash
curl -X DELETE "https://crm.nf-hotel.pl/customers/todos" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/customers/todos`

List customertodos

Returns a paginated collection of customertodos scoped to the authenticated organization.

Requires features: customers.view

**Tags:** Customers

**Requires authentication.**

**Features:** customers.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| page | query | any | Optional |
| pageSize | query | any | Optional |
| search | query | any | Optional |
| all | query | any | Optional |
| entityId | query | any | Optional |
| ids | query | any | Optional. Comma-separated list of record UUIDs to filter by (max 200). |

### Responses

**200** – Paginated customertodos

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "string",
      "todoId": "string",
      "todoSource": "string",
      "todoTitle": null,
      "todoIsDone": null,
      "todoPriority": null,
      "todoSeverity": null,
      "todoDescription": null,
      "todoDueAt": null,
      "todoCustomValues": null,
      "todoOrganizationId": null,
      "todoUpdatedAt": null,
      "organizationId": "string",
      "tenantId": "string",
      "createdAt": "string",
      "externalHref": null,
      "customer": {
        "id": null,
        "displayName": null,
        "kind": null
      }
    }
  ],
  "total": 1,
  "totalPages": 1
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/customers/todos?page=1&pageSize=50" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/customers/todos`

Create customertodo

DEPRECATED (sunset 2026-06-30): Creates a customer task. Use POST /api/customers/interactions instead.

Requires features: customers.interactions.manage

**Tags:** Customers

**Requires authentication.**

**Features:** customers.interactions.manage

### Request Body

Content-Type: `application/json`

```json
{
  "entityId": "00000000-0000-4000-8000-000000000000",
  "title": "string",
  "todoSource": "customers:interaction"
}
```

### Responses

**201** – CustomerTodo created

Content-Type: `application/json`

```json
{
  "linkId": null,
  "todoId": null
}
```

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/customers/todos" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"entityId\": \"00000000-0000-4000-8000-000000000000\",
  \"title\": \"string\",
  \"todoSource\": \"customers:interaction\"
}"
```

## PUT `/customers/todos`

Update customertodo

DEPRECATED (sunset 2026-06-30): Updates a customer task. Use PUT /api/customers/interactions instead.

Requires features: customers.interactions.manage

**Tags:** Customers

**Requires authentication.**

**Features:** customers.interactions.manage

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – CustomerTodo updated

Content-Type: `application/json`

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

### Example

```bash
curl -X PUT "https://crm.nf-hotel.pl/customers/todos" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## 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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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 `/dictionaries`

List dictionaries

Returns dictionaries accessible to the current organization, optionally including inactive records.

Requires features: dictionaries.view

**Tags:** Dictionaries

**Requires authentication.**

**Features:** dictionaries.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| includeInactive | query | any | Optional |

### Responses

**200** – Dictionary collection.

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "key": "string",
      "name": "string",
      "description": null,
      "isSystem": true,
      "isActive": true,
      "managerVisibility": null,
      "organizationId": null,
      "createdAt": "string",
      "updatedAt": null
    }
  ]
}
```

**500** – Failed to load dictionaries

Content-Type: `application/json`

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

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/dictionaries" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/dictionaries`

Create dictionary

Registers a dictionary scoped to the current organization.

Requires features: dictionaries.manage

**Tags:** Dictionaries

**Requires authentication.**

**Features:** dictionaries.manage

### Request Body

Content-Type: `application/json`

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

### Responses

**201** – Dictionary created.

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "key": "string",
  "name": "string",
  "description": null,
  "isSystem": true,
  "isActive": true,
  "managerVisibility": null,
  "organizationId": null,
  "createdAt": "string",
  "updatedAt": null
}
```

**400** – Validation failed

Content-Type: `application/json`

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

**409** – Dictionary key already exists

Content-Type: `application/json`

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

**500** – Failed to create dictionary

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/dictionaries" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"key\": \"string\",
  \"name\": \"string\"
}"
```

## DELETE `/dictionaries/{dictionaryId}`

Delete dictionary

Soft deletes the dictionary unless it is the protected currency dictionary.

Requires features: dictionaries.manage

**Tags:** Dictionaries

**Requires authentication.**

**Features:** dictionaries.manage

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

### Responses

**200** – Dictionary archived.

Content-Type: `application/json`

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

**400** – Protected dictionary cannot be deleted

Content-Type: `application/json`

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

**404** – Dictionary not found

Content-Type: `application/json`

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

**500** – Failed to delete dictionary

Content-Type: `application/json`

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

### Example

```bash
curl -X DELETE "https://crm.nf-hotel.pl/dictionaries/00000000-0000-4000-8000-000000000000" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/dictionaries/{dictionaryId}`

Get dictionary

Returns details for the specified dictionary, including inheritance flags.

Requires features: dictionaries.view

**Tags:** Dictionaries

**Requires authentication.**

**Features:** dictionaries.view

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

### Responses

**200** – Dictionary details.

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "key": "string",
  "name": "string",
  "description": null,
  "isSystem": true,
  "isActive": true,
  "managerVisibility": null,
  "organizationId": null,
  "createdAt": "string",
  "updatedAt": null
}
```

**400** – Invalid parameters

Content-Type: `application/json`

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

**404** – Dictionary not found

Content-Type: `application/json`

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

**500** – Failed to load dictionary

Content-Type: `application/json`

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

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/dictionaries/00000000-0000-4000-8000-000000000000" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## PATCH `/dictionaries/{dictionaryId}`

Update dictionary

Updates mutable attributes of the dictionary. Currency dictionaries are protected from modification.

Requires features: dictionaries.manage

**Tags:** Dictionaries

**Requires authentication.**

**Features:** dictionaries.manage

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

### Request Body

Content-Type: `application/json`

```json
{}
```

### Responses

**200** – Dictionary updated.

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "key": "string",
  "name": "string",
  "description": null,
  "isSystem": true,
  "isActive": true,
  "managerVisibility": null,
  "organizationId": null,
  "createdAt": "string",
  "updatedAt": null
}
```

**400** – Validation failed or protected dictionary

Content-Type: `application/json`

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

**404** – Dictionary not found

Content-Type: `application/json`

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

**409** – Dictionary key already exists

Content-Type: `application/json`

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

**500** – Failed to update dictionary

Content-Type: `application/json`

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

### Example

```bash
curl -X PATCH "https://crm.nf-hotel.pl/dictionaries/00000000-0000-4000-8000-000000000000" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{}"
```

## GET `/dictionaries/{dictionaryId}/entries`

List dictionary entries

Returns entries for the specified dictionary ordered by its configured entry sort mode.

Requires features: dictionaries.view

**Tags:** Dictionaries

**Requires authentication.**

**Features:** dictionaries.view

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

### Responses

**200** – Dictionary entries.

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "value": "string",
      "label": "string",
      "color": null,
      "icon": null,
      "position": 1,
      "isDefault": true,
      "createdAt": "string",
      "updatedAt": null
    }
  ]
}
```

**400** – Invalid parameters

Content-Type: `application/json`

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

**404** – Dictionary not found

Content-Type: `application/json`

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

**500** – Failed to load dictionary entries

Content-Type: `application/json`

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

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/dictionaries/00000000-0000-4000-8000-000000000000/entries" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/dictionaries/{dictionaryId}/entries`

Create dictionary entry

Creates a new entry in the specified dictionary.

Requires features: dictionaries.manage

**Tags:** Dictionaries

**Requires authentication.**

**Features:** dictionaries.manage

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

### Request Body

Content-Type: `application/json`

```json
{
  "value": "string",
  "color": null,
  "icon": null
}
```

### Responses

**201** – Dictionary entry created.

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "value": "string",
  "label": "string",
  "color": null,
  "icon": null,
  "position": 1,
  "isDefault": true,
  "createdAt": "string",
  "updatedAt": null
}
```

**400** – Validation failed

Content-Type: `application/json`

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

**404** – Dictionary not found

Content-Type: `application/json`

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

**500** – Failed to create dictionary entry

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/dictionaries/00000000-0000-4000-8000-000000000000/entries" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"value\": \"string\",
  \"color\": null,
  \"icon\": null
}"
```

## DELETE `/dictionaries/{dictionaryId}/entries/{entryId}`

Delete dictionary entry

Deletes the specified dictionary entry via the command bus.

Requires features: dictionaries.manage

**Tags:** Dictionaries

**Requires authentication.**

**Features:** dictionaries.manage

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| dictionaryId | path | any | Required |
| entryId | path | any | Required |

### Responses

**200** – Entry deleted.

Content-Type: `application/json`

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

**400** – Validation failed

Content-Type: `application/json`

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

**404** – Dictionary or entry not found

Content-Type: `application/json`

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

**500** – Failed to delete entry

Content-Type: `application/json`

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

### Example

```bash
curl -X DELETE "https://crm.nf-hotel.pl/dictionaries/00000000-0000-4000-8000-000000000000/entries/00000000-0000-4000-8000-000000000000" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## PATCH `/dictionaries/{dictionaryId}/entries/{entryId}`

Update dictionary entry

Updates the specified dictionary entry using the command bus pipeline.

Requires features: dictionaries.manage

**Tags:** Dictionaries

**Requires authentication.**

**Features:** dictionaries.manage

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| dictionaryId | path | any | Required |
| entryId | path | any | Required |

### Request Body

Content-Type: `application/json`

```json
{
  "color": null,
  "icon": null
}
```

### Responses

**200** – Dictionary entry updated.

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "value": "string",
  "label": "string",
  "color": null,
  "icon": null,
  "position": 1,
  "isDefault": true,
  "createdAt": "string",
  "updatedAt": null
}
```

**400** – Validation failed

Content-Type: `application/json`

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

**404** – Dictionary or entry not found

Content-Type: `application/json`

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

**500** – Failed to update entry

Content-Type: `application/json`

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

### Example

```bash
curl -X PATCH "https://crm.nf-hotel.pl/dictionaries/00000000-0000-4000-8000-000000000000/entries/00000000-0000-4000-8000-000000000000" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"color\": null,
  \"icon\": null
}"
```

## POST `/dictionaries/{dictionaryId}/entries/reorder`

Reorder dictionary entries

Updates the position of dictionary entries for drag-and-drop reordering.

Requires features: dictionaries.manage

**Tags:** Dictionaries

**Requires authentication.**

**Features:** dictionaries.manage

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

### Request Body

Content-Type: `application/json`

```json
{
  "entries": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "position": 1
    }
  ]
}
```

### Responses

**200** – Entries reordered.

Content-Type: `application/json`

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

**400** – Validation failed

Content-Type: `application/json`

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

**404** – Dictionary not found

Content-Type: `application/json`

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

**500** – Failed to reorder entries

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/dictionaries/00000000-0000-4000-8000-000000000000/entries/reorder" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"entries\": [
    {
      \"id\": \"00000000-0000-4000-8000-000000000000\",
      \"position\": 1
    }
  ]
}"
```

## POST `/dictionaries/{dictionaryId}/entries/set-default`

Set default dictionary entry

Marks the specified entry as the default for this dictionary, clearing any previous default.

Requires features: dictionaries.manage

**Tags:** Dictionaries

**Requires authentication.**

**Features:** dictionaries.manage

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

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – Default entry set.

Content-Type: `application/json`

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

**400** – Validation failed

Content-Type: `application/json`

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

**404** – Dictionary or entry not found

Content-Type: `application/json`

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

**500** – Failed to set default entry

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/dictionaries/00000000-0000-4000-8000-000000000000/entries/set-default" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"entryId\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## 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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/events/stream" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## DELETE `/favorites/items`

Reset the calling user's favorites back to the defaults

**Tags:** Favorites

**Requires authentication.**

### Responses

**200** – Reset (or nothing to reset).

Content-Type: `application/json`

```json
{
  "ok": true,
  "hrefs": null
}
```

### Example

```bash
curl -X DELETE "https://crm.nf-hotel.pl/favorites/items" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/favorites/items`

Read the calling user's favorites

`hrefs` is `null` when the user never configured the bar — render `defaults` in that case.

**Tags:** Favorites

**Requires authentication.**

### Responses

**200** – Current favorites.

Content-Type: `application/json`

```json
{
  "hrefs": null,
  "defaults": [
    "string"
  ],
  "maxFavorites": 1
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/favorites/items" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## PUT `/favorites/items`

Replace the calling user's favorites

Accepts up to 8 in-app `/backend` paths, in display order. An empty array means "no favorites" and suppresses the defaults.

**Tags:** Favorites

**Requires authentication.**

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – Saved.

Content-Type: `application/json`

```json
{
  "ok": true,
  "hrefs": null
}
```

**422** – Invalid payload (non-/backend href, or over the limit).

Content-Type: `application/json`

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

### Example

```bash
curl -X PUT "https://crm.nf-hotel.pl/favorites/items" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"hrefs\": [
    \"string\"
  ]
}"
```

## 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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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 `/gateway_przelewy24/accounts`

Lista kont Przelewy24 według organizacji

Poświadczenia Przelewy24 są trzymane po jednym komplecie na organizację. Ta trasa pokazuje wszystkie widoczne organizacje naraz i pozwala zapisać konto wskazanej, bez przełączania kontekstu organizacji.

Requires features: gateway_przelewy24.view

**Tags:** Przelewy24

**Requires authentication.**

**Features:** gateway_przelewy24.view

### Responses

**200** – Konta zwrócone (sekrety wyłącznie jako flagi).

Content-Type: `application/json`

```json
{
  "items": [
    {
      "organizationId": "00000000-0000-4000-8000-000000000000",
      "organizationName": "string",
      "parentOrganizationId": null,
      "configured": true,
      "merchantId": null,
      "posId": null,
      "environment": null,
      "secretsConfigured": {
        "crcKey": true,
        "apiKey": true
      },
      "updatedAt": null,
      "health": {
        "status": null,
        "checkedAt": null
      }
    }
  ],
  "effectiveOrganizationId": null,
  "encryptionUnavailable": true
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/gateway_przelewy24/accounts" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## PUT `/gateway_przelewy24/accounts`

Zapis konta Przelewy24 wskazanej organizacji

Poświadczenia Przelewy24 są trzymane po jednym komplecie na organizację. Ta trasa pokazuje wszystkie widoczne organizacje naraz i pozwala zapisać konto wskazanej, bez przełączania kontekstu organizacji.

Requires features: gateway_przelewy24.configure

**Tags:** Przelewy24

**Requires authentication.**

**Features:** gateway_przelewy24.configure

### Request Body

Content-Type: `application/json`

```json
{
  "organizationId": "00000000-0000-4000-8000-000000000000",
  "merchantId": "string",
  "environment": "sandbox"
}
```

### Responses

**200** – Konto zapisane.

Content-Type: `application/json`

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

**422** – Niepoprawny ładunek.

Content-Type: `application/json`

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

**503** – Szyfrowanie poświadczeń niedostępne.

Content-Type: `application/json`

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

### Example

```bash
curl -X PUT "https://crm.nf-hotel.pl/gateway_przelewy24/accounts" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"organizationId\": \"00000000-0000-4000-8000-000000000000\",
  \"merchantId\": \"string\",
  \"environment\": \"sandbox\"
}"
```

## POST `/gateway_przelewy24/accounts/test`

Uruchom test połączenia dla organizacji

Requires features: gateway_przelewy24.configure

**Tags:** Przelewy24

**Requires authentication.**

**Features:** gateway_przelewy24.configure

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – Wynik testu.

Content-Type: `application/json`

```json
{
  "status": "string"
}
```

**422** – Niepoprawny ładunek.

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/gateway_przelewy24/accounts/test" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"organizationId\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/gateway_przelewy24/transactions`

Lista transakcji bramek

Rdzeniowa lista transakcji filtruje sztywno po organizacji z sesji, a transakcje zapisują się pod organizacją spółki wystawcy. Ta trasa zakresuje się jak lista faktur — organizacja nadrzędna widzi podrzędne. Numer transakcji u operatora pochodzi z kolumny szyfrowanej, więc wyszukiwanie po nim działa na oknie ostatnich 500 transakcji i wtedy `totalIsWindowed` jest prawdziwe.

Requires features: payment_gateways.view

**Tags:** Przelewy24

**Requires authentication.**

**Features:** payment_gateways.view

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

### Responses

**200** – Transakcje zwrócone.

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "organizationId": "00000000-0000-4000-8000-000000000000",
      "organizationName": "string",
      "providerKey": "string",
      "unifiedStatus": "string",
      "amount": "string",
      "currencyCode": "string",
      "gatewayReference": null,
      "invoiceId": null,
      "invoiceNumber": null,
      "createdAt": "string",
      "lastWebhookAt": null
    }
  ],
  "total": 1,
  "page": 1,
  "pageSize": 1,
  "totalIsWindowed": true
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/gateway_przelewy24/transactions?page=1&pageSize=50" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/gus/lookup`

Look up a company by NIP

Resolves the encrypted GUS credentials and returns the company name and address for a given NIP, used to prefill buyer data.

Requires features: gus.lookup

**Tags:** GUS (REGON)

**Requires authentication.**

**Features:** gus.lookup

### Request Body

Content-Type: `application/json`

```json
{
  "nip": "string"
}
```

### Responses

**200** – Company found.

Content-Type: `application/json`

```json
{
  "ok": true,
  "company": {
    "name": "string",
    "nip": null,
    "regon": null,
    "street": null,
    "zip": null,
    "city": null,
    "region": null,
    "type": null
  }
}
```

**400** – Bad request / not configured.

Content-Type: `application/json`

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

**404** – Company not found.

Content-Type: `application/json`

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

**502** – GUS service error.

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/gus/lookup" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"nip\": \"string\"
}"
```

## GET `/integrations`

List integrations

Returns a paginated collection of integrations.

Requires features: integrations.view

**Tags:** Integrations

**Requires authentication.**

**Features:** integrations.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| q | query | any | Optional |
| category | query | any | Optional |
| bundleId | query | any | Optional |
| isEnabled | query | any | Optional |
| healthStatus | query | any | Optional |
| sort | query | any | Optional |
| order | 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 integrations

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "string",
      "title": "string",
      "description": null,
      "category": null,
      "tags": [
        "string"
      ],
      "hub": null,
      "providerKey": null,
      "bundleId": null,
      "author": null,
      "company": null,
      "version": null,
      "hasCredentials": true,
      "isEnabled": true,
      "apiVersion": null,
      "healthStatus": "healthy",
      "lastHealthCheckedAt": null,
      "lastHealthLatencyMs": null,
      "enabledAt": null,
      "analytics": {
        "lastActivityAt": null,
        "totalCount": 1,
        "errorCount": 1,
        "errorRate": 1,
        "dailyCounts": [
          1
        ]
      }
    }
  ],
  "total": 1,
  "totalPages": 1,
  "bundles": [
    {
      "id": "string",
      "title": "string",
      "description": "string",
      "icon": null,
      "integrationCount": 1,
      "enabledCount": 1
    }
  ]
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/integrations?order=asc&page=1&pageSize=100" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/integrations/{id}`

Get integration detail

Requires features: integrations.view

**Tags:** Integrations

**Requires authentication.**

**Features:** integrations.view

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

### Responses

**200** – Success response

Content-Type: `application/json`

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/integrations/:id" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/integrations/{id}/credentials`

Get or save integration credentials

Requires features: integrations.credentials.manage

**Tags:** Integrations

**Requires authentication.**

**Features:** integrations.credentials.manage

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

### Responses

**200** – Success response

Content-Type: `application/json`

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/integrations/:id/credentials" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## PUT `/integrations/{id}/credentials`

Get or save integration credentials

Requires features: integrations.credentials.manage

**Tags:** Integrations

**Requires authentication.**

**Features:** integrations.credentials.manage

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

### Responses

**200** – Success response

Content-Type: `application/json`

### Example

```bash
curl -X PUT "https://crm.nf-hotel.pl/integrations/:id/credentials" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/integrations/{id}/health`

Run health check for an integration

Requires features: integrations.manage

**Tags:** Integrations

**Requires authentication.**

**Features:** integrations.manage

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

### Responses

**201** – Success response

Content-Type: `application/json`

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/integrations/:id/health" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## PUT `/integrations/{id}/state`

Update integration state

Requires features: integrations.manage

**Tags:** Integrations

**Requires authentication.**

**Features:** integrations.manage

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

### Responses

**200** – Success response

Content-Type: `application/json`

### Example

```bash
curl -X PUT "https://crm.nf-hotel.pl/integrations/:id/state" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## PUT `/integrations/{id}/version`

Change integration API version

Requires features: integrations.manage

**Tags:** Integrations

**Requires authentication.**

**Features:** integrations.manage

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

### Responses

**200** – Success response

Content-Type: `application/json`

### Example

```bash
curl -X PUT "https://crm.nf-hotel.pl/integrations/:id/version" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/integrations/logs`

List integration logs

Requires features: integrations.manage

**Tags:** Integrations

**Requires authentication.**

**Features:** integrations.manage

### Responses

**200** – Success response

Content-Type: `application/json`

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/integrations/logs" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/invoice_notifications/deliveries`

List invoice notification deliveries (optionally scoped to one invoice)

Requires features: invoice_notifications.deliveries.view

**Tags:** Invoice notifications

**Requires authentication.**

**Features:** invoice_notifications.deliveries.view

### Responses

**200** – Paginated deliveries.

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "string",
      "invoiceId": null,
      "invoiceNumber": null,
      "channel": "string",
      "type": "string",
      "origin": "string",
      "status": "string",
      "deliveryStatus": "string",
      "deliveredAt": null,
      "deliveryDetail": null,
      "recipientEmail": null,
      "recipientPhone": null,
      "subject": null,
      "sentAt": null,
      "lastError": null,
      "createdAt": null
    }
  ],
  "total": 1,
  "totalPages": 1,
  "page": 1,
  "pageSize": 1
}
```

**400** – Bad request.

Content-Type: `application/json`

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

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/invoice_notifications/deliveries" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/invoice_notifications/deliveries/{id}/message`

Preview a sent notification message

Returns the message as stored at send time: subject and body for e-mail (re-rendered through the shared e-mail wrapper), plain text plus segment analysis for SMS, and the list of invoice documents the message carried (inferred — the PDFs themselves are not archived).

Requires features: invoice_notifications.deliveries.view

**Tags:** Invoice notifications

**Requires authentication.**

**Features:** invoice_notifications.deliveries.view

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

### Responses

**200** – Message content and delivery metadata.

Content-Type: `application/json`

```json
{
  "id": "string",
  "invoiceId": null,
  "invoiceNumber": null,
  "channel": "string",
  "type": "string",
  "status": "string",
  "deliveryStatus": "string",
  "recipient": null,
  "subject": null,
  "createdAt": null,
  "sentAt": null,
  "deliveredAt": null,
  "lastError": null,
  "deliveryDetail": null,
  "email": null,
  "sms": null,
  "attachments": [
    {
      "filename": "string",
      "invoiceId": "string",
      "invoiceNumber": null,
      "inferred": true
    }
  ],
  "contentUnavailable": true
}
```

**400** – Bad request.

Content-Type: `application/json`

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

**404** – Delivery not found.

Content-Type: `application/json`

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

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/invoice_notifications/deliveries/:id/message" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/invoice_notifications/deliveries/{id}/resend`

Retry sending one delivery (skips already-sent rows)

Requires features: invoice_notifications.send

**Tags:** Invoice notifications

**Requires authentication.**

**Features:** invoice_notifications.send

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

### Responses

**200** – Retry applied; current status returned.

Content-Type: `application/json`

```json
{
  "ok": true,
  "id": "string",
  "status": "string"
}
```

**400** – Bad request.

Content-Type: `application/json`

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

**404** – Delivery not found.

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/invoice_notifications/deliveries/:id/resend" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/invoice_notifications/deliveries/webhooks/ses`

Receive SES delivery/bounce notifications via SNS

**Tags:** Invoice notifications

### Responses

**200** – Acknowledged.

Content-Type: `application/json`

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/invoice_notifications/deliveries/webhooks/ses" \
  -H "Accept: application/json"
```

## GET `/invoice_notifications/deliveries/webhooks/smsapi`

Receive SMSAPI delivery reports (DLR) sent as a GET callback

**Tags:** Invoice notifications

### Responses

**200** – Acknowledged.

Content-Type: `application/json`

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/invoice_notifications/deliveries/webhooks/smsapi" \
  -H "Accept: application/json"
```

## POST `/invoice_notifications/deliveries/webhooks/smsapi`

Receive SMSAPI delivery reports (DLR)

**Tags:** Invoice notifications

### Responses

**200** – Acknowledged.

Content-Type: `application/json`

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/invoice_notifications/deliveries/webhooks/smsapi" \
  -H "Accept: application/json"
```

## GET `/invoice_notifications/queue`

List queued deliveries; ?scope=abandoned lists the ones given up on after 24 h

Requires features: invoice_notifications.deliveries.view

**Tags:** Invoice notifications

**Requires authentication.**

**Features:** invoice_notifications.deliveries.view

### Responses

**200** – Paginated queue rows.

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "string",
      "invoiceId": "string",
      "invoiceNumber": null,
      "channel": "string",
      "type": "string",
      "recipient": null,
      "blockedReason": null,
      "blockedReasonLabel": null,
      "attemptCount": 1,
      "nextAttemptAt": null,
      "lastError": null,
      "createdAt": null,
      "abandoned": true
    }
  ],
  "total": 1,
  "totalPages": 1,
  "page": 1,
  "pageSize": 1
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/invoice_notifications/queue" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## DELETE `/invoice_notifications/queue/{id}`

Remove a delivery from the send queue without deleting its record

Requires features: invoice_notifications.send

**Tags:** Invoice notifications

**Requires authentication.**

**Features:** invoice_notifications.send

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

### Responses

**200** – Removed from the queue.

Content-Type: `application/json`

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

**400** – Bad request.

Content-Type: `application/json`

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

**404** – Delivery not found.

Content-Type: `application/json`

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

### Example

```bash
curl -X DELETE "https://crm.nf-hotel.pl/invoice_notifications/queue/:id" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/invoice_notifications/queue/summary`

Count queued deliveries, abandoned ones, and invoices that never reached wFirma

Requires features: invoice_notifications.deliveries.view, wfirma.invoices.view

**Tags:** Invoice notifications

**Requires authentication.**

**Features:** invoice_notifications.deliveries.view, wfirma.invoices.view

### Responses

**200** – Counters.

Content-Type: `application/json`

```json
{
  "queued": 1,
  "abandoned": 1,
  "problems": 1
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/invoice_notifications/queue/summary" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/invoice_notifications/reminders/preview`

Dry run of the payment reminder sweep — sends nothing, writes nothing

Requires features: invoice_notifications.reminders.view

**Tags:** Invoice notifications

**Requires authentication.**

**Features:** invoice_notifications.reminders.view

### Responses

**200** – Invoices that would be reminded, plus skipped ones with reasons.

Content-Type: `application/json`

```json
{
  "planned": [
    {
      "invoiceId": "00000000-0000-4000-8000-000000000000",
      "invoiceNumber": null,
      "hasPdf": true,
      "propertyId": null,
      "dueDate": null,
      "amountGross": "string",
      "currencyCode": "string",
      "recipients": [
        {
          "name": "string",
          "email": "string"
        }
      ]
    }
  ],
  "plannedTotal": 1,
  "skippedTotal": 1,
  "listLimit": 1,
  "canAssignProperty": true,
  "skipped": [
    {
      "invoiceId": "00000000-0000-4000-8000-000000000000",
      "invoiceNumber": null,
      "reason": "no_property",
      "matches": [
        {
          "propertyId": "00000000-0000-4000-8000-000000000000",
          "name": "string",
          "city": null,
          "matchedByProduct": true
        }
      ]
    }
  ],
  "enabled": true
}
```

**400** – No concrete organization selected.

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/invoice_notifications/reminders/preview" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/invoice_notifications/reminders/run`

Execute the reminder sweep immediately (records a manual run and sends real e-mails)

Requires features: invoice_notifications.reminders.manage

**Tags:** Invoice notifications

**Requires authentication.**

**Features:** invoice_notifications.reminders.manage

### Request Body

Content-Type: `application/json`

```json
{}
```

### Responses

**200** – Run summary.

Content-Type: `application/json`

```json
{
  "ok": true,
  "runId": null,
  "sent": 1,
  "failed": 1,
  "skipped": 1
}
```

**400** – No organization selected, or no active overdue template.

Content-Type: `application/json`

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

**409** – A reminder run for this organization is still in progress.

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/invoice_notifications/reminders/run" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{}"
```

## GET `/invoice_notifications/reminders/runs`

Paginated history of reminder runs with their outcome

Requires features: invoice_notifications.reminders.view

**Tags:** Invoice notifications

**Requires authentication.**

**Features:** invoice_notifications.reminders.view

### Responses

**200** – Paginated runs.

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "string",
      "mode": "scheduled",
      "startedAt": "string",
      "finishedAt": null,
      "sentCount": 1,
      "failedCount": 1,
      "skippedCount": 1,
      "capped": true
    }
  ],
  "total": 1,
  "totalPages": 1,
  "page": 1,
  "pageSize": 1,
  "permissions": {
    "canManage": true
  }
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/invoice_notifications/reminders/runs" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/invoice_notifications/reminders/settings`

Read payment reminder settings for the selected organization

Requires features: invoice_notifications.reminders.view

**Tags:** Invoice notifications

**Requires authentication.**

**Features:** invoice_notifications.reminders.view

### Responses

**200** – Current settings, resolved cron and template availability.

Content-Type: `application/json`

```json
{
  "enabled": false,
  "daysAfterDue": 7,
  "minGapDays": 6,
  "channels": [
    "email"
  ],
  "runDayOfWeek": 1,
  "runHour": 9,
  "documentKinds": [
    "vat",
    "proforma"
  ],
  "cron": "string",
  "timezone": "string",
  "hasTemplate": true
}
```

**400** – No concrete organization selected.

Content-Type: `application/json`

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

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/invoice_notifications/reminders/settings" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## PUT `/invoice_notifications/reminders/settings`

Update payment reminder settings and reschedule the weekly sweep

Requires features: invoice_notifications.reminders.manage

**Tags:** Invoice notifications

**Requires authentication.**

**Features:** invoice_notifications.reminders.manage

### Request Body

Content-Type: `application/json`

```json
{
  "enabled": false,
  "daysAfterDue": 7,
  "minGapDays": 6,
  "channels": [
    "email"
  ],
  "runDayOfWeek": 1,
  "runHour": 9,
  "documentKinds": [
    "vat",
    "proforma"
  ]
}
```

### Responses

**200** – Saved settings and the resolved cron.

Content-Type: `application/json`

```json
{
  "enabled": false,
  "daysAfterDue": 7,
  "minGapDays": 6,
  "channels": [
    "email"
  ],
  "runDayOfWeek": 1,
  "runHour": 9,
  "documentKinds": [
    "vat",
    "proforma"
  ],
  "cron": "string",
  "timezone": "string",
  "hasTemplate": true
}
```

**400** – Invalid payload, no organization selected, or enabling without an active overdue template.

Content-Type: `application/json`

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

### Example

```bash
curl -X PUT "https://crm.nf-hotel.pl/invoice_notifications/reminders/settings" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"enabled\": false,
  \"daysAfterDue\": 7,
  \"minGapDays\": 6,
  \"channels\": [
    \"email\"
  ],
  \"runDayOfWeek\": 1,
  \"runHour\": 9,
  \"documentKinds\": [
    \"vat\",
    \"proforma\"
  ]
}"
```

## GET `/invoice_notifications/reminders/skips`

Paginated list of skipped invoices (filter by run, reason, invoice)

Requires features: invoice_notifications.reminders.view

**Tags:** Invoice notifications

**Requires authentication.**

**Features:** invoice_notifications.reminders.view

### Responses

**200** – Paginated skips.

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "string",
      "runId": "string",
      "invoiceId": "string",
      "invoiceNumber": null,
      "propertyId": null,
      "reason": "no_property",
      "detail": null,
      "createdAt": null
    }
  ],
  "total": 1,
  "totalPages": 1,
  "page": 1,
  "pageSize": 1
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/invoice_notifications/reminders/skips" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## DELETE `/invoice_notifications/templates`

Delete notification template

Requires features: invoice_notifications.templates.manage

**Tags:** Invoice notifications

**Requires authentication.**

**Features:** invoice_notifications.templates.manage

### Responses

**200** – Template deleted.

Content-Type: `application/json`

### Example

```bash
curl -X DELETE "https://crm.nf-hotel.pl/invoice_notifications/templates" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/invoice_notifications/templates`

List notification templates

Requires features: invoice_notifications.templates.view

**Tags:** Invoice notifications

**Requires authentication.**

**Features:** invoice_notifications.templates.view

### Responses

**200** – Templates returned.

Content-Type: `application/json`

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/invoice_notifications/templates" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/invoice_notifications/templates`

Create notification template

Requires features: invoice_notifications.templates.manage

**Tags:** Invoice notifications

**Requires authentication.**

**Features:** invoice_notifications.templates.manage

### Request Body

Content-Type: `application/json`

```json
{
  "name": "string",
  "type": "invoice_created",
  "channel": "email",
  "subject": null,
  "emailBody": null,
  "smsBody": null,
  "isActive": true
}
```

### Responses

**200** – Template created.

Content-Type: `application/json`

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/invoice_notifications/templates" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"name\": \"string\",
  \"type\": \"invoice_created\",
  \"channel\": \"email\",
  \"subject\": null,
  \"emailBody\": null,
  \"smsBody\": null,
  \"isActive\": true
}"
```

## PUT `/invoice_notifications/templates`

Update notification template

Requires features: invoice_notifications.templates.manage

**Tags:** Invoice notifications

**Requires authentication.**

**Features:** invoice_notifications.templates.manage

### Request Body

Content-Type: `application/json`

```json
{
  "type": "invoice_created",
  "channel": "email",
  "subject": null,
  "emailBody": null,
  "smsBody": null,
  "isActive": true,
  "id": "00000000-0000-4000-8000-000000000000"
}
```

### Responses

**200** – Template updated.

Content-Type: `application/json`

### Example

```bash
curl -X PUT "https://crm.nf-hotel.pl/invoice_notifications/templates" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"type\": \"invoice_created\",
  \"channel\": \"email\",
  \"subject\": null,
  \"emailBody\": null,
  \"smsBody\": null,
  \"isActive\": true,
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/nfhotel_sync/commissions`

List NFHotel commissions for a settlement period

Read-only list of commissions pulled from NFHotel. Rows are created by the pull worker and advanced by the three staged actions — they are never edited by hand.

Requires features: nfhotel_sync.commissions.view

**Tags:** NFHotel commissions

**Requires authentication.**

**Features:** nfhotel_sync.commissions.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| page | query | any | Optional |
| pageSize | query | any | Optional |
| sort | query | any | Optional |
| order | query | any | Optional |
| search | query | any | Optional |
| periodYear | query | any | Optional |
| periodMonth | query | any | Optional |
| providerType | query | any | Optional |
| status | query | any | Optional |
| propertyId | query | any | Optional |
| errorCode | query | any | Optional |

### Responses

**200** – Commission rows.

Content-Type: `application/json`

```json
{
  "items": [
    {}
  ],
  "total": 1,
  "page": 1,
  "pageSize": 1
}
```

**400** – Invalid query.

Content-Type: `application/json`

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

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/nfhotel_sync/commissions?page=1&pageSize=50" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/nfhotel_sync/commissions/attach-property`

Attach a property to an unmatched commission account

Writes the saas_user_id into the property code and re-resolves every commission row of that account.

Requires features: nfhotel_sync.commissions.manage

**Tags:** NFHotel commissions

**Requires authentication.**

**Features:** nfhotel_sync.commissions.manage

### Request Body

Content-Type: `application/json`

```json
{
  "commissionId": "00000000-0000-4000-8000-000000000000",
  "propertyId": "00000000-0000-4000-8000-000000000000"
}
```

### Responses

**200** – Account attached.

Content-Type: `application/json`

```json
{
  "ok": true,
  "saasUserId": 1,
  "rozwiazanych": 1
}
```

**400** – Invalid payload or missing organization.

Content-Type: `application/json`

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

**404** – Commission row or property not found.

Content-Type: `application/json`

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

**409** – Another property already uses this SaaS id.

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/nfhotel_sync/commissions/attach-property" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"commissionId\": \"00000000-0000-4000-8000-000000000000\",
  \"propertyId\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## POST `/nfhotel_sync/commissions/create-drafts`

Queue creation of draft invoices from commission services

Step 2 of 4: groups services by property and creates ONE draft invoice per property, with up to three lines.

Requires features: nfhotel_sync.commissions.manage

**Tags:** NFHotel commissions

**Requires authentication.**

**Features:** nfhotel_sync.commissions.manage

### Request Body

Content-Type: `application/json`

```json
{}
```

### Responses

**200** – Stage run queued.

Content-Type: `application/json`

```json
{
  "ok": true,
  "queued": true,
  "doZrobienia": 1,
  "progressJobId": null,
  "obciete": true
}
```

**400** – Invalid payload or missing organization.

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/nfhotel_sync/commissions/create-drafts" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{}"
```

## POST `/nfhotel_sync/commissions/create-services`

Queue creation of services from commission rows

Step 1 of 4: turns pulled commission rows into wFirma subscriptions (one-off, one per channel and month).

Requires features: nfhotel_sync.commissions.manage

**Tags:** NFHotel commissions

**Requires authentication.**

**Features:** nfhotel_sync.commissions.manage

### Request Body

Content-Type: `application/json`

```json
{}
```

### Responses

**200** – Stage run queued.

Content-Type: `application/json`

```json
{
  "ok": true,
  "queued": true,
  "doZrobienia": 1,
  "progressJobId": null,
  "obciete": true
}
```

**400** – Invalid payload or missing organization.

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/nfhotel_sync/commissions/create-services" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{}"
```

## POST `/nfhotel_sync/commissions/exclude`

Toggle exclusion on commission rows

Marks rows as excluded (or restores them). Excluded rows stay visible but are skipped by the staged actions.

Requires features: nfhotel_sync.commissions.manage

**Tags:** NFHotel commissions

**Requires authentication.**

**Features:** nfhotel_sync.commissions.manage

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – Exclusion updated.

Content-Type: `application/json`

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

**400** – Invalid payload or missing organization.

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/nfhotel_sync/commissions/exclude" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"ids\": [
    \"00000000-0000-4000-8000-000000000000\"
  ],
  \"excluded\": true
}"
```

## POST `/nfhotel_sync/commissions/issue`

Queue issuing of commission draft invoices to wFirma

Step 3 of 4: issues the draft invoices. Irreversible — guarded by a separate permission.

Requires features: nfhotel_sync.commissions.issue

**Tags:** NFHotel commissions

**Requires authentication.**

**Features:** nfhotel_sync.commissions.issue

### Request Body

Content-Type: `application/json`

```json
{}
```

### Responses

**200** – Stage run queued.

Content-Type: `application/json`

```json
{
  "ok": true,
  "queued": true,
  "doZrobienia": 1,
  "progressJobId": null,
  "obciete": true
}
```

**400** – Invalid payload or missing organization.

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/nfhotel_sync/commissions/issue" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{}"
```

## POST `/nfhotel_sync/commissions/pull`

Queue a commission pull for a month

Queues a background pull of commissions for one settlement month.

Requires features: nfhotel_sync.commissions.manage

**Tags:** NFHotel commissions

**Requires authentication.**

**Features:** nfhotel_sync.commissions.manage

### Request Body

Content-Type: `application/json`

```json
{
  "periodYear": 1,
  "periodMonth": 1
}
```

### Responses

**200** – Pull queued.

Content-Type: `application/json`

```json
{
  "ok": true,
  "queued": true,
  "progressJobId": null
}
```

**400** – Invalid payload or missing organization.

Content-Type: `application/json`

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

**409** – Another commission run is active.

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/nfhotel_sync/commissions/pull" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"periodYear\": 1,
  \"periodMonth\": 1
}"
```

## POST `/nfhotel_sync/commissions/run-all`

Queue the whole commission run

Runs all four stages for the selection in one background run, starting at the first stage that has work. Each stage with work gets its own progress job. The run stops at a stage whose safety breaker trips; rows already moved stay moved (every stage is idempotent), so a repeat continues where it stopped. The send stage only sends documents that are ready (pro forma, or invoices with a KSeF number); the rest are reported as waiting for KSeF. Refused with 409 while another commission run is active.

Requires features: nfhotel_sync.commissions.manage, nfhotel_sync.commissions.issue

**Tags:** NFHotel commissions

**Requires authentication.**

**Features:** nfhotel_sync.commissions.manage, nfhotel_sync.commissions.issue

### Request Body

Content-Type: `application/json`

```json
{}
```

### Responses

**200** – Run queued (or nothing to do: `queued: false`).

Content-Type: `application/json`

```json
{
  "ok": true,
  "queued": true,
  "doZrobienia": 1,
  "progressJobId": null,
  "obciete": true
}
```

**400** – Invalid payload or missing organization.

Content-Type: `application/json`

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

**409** – Another commission run is active.

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/nfhotel_sync/commissions/run-all" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{}"
```

## POST `/nfhotel_sync/commissions/send`

Queue sending of issued commission invoices to clients

Step 4 of 4: emails issued commission invoices, but only those that already carry a KSeF number.

Requires features: nfhotel_sync.commissions.issue

**Tags:** NFHotel commissions

**Requires authentication.**

**Features:** nfhotel_sync.commissions.issue

### Request Body

Content-Type: `application/json`

```json
{}
```

### Responses

**200** – Stage run queued.

Content-Type: `application/json`

```json
{
  "ok": true,
  "queued": true,
  "doZrobienia": 1,
  "progressJobId": null,
  "obciete": true
}
```

**400** – Invalid payload or missing organization.

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/nfhotel_sync/commissions/send" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{}"
```

## GET `/nfhotel_sync/commissions/summary`

Commission summary for a month

Per-product and per-stage aggregates, computed in the database (the list itself is paginated).

Requires features: nfhotel_sync.commissions.view

**Tags:** NFHotel commissions

**Requires authentication.**

**Features:** nfhotel_sync.commissions.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| periodYear | query | any | Required |
| periodMonth | query | any | Required |
| status | query | any | Optional |
| providerType | query | any | Optional |
| errorCode | query | any | Optional |

### Responses

**200** – Summary.

Content-Type: `application/json`

```json
{
  "produkty": [
    {
      "productName": null,
      "providerType": null,
      "waluta": "string",
      "liczba": 1,
      "netto": "string"
    }
  ],
  "etapy": [
    {
      "status": "string",
      "liczba": 1,
      "kwoty": [
        {
          "waluta": "string",
          "netto": "string"
        }
      ]
    }
  ],
  "razem": {
    "liczba": 1,
    "kwoty": [
      {
        "waluta": "string",
        "netto": "string"
      }
    ]
  },
  "wykluczone": {
    "liczba": 1,
    "kwoty": [
      {
        "waluta": "string",
        "netto": "string"
      }
    ],
    "pozycje": [
      {
        "saasUserId": 1,
        "nazwa": "string",
        "waluta": "string",
        "netto": "string"
      }
    ]
  }
}
```

**400** – Invalid query.

Content-Type: `application/json`

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

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/nfhotel_sync/commissions/summary?periodYear=1&periodMonth=1" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/nfhotel_sync/locations`

List towns

Lists towns (filtered by country and search) and adds a missing one. Adding is idempotent by normalized name within a country.

Requires features: nfhotel_sync.properties.view

**Tags:** NFHotel locations

**Requires authentication.**

**Features:** nfhotel_sync.properties.view

### Responses

**200** – Matching towns.

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "name": "string",
      "countryCode": "string",
      "region": null,
      "county": null,
      "postalCode": null,
      "matchedPostalCode": null
    }
  ]
}
```

**400** – Invalid query.

Content-Type: `application/json`

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

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/nfhotel_sync/locations" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/nfhotel_sync/locations`

Add a missing town

Lists towns (filtered by country and search) and adds a missing one. Adding is idempotent by normalized name within a country.

Requires features: nfhotel_sync.properties.manage

**Tags:** NFHotel locations

**Requires authentication.**

**Features:** nfhotel_sync.properties.manage

### Request Body

Content-Type: `application/json`

```json
{
  "name": "string",
  "region": null,
  "county": null,
  "postalCode": null
}
```

### Responses

**200** – Town already existed; the existing one is returned.

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "name": "string",
  "countryCode": "string",
  "region": null,
  "county": null,
  "postalCode": null,
  "matchedPostalCode": null
}
```

**201** – Town created.

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "name": "string",
  "countryCode": "string",
  "region": null,
  "county": null,
  "postalCode": null,
  "matchedPostalCode": null
}
```

**400** – Invalid payload or no organization in scope.

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/nfhotel_sync/locations" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"name\": \"string\",
  \"region\": null,
  \"county\": null,
  \"postalCode\": null
}"
```

## GET `/nfhotel_sync/locations/counties`

List counties in use

Distinct counties (powiaty) actually used by properties — feeds the property list filter.

Requires features: nfhotel_sync.properties.view

**Tags:** NFHotel locations

**Requires authentication.**

**Features:** nfhotel_sync.properties.view

### Responses

**200** – Counties.

Content-Type: `application/json`

```json
{
  "items": [
    {
      "value": "string",
      "label": "string"
    }
  ]
}
```

**400** – Invalid query.

Content-Type: `application/json`

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

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/nfhotel_sync/locations/counties" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## DELETE `/nfhotel_sync/properties`

Delete a property

CRUD for accommodation properties (objects) that belong to a customer company.

Requires features: nfhotel_sync.properties.delete

**Tags:** NFHotel properties

**Requires authentication.**

**Features:** nfhotel_sync.properties.delete

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

### Responses

**200** – Property deleted.

Content-Type: `application/json`

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

**400** – Invalid identifier.

Content-Type: `application/json`

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

**409** – Property still has invoices or subscriptions attached.

Content-Type: `application/json`

```json
{
  "error": "string",
  "blockers": {
    "invoices": 1,
    "subscriptions": 1
  }
}
```

### Example

```bash
curl -X DELETE "https://crm.nf-hotel.pl/nfhotel_sync/properties?id=00000000-0000-4000-8000-000000000000" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/nfhotel_sync/properties`

List properties

CRUD for accommodation properties (objects) that belong to a customer company.

Requires features: nfhotel_sync.properties.view

**Tags:** NFHotel properties

**Requires authentication.**

**Features:** nfhotel_sync.properties.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| page | query | any | Optional |
| pageSize | query | any | Optional |
| sort | query | any | Optional |
| order | query | any | Optional |
| search | query | any | Optional |
| companyId | query | any | Optional |
| personId | query | any | Optional |
| id | query | any | Optional |

### Responses

**200** – Properties for the current scope.

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "companyEntityId": null,
      "companyName": null,
      "activeServices": [
        {
          "id": "00000000-0000-4000-8000-000000000000",
          "name": "string"
        }
      ],
      "name": "string",
      "code": null,
      "city": null,
      "county": null,
      "locationId": null,
      "countryCode": null,
      "postalCode": null,
      "address": null,
      "url": null,
      "notes": null,
      "pmsProgram": null,
      "bookingEngine": null,
      "channelManager": null,
      "roomsCount": null,
      "cottagesCount": null,
      "apartmentsCount": null,
      "priceStandard": null,
      "pricePlus": null,
      "pricePro": null,
      "priceUltimate": null,
      "coupon": null,
      "hasPanorama": true,
      "hasGuide": true,
      "hasColoring": true,
      "autoCommission": true,
      "proformaOnly": true,
      "isAccountPrimary": true,
      "paymentDeferredUntil": null,
      "paymentDeferredAt": null,
      "paymentDeferredByName": null,
      "defaultCurrencyCode": "string",
      "isActive": true,
      "createdAt": null
    }
  ],
  "total": 1,
  "permissions": {
    "canManage": true,
    "canDelete": true,
    "canDeferPayment": true
  }
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/nfhotel_sync/properties?page=1&pageSize=50" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/nfhotel_sync/properties`

Create a property

CRUD for accommodation properties (objects) that belong to a customer company.

Requires features: nfhotel_sync.properties.manage

**Tags:** NFHotel properties

**Requires authentication.**

**Features:** nfhotel_sync.properties.manage

### Request Body

Content-Type: `application/json`

```json
{
  "companyEntityId": null,
  "name": "string",
  "code": null,
  "city": null,
  "locationId": null,
  "county": null,
  "countryCode": null,
  "postalCode": null,
  "address": null,
  "notes": null,
  "pmsProgram": null,
  "bookingEngine": null,
  "channelManager": null,
  "roomsCount": null,
  "cottagesCount": null,
  "apartmentsCount": null,
  "priceStandard": null,
  "pricePlus": null,
  "pricePro": null,
  "priceUltimate": null,
  "coupon": null,
  "paymentDeferredUntil": null,
  "invoiceRecipient": null
}
```

### Responses

**201** – Property created.

Content-Type: `application/json`

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

**400** – Invalid payload, or `paymentDeferredUntil` is before today (UTC).

Content-Type: `application/json`

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

**404** – Company was not found.

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/nfhotel_sync/properties" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"companyEntityId\": null,
  \"name\": \"string\",
  \"code\": null,
  \"city\": null,
  \"locationId\": null,
  \"county\": null,
  \"countryCode\": null,
  \"postalCode\": null,
  \"address\": null,
  \"notes\": null,
  \"pmsProgram\": null,
  \"bookingEngine\": null,
  \"channelManager\": null,
  \"roomsCount\": null,
  \"cottagesCount\": null,
  \"apartmentsCount\": null,
  \"priceStandard\": null,
  \"pricePlus\": null,
  \"pricePro\": null,
  \"priceUltimate\": null,
  \"coupon\": null,
  \"paymentDeferredUntil\": null,
  \"invoiceRecipient\": null
}"
```

## PUT `/nfhotel_sync/properties`

Update a property

`paymentDeferredUntil` (property-wide payment deferral, `YYYY-MM-DD` inclusive, `null` removes it) is checked only when it CHANGES: the change requires `nfhotel_sync.properties.defer_payment` (403 otherwise — the whole update is rejected) and a new date must not be before today (UTC midnight). Re-sending the stored value, even an expired one, is a no-op. Every change records who and when (`paymentDeferredAt`, `paymentDeferredByName` in the list response). `isAccountPrimary` (SPEC-071) marks the property as the primary one of its NFHotel account (the account = all non-deleted properties sharing `code`): setting it to `true` requires a code (400 otherwise) and removes the mark from other properties with the same code; changing `code` without re-sending `isAccountPrimary: true` removes it.

Requires features: nfhotel_sync.properties.manage

**Tags:** NFHotel properties

**Requires authentication.**

**Features:** nfhotel_sync.properties.manage

### Request Body

Content-Type: `application/json`

```json
{
  "companyEntityId": null,
  "code": null,
  "city": null,
  "locationId": null,
  "county": null,
  "countryCode": null,
  "postalCode": null,
  "address": null,
  "notes": null,
  "pmsProgram": null,
  "bookingEngine": null,
  "channelManager": null,
  "roomsCount": null,
  "cottagesCount": null,
  "apartmentsCount": null,
  "priceStandard": null,
  "pricePlus": null,
  "pricePro": null,
  "priceUltimate": null,
  "coupon": null,
  "paymentDeferredUntil": null,
  "invoiceRecipient": null,
  "id": "00000000-0000-4000-8000-000000000000"
}
```

### Responses

**200** – Property updated.

Content-Type: `application/json`

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

**400** – Invalid payload, a changed `paymentDeferredUntil` is before today (UTC), or `isAccountPrimary: true` on a property without a code.

Content-Type: `application/json`

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

**404** – Property or company was not found.

Content-Type: `application/json`

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

### Example

```bash
curl -X PUT "https://crm.nf-hotel.pl/nfhotel_sync/properties" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"companyEntityId\": null,
  \"code\": null,
  \"city\": null,
  \"locationId\": null,
  \"county\": null,
  \"countryCode\": null,
  \"postalCode\": null,
  \"address\": null,
  \"notes\": null,
  \"pmsProgram\": null,
  \"bookingEngine\": null,
  \"channelManager\": null,
  \"roomsCount\": null,
  \"cottagesCount\": null,
  \"apartmentsCount\": null,
  \"priceStandard\": null,
  \"pricePlus\": null,
  \"pricePro\": null,
  \"priceUltimate\": null,
  \"coupon\": null,
  \"paymentDeferredUntil\": null,
  \"invoiceRecipient\": null,
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## DELETE `/nfhotel_sync/properties/{id}/companies`

Unlink a company from a property

Lists, links, re-roles and unlinks companies of an accommodation property. One property can have a main company (owner), parallel payers and historical companies from before an ownership or legal-form change.

Requires features: nfhotel_sync.properties.manage

**Tags:** NFHotel properties

**Requires authentication.**

**Features:** nfhotel_sync.properties.manage

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

### Responses

**200** – Company unlinked.

Content-Type: `application/json`

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

**400** – Invalid payload.

Content-Type: `application/json`

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

**404** – Property was not found.

Content-Type: `application/json`

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

### Example

```bash
curl -X DELETE "https://crm.nf-hotel.pl/nfhotel_sync/properties/:id/companies?companyId=00000000-0000-4000-8000-000000000000" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/nfhotel_sync/properties/{id}/companies`

List property companies

Lists, links, re-roles and unlinks companies of an accommodation property. One property can have a main company (owner), parallel payers and historical companies from before an ownership or legal-form change.

Requires features: nfhotel_sync.properties.view

**Tags:** NFHotel properties

**Requires authentication.**

**Features:** nfhotel_sync.properties.view

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

### Responses

**200** – Linked companies.

Content-Type: `application/json`

```json
{
  "items": [
    {
      "companyId": "00000000-0000-4000-8000-000000000000",
      "name": null,
      "taxId": null,
      "taxIdType": null,
      "street": null,
      "zip": null,
      "city": null,
      "role": "owner",
      "validFrom": null,
      "validTo": null,
      "note": null,
      "isPrimaryColumn": true
    }
  ],
  "total": 1,
  "organizationId": null
}
```

**404** – Property was not found.

Content-Type: `application/json`

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

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/nfhotel_sync/properties/:id/companies" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/nfhotel_sync/properties/{id}/companies`

Link a company to a property

Lists, links, re-roles and unlinks companies of an accommodation property. One property can have a main company (owner), parallel payers and historical companies from before an ownership or legal-form change.

Requires features: nfhotel_sync.properties.manage

**Tags:** NFHotel properties

**Requires authentication.**

**Features:** nfhotel_sync.properties.manage

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

### Request Body

Content-Type: `application/json`

```json
{
  "companyId": "00000000-0000-4000-8000-000000000000",
  "role": "payer",
  "validFrom": null,
  "validTo": null,
  "note": null
}
```

### Responses

**200** – Company linked.

Content-Type: `application/json`

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

**400** – Invalid payload.

Content-Type: `application/json`

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

**404** – Property or company was not found.

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/nfhotel_sync/properties/:id/companies" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"companyId\": \"00000000-0000-4000-8000-000000000000\",
  \"role\": \"payer\",
  \"validFrom\": null,
  \"validTo\": null,
  \"note\": null
}"
```

## PUT `/nfhotel_sync/properties/{id}/companies`

Update role or validity window of a linked company

Lists, links, re-roles and unlinks companies of an accommodation property. One property can have a main company (owner), parallel payers and historical companies from before an ownership or legal-form change.

Requires features: nfhotel_sync.properties.manage

**Tags:** NFHotel properties

**Requires authentication.**

**Features:** nfhotel_sync.properties.manage

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

### Request Body

Content-Type: `application/json`

```json
{
  "companyId": "00000000-0000-4000-8000-000000000000",
  "validFrom": null,
  "validTo": null,
  "note": null
}
```

### Responses

**200** – Link updated.

Content-Type: `application/json`

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

**400** – Invalid payload.

Content-Type: `application/json`

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

**404** – Property or link was not found.

Content-Type: `application/json`

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

### Example

```bash
curl -X PUT "https://crm.nf-hotel.pl/nfhotel_sync/properties/:id/companies" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"companyId\": \"00000000-0000-4000-8000-000000000000\",
  \"validFrom\": null,
  \"validTo\": null,
  \"note\": null
}"
```

## DELETE `/nfhotel_sync/properties/{id}/customers`

Unlink a customer from a property

Lists, links, and unlinks existing CRM people as customers of an accommodation property.

Requires features: nfhotel_sync.properties.manage

**Tags:** NFHotel properties

**Requires authentication.**

**Features:** nfhotel_sync.properties.manage

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

### Responses

**200** – Customer unlinked.

Content-Type: `application/json`

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

**400** – Invalid payload.

Content-Type: `application/json`

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

**404** – Property was not found.

Content-Type: `application/json`

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

### Example

```bash
curl -X DELETE "https://crm.nf-hotel.pl/nfhotel_sync/properties/:id/customers?personId=00000000-0000-4000-8000-000000000000" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/nfhotel_sync/properties/{id}/customers`

List property customers

Lists, links, and unlinks existing CRM people as customers of an accommodation property.

Requires features: nfhotel_sync.properties.view

**Tags:** NFHotel properties

**Requires authentication.**

**Features:** nfhotel_sync.properties.view

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

### Responses

**200** – Linked customers.

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "displayName": "string",
      "primaryEmail": null,
      "primaryPhone": null,
      "jobTitle": null,
      "department": null,
      "role": null,
      "isPrimary": true,
      "linkedAt": null
    }
  ],
  "total": 1,
  "organizationId": null
}
```

**404** – Property was not found.

Content-Type: `application/json`

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

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/nfhotel_sync/properties/:id/customers" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/nfhotel_sync/properties/{id}/customers`

Link a customer to a property

Lists, links, and unlinks existing CRM people as customers of an accommodation property.

Requires features: nfhotel_sync.properties.manage

**Tags:** NFHotel properties

**Requires authentication.**

**Features:** nfhotel_sync.properties.manage

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

### Request Body

Content-Type: `application/json`

```json
{
  "personId": "00000000-0000-4000-8000-000000000000",
  "role": null
}
```

### Responses

**200** – Customer linked.

Content-Type: `application/json`

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

**400** – Invalid payload.

Content-Type: `application/json`

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

**404** – Property or customer was not found.

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/nfhotel_sync/properties/:id/customers" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"personId\": \"00000000-0000-4000-8000-000000000000\",
  \"role\": null
}"
```

## PUT `/nfhotel_sync/properties/{id}/customers`

Update the role of a linked customer

Lists, links, and unlinks existing CRM people as customers of an accommodation property.

Requires features: nfhotel_sync.properties.manage

**Tags:** NFHotel properties

**Requires authentication.**

**Features:** nfhotel_sync.properties.manage

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

### Request Body

Content-Type: `application/json`

```json
{
  "personId": "00000000-0000-4000-8000-000000000000",
  "role": null
}
```

### Responses

**200** – Role updated.

Content-Type: `application/json`

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

**400** – Invalid payload.

Content-Type: `application/json`

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

**404** – Property or link was not found.

Content-Type: `application/json`

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

### Example

```bash
curl -X PUT "https://crm.nf-hotel.pl/nfhotel_sync/properties/:id/customers" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"personId\": \"00000000-0000-4000-8000-000000000000\",
  \"role\": null
}"
```

## GET `/nfhotel_sync/properties/{id}/history`

List property history entries

Paginated CRM activity history entries migrated for an accommodation property, newest first.

Requires features: nfhotel_sync.properties.view

**Tags:** NFHotel properties

**Requires authentication.**

**Features:** nfhotel_sync.properties.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| id | path | any | Required |
| page | query | any | Optional |
| pageSize | query | any | Optional |

### Responses

**200** – History entries for the property.

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "comment": null,
      "author": null,
      "done": true,
      "nextContact": null,
      "nextContactAt": null,
      "rating": null,
      "phoneDone": null,
      "createdAt": null
    }
  ],
  "total": 1,
  "page": 1,
  "pageSize": 1
}
```

**400** – Invalid identifier.

Content-Type: `application/json`

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

**404** – Property was not found.

Content-Type: `application/json`

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

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/nfhotel_sync/properties/:id/history?page=1&pageSize=20" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/nfhotel_sync/properties/{id}/timeline`

List unified timeline events for a property

Paginated, newest-first union of comments, wFirma invoices and transcribed phone calls linked to an accommodation property, normalized to a common event DTO. For invoice events `occurredAt` is the moment the document was issued (`issued_at`, falling back to `created_at` for drafts) — the business issue date travels separately as `payload.issueDate`. A `dateTo` given without a time covers the whole of that day.

Requires features: nfhotel_sync.properties.view

**Tags:** NFHotel properties

**Requires authentication.**

**Features:** nfhotel_sync.properties.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| id | path | any | Required |
| types | query | any | Optional |
| userId | query | any | Optional |
| dateFrom | query | any | Optional |
| dateTo | query | any | Optional |
| cursor | query | any | Optional |
| pageSize | query | any | Optional |

### Responses

**200** – Timeline events for the property.

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "string",
      "type": "comment",
      "occurredAt": "string",
      "author": {
        "userId": null,
        "name": "string"
      },
      "title": "string",
      "summary": null,
      "payload": {}
    }
  ],
  "nextCursor": null,
  "pageSize": 1
}
```

**400** – Invalid identifier or query.

Content-Type: `application/json`

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

**404** – Property was not found.

Content-Type: `application/json`

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

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/nfhotel_sync/properties/:id/timeline?pageSize=20" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## 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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/notifications/unread-count" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/payment_gateways/cancel`

Cancel payment

Requires features: payment_gateways.manage

**Tags:** PaymentGateways

**Requires authentication.**

**Features:** payment_gateways.manage

### Responses

**200** – Payment cancelled

Content-Type: `application/json`

**409** – Invalid payment status transition

Content-Type: `application/json`

**422** – Invalid payload

Content-Type: `application/json`

**502** – Gateway provider error

Content-Type: `application/json`

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/payment_gateways/cancel" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/payment_gateways/capture`

Capture payment

Requires features: payment_gateways.capture

**Tags:** PaymentGateways

**Requires authentication.**

**Features:** payment_gateways.capture

### Responses

**200** – Payment captured

Content-Type: `application/json`

**409** – Invalid payment status transition

Content-Type: `application/json`

**422** – Invalid payload

Content-Type: `application/json`

**502** – Gateway provider error

Content-Type: `application/json`

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/payment_gateways/capture" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/payment_gateways/providers`

List payment gateway descriptors

Requires features: payment_gateways.view

**Tags:** PaymentGateways

**Requires authentication.**

**Features:** payment_gateways.view

### Responses

**200** – List of safe payment gateway descriptors

Content-Type: `application/json`

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/payment_gateways/providers" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/payment_gateways/providers/{providerKey}`

Get payment gateway descriptor

Requires features: payment_gateways.view

**Tags:** PaymentGateways

**Requires authentication.**

**Features:** payment_gateways.view

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

### Responses

**200** – Provider descriptor

Content-Type: `application/json`

**404** – Provider descriptor not found

Content-Type: `application/json`

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/payment_gateways/providers/:providerKey" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/payment_gateways/refund`

Refund payment

Requires features: payment_gateways.refund

**Tags:** PaymentGateways

**Requires authentication.**

**Features:** payment_gateways.refund

### Responses

**200** – Payment refunded

Content-Type: `application/json`

**409** – Invalid payment status transition

Content-Type: `application/json`

**422** – Invalid payload

Content-Type: `application/json`

**502** – Gateway provider error

Content-Type: `application/json`

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/payment_gateways/refund" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/payment_gateways/sessions`

Create payment session

Requires features: payment_gateways.manage

**Tags:** PaymentGateways

**Requires authentication.**

**Features:** payment_gateways.manage

### Responses

**201** – Payment session created

Content-Type: `application/json`

**422** – Invalid payload or unknown provider

Content-Type: `application/json`

**502** – Gateway provider error

Content-Type: `application/json`

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/payment_gateways/sessions" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/payment_gateways/status`

Get stored transaction status

Requires features: payment_gateways.view

**Tags:** PaymentGateways

**Requires authentication.**

**Features:** payment_gateways.view

### Responses

**200** – Stored transaction status

Content-Type: `application/json`

**400** – Missing or malformed transactionId

Content-Type: `application/json`

**404** – Transaction not found

Content-Type: `application/json`

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/payment_gateways/status" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/payment_gateways/status`

Refresh transaction status from the provider

Requires features: payment_gateways.manage

**Tags:** PaymentGateways

**Requires authentication.**

**Features:** payment_gateways.manage

### Responses

**200** – Refreshed transaction status

Content-Type: `application/json`

**404** – Transaction not found

Content-Type: `application/json`

**422** – Missing or malformed payload

Content-Type: `application/json`

**502** – Gateway provider error

Content-Type: `application/json`

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/payment_gateways/status" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/payment_gateways/transactions`

List payment transactions

Requires features: payment_gateways.view

**Tags:** PaymentGateways

**Requires authentication.**

**Features:** payment_gateways.view

### Responses

**200** – Payment transaction list

Content-Type: `application/json`

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/payment_gateways/transactions" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/payment_gateways/transactions/{id}`

Get payment transaction details

Requires features: payment_gateways.view

**Tags:** PaymentGateways

**Requires authentication.**

**Features:** payment_gateways.view

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

### Responses

**200** – Payment transaction details

Content-Type: `application/json`

**404** – Transaction not found

Content-Type: `application/json`

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/payment_gateways/transactions/:id" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/payment_gateways/webhook/{provider}`

Process inbound webhook from payment provider

**Tags:** PaymentGateways

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

### Responses

**202** – Webhook accepted for async processing

Content-Type: `application/json`

**401** – Signature verification failed

Content-Type: `application/json`

**404** – Unknown provider

Content-Type: `application/json`

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/payment_gateways/webhook/:provider" \
  -H "Accept: application/json"
```

## DELETE `/planner/availability`

Delete availability rule

Deletes an availability rule by id.

**Tags:** Planner

**Requires authentication.**

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – Availability rule deleted

Content-Type: `application/json`

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

### Example

```bash
curl -X DELETE "https://crm.nf-hotel.pl/planner/availability" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/planner/availability`

List availability rules

Returns a paginated collection of availability rules scoped to the authenticated organization.

Requires features: planner.view

**Tags:** Planner

**Requires authentication.**

**Features:** planner.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| page | query | any | Optional |
| pageSize | query | any | Optional |
| subjectType | query | any | Optional |
| subjectIds | query | any | Optional |
| sortField | query | any | Optional |
| sortDir | query | any | Optional |
| ids | query | any | Optional. Comma-separated list of record UUIDs to filter by (max 200). |

### Responses

**200** – Paginated availability rules

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": null,
      "organization_id": null,
      "tenant_id": null,
      "subject_type": null,
      "subject_id": null,
      "timezone": null,
      "rrule": null,
      "exdates": null,
      "kind": null,
      "note": null,
      "unavailability_reason_entry_id": null,
      "unavailability_reason_value": null,
      "created_at": null,
      "updated_at": null
    }
  ],
  "total": 1,
  "totalPages": 1
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/planner/availability?page=1&pageSize=50" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/planner/availability`

Create availability rule

Creates an availability rule for the selected subject.

**Tags:** Planner

**Requires authentication.**

### Request Body

Content-Type: `application/json`

```json
{
  "tenantId": "00000000-0000-4000-8000-000000000000",
  "organizationId": "00000000-0000-4000-8000-000000000000",
  "subjectType": "member",
  "subjectId": "00000000-0000-4000-8000-000000000000",
  "timezone": "string",
  "rrule": "string",
  "exdates": [],
  "kind": "availability",
  "note": null,
  "unavailabilityReasonEntryId": null,
  "unavailabilityReasonValue": null
}
```

### Responses

**201** – Availability rule created

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/planner/availability" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"tenantId\": \"00000000-0000-4000-8000-000000000000\",
  \"organizationId\": \"00000000-0000-4000-8000-000000000000\",
  \"subjectType\": \"member\",
  \"subjectId\": \"00000000-0000-4000-8000-000000000000\",
  \"timezone\": \"string\",
  \"rrule\": \"string\",
  \"exdates\": [],
  \"kind\": \"availability\",
  \"note\": null,
  \"unavailabilityReasonEntryId\": null,
  \"unavailabilityReasonValue\": null
}"
```

## PUT `/planner/availability`

Update availability rule

Updates an availability rule by id.

**Tags:** Planner

**Requires authentication.**

### Request Body

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "note": null,
  "unavailabilityReasonEntryId": null,
  "unavailabilityReasonValue": null
}
```

### Responses

**200** – Availability rule updated

Content-Type: `application/json`

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

### Example

```bash
curl -X PUT "https://crm.nf-hotel.pl/planner/availability" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\",
  \"note\": null,
  \"unavailabilityReasonEntryId\": null,
  \"unavailabilityReasonValue\": null
}"
```

## POST `/planner/availability-date-specific`

Replace date-specific availability

Replaces date-specific availability rules for the subject in a single request.

**Tags:** Planner

**Requires authentication.**

### Request Body

Content-Type: `application/json`

```json
{
  "tenantId": "00000000-0000-4000-8000-000000000000",
  "organizationId": "00000000-0000-4000-8000-000000000000",
  "subjectType": "member",
  "subjectId": "00000000-0000-4000-8000-000000000000",
  "timezone": "string",
  "windows": [],
  "isAvailable": true,
  "note": null,
  "unavailabilityReasonEntryId": null,
  "unavailabilityReasonValue": null
}
```

### Responses

**200** – Date-specific availability updated

Content-Type: `application/json`

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

**400** – Invalid payload

Content-Type: `application/json`

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

**403** – Forbidden

Content-Type: `application/json`

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

**409** – Conflict — the subject/date availability was modified by another edit

Content-Type: `application/json`

```json
{
  "error": "string",
  "code": "string",
  "currentUpdatedAt": "string",
  "expectedUpdatedAt": "string"
}
```

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/planner/availability-date-specific" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"tenantId\": \"00000000-0000-4000-8000-000000000000\",
  \"organizationId\": \"00000000-0000-4000-8000-000000000000\",
  \"subjectType\": \"member\",
  \"subjectId\": \"00000000-0000-4000-8000-000000000000\",
  \"timezone\": \"string\",
  \"windows\": [],
  \"isAvailable\": true,
  \"note\": null,
  \"unavailabilityReasonEntryId\": null,
  \"unavailabilityReasonValue\": null
}"
```

## DELETE `/planner/availability-rule-sets`

Delete availability rule set

Deletes an availability rule set by id.

Requires features: planner.manage_availability

**Tags:** Planner

**Requires authentication.**

**Features:** planner.manage_availability

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – Availability rule set deleted

Content-Type: `application/json`

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

### Example

```bash
curl -X DELETE "https://crm.nf-hotel.pl/planner/availability-rule-sets" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/planner/availability-rule-sets`

List availability rule sets

Returns a paginated collection of availability rule sets scoped to the authenticated organization.

Requires features: planner.view

**Tags:** Planner

**Requires authentication.**

**Features:** planner.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| page | query | any | Optional |
| pageSize | query | any | Optional |
| ids | query | any | Optional. Comma-separated list of record UUIDs to filter by (max 200). |
| search | query | any | Optional |
| sortField | query | any | Optional |
| sortDir | query | any | Optional |

### Responses

**200** – Paginated availability rule sets

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": null,
      "organization_id": null,
      "tenant_id": null,
      "name": null,
      "description": null,
      "timezone": null,
      "created_at": null,
      "updated_at": null
    }
  ],
  "total": 1,
  "totalPages": 1
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/planner/availability-rule-sets?page=1&pageSize=50" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/planner/availability-rule-sets`

Create availability rule set

Creates a reusable availability rule set.

Requires features: planner.manage_availability

**Tags:** Planner

**Requires authentication.**

**Features:** planner.manage_availability

### Request Body

Content-Type: `application/json`

```json
{
  "tenantId": "00000000-0000-4000-8000-000000000000",
  "organizationId": "00000000-0000-4000-8000-000000000000",
  "name": "string",
  "description": null,
  "timezone": "string"
}
```

### Responses

**201** – Availability rule set created

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/planner/availability-rule-sets" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"tenantId\": \"00000000-0000-4000-8000-000000000000\",
  \"organizationId\": \"00000000-0000-4000-8000-000000000000\",
  \"name\": \"string\",
  \"description\": null,
  \"timezone\": \"string\"
}"
```

## PUT `/planner/availability-rule-sets`

Update availability rule set

Updates an availability rule set by id.

Requires features: planner.manage_availability

**Tags:** Planner

**Requires authentication.**

**Features:** planner.manage_availability

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – Availability rule set updated

Content-Type: `application/json`

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

### Example

```bash
curl -X PUT "https://crm.nf-hotel.pl/planner/availability-rule-sets" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\",
  \"description\": null
}"
```

## POST `/planner/availability-weekly`

Replace weekly availability

Replaces weekly availability rules for the subject in a single request.

**Tags:** Planner

**Requires authentication.**

### Request Body

Content-Type: `application/json`

```json
{
  "tenantId": "00000000-0000-4000-8000-000000000000",
  "organizationId": "00000000-0000-4000-8000-000000000000",
  "subjectType": "member",
  "subjectId": "00000000-0000-4000-8000-000000000000",
  "timezone": "string",
  "windows": []
}
```

### Responses

**200** – Weekly availability updated

Content-Type: `application/json`

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

**400** – Invalid payload

Content-Type: `application/json`

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

**403** – Forbidden

Content-Type: `application/json`

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

**409** – Optimistic lock conflict

Content-Type: `application/json`

```json
{
  "error": "string",
  "code": "optimistic_lock_conflict",
  "currentUpdatedAt": "string",
  "expectedUpdatedAt": "string"
}
```

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/planner/availability-weekly" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"tenantId\": \"00000000-0000-4000-8000-000000000000\",
  \"organizationId\": \"00000000-0000-4000-8000-000000000000\",
  \"subjectType\": \"member\",
  \"subjectId\": \"00000000-0000-4000-8000-000000000000\",
  \"timezone\": \"string\",
  \"windows\": []
}"
```

## GET `/progress/active`

GET /progress/active

Requires features: progress.view

**Tags:** Progress

**Requires authentication.**

**Features:** progress.view

### Responses

**200** – Success response

Content-Type: `application/json`

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/progress/active" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/progress/jobs`

List progressjobs

Returns a paginated collection of progressjobs scoped to the authenticated tenant.

Requires features: progress.view

**Tags:** Progress

**Requires authentication.**

**Features:** progress.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| status | query | any | Optional |
| jobType | query | any | Optional |
| parentJobId | query | any | Optional |
| includeCompleted | query | any | Optional |
| completedSince | query | any | Optional |
| page | query | any | Optional |
| pageSize | query | any | Optional |
| search | query | any | Optional |
| sortField | query | any | Optional |
| sortDir | query | any | Optional |
| ids | query | any | Optional. Comma-separated list of record UUIDs to filter by (max 200). |

### Responses

**200** – Paginated progressjobs

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "jobType": "string",
      "name": "string",
      "description": null,
      "status": "string",
      "progressPercent": 1,
      "processedCount": 1,
      "totalCount": null,
      "etaSeconds": null,
      "cancellable": true,
      "startedAt": null,
      "finishedAt": null,
      "errorMessage": null,
      "createdAt": null,
      "tenantId": "00000000-0000-4000-8000-000000000000",
      "organizationId": null
    }
  ],
  "total": 1,
  "totalPages": 1
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/progress/jobs?page=1&pageSize=20" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/progress/jobs`

Create progressjob

Creates a new progress job for tracking a long-running operation.

Requires features: progress.create

**Tags:** Progress

**Requires authentication.**

**Features:** progress.create

### Request Body

Content-Type: `application/json`

```json
{
  "jobType": "string",
  "name": "string",
  "cancellable": false
}
```

### Responses

**201** – ProgressJob created

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/progress/jobs" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"jobType\": \"string\",
  \"name\": \"string\",
  \"cancellable\": false
}"
```

## DELETE `/progress/jobs/{id}`

DELETE /progress/jobs/{id}

Requires features: progress.cancel

**Tags:** Progress

**Requires authentication.**

**Features:** progress.cancel

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

### Responses

**204** – Success

### Example

```bash
curl -X DELETE "https://crm.nf-hotel.pl/progress/jobs/:id" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/progress/jobs/{id}`

GET /progress/jobs/{id}

Requires features: progress.view

**Tags:** Progress

**Requires authentication.**

**Features:** progress.view

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

### Responses

**200** – Success response

Content-Type: `application/json`

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/progress/jobs/:id" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## PUT `/progress/jobs/{id}`

PUT /progress/jobs/{id}

Requires features: progress.update

**Tags:** Progress

**Requires authentication.**

**Features:** progress.update

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

### Responses

**200** – Success response

Content-Type: `application/json`

### Example

```bash
curl -X PUT "https://crm.nf-hotel.pl/progress/jobs/:id" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## 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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/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://crm.nf-hotel.pl/query_index/status" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## DELETE `/scheduler/jobs`

Delete scheduledjob

Deletes a scheduled job by ID.

Requires features: scheduler.jobs.manage

**Tags:** Scheduler

**Requires authentication.**

**Features:** scheduler.jobs.manage

### Request Body

Content-Type: `application/json`

```json
{
  "id": "string"
}
```

### Responses

**200** – ScheduledJob deleted

Content-Type: `application/json`

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

### Example

```bash
curl -X DELETE "https://crm.nf-hotel.pl/scheduler/jobs" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"string\"
}"
```

## GET `/scheduler/jobs`

List scheduledjobs

Returns a paginated collection of scheduledjobs scoped to the authenticated organization.

Requires features: scheduler.jobs.view

**Tags:** Scheduler

**Requires authentication.**

**Features:** scheduler.jobs.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| page | query | any | Optional |
| pageSize | query | any | Optional |
| id | query | any | Optional |
| search | query | any | Optional |
| scopeType | query | any | Optional |
| isEnabled | query | any | Required |
| sourceType | query | any | Optional |
| sourceModule | query | any | Optional |
| sort | query | any | Optional |
| order | query | any | Optional |
| ids | query | any | Optional. Comma-separated list of record UUIDs to filter by (max 200). |

### Responses

**200** – Paginated scheduledjobs

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "name": "string",
      "description": null,
      "scopeType": "system",
      "organizationId": null,
      "tenantId": null,
      "scheduleType": "cron",
      "scheduleValue": "string",
      "timezone": "string",
      "targetType": "queue",
      "targetQueue": null,
      "targetCommand": null,
      "targetPayload": null,
      "requireFeature": null,
      "isEnabled": true,
      "lastRunAt": null,
      "nextRunAt": null,
      "sourceType": "user",
      "sourceModule": null,
      "createdAt": "string",
      "updatedAt": "string"
    }
  ],
  "total": 1,
  "totalPages": 1
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/scheduler/jobs?page=1&pageSize=20" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/scheduler/jobs`

Create scheduledjob

Creates a new scheduled job with cron or interval-based scheduling.

Requires features: scheduler.jobs.manage

**Tags:** Scheduler

**Requires authentication.**

**Features:** scheduler.jobs.manage

### Request Body

Content-Type: `application/json`

```json
{
  "name": "string",
  "description": null,
  "scopeType": "system",
  "organizationId": null,
  "tenantId": null,
  "scheduleType": "cron",
  "scheduleValue": "string",
  "timezone": "UTC",
  "targetType": "queue",
  "targetQueue": null,
  "targetCommand": null,
  "targetPayload": null,
  "requireFeature": null,
  "isEnabled": true,
  "sourceType": "user",
  "sourceModule": null
}
```

### Responses

**201** – ScheduledJob created

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/scheduler/jobs" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"name\": \"string\",
  \"description\": null,
  \"scopeType\": \"system\",
  \"organizationId\": null,
  \"tenantId\": null,
  \"scheduleType\": \"cron\",
  \"scheduleValue\": \"string\",
  \"timezone\": \"UTC\",
  \"targetType\": \"queue\",
  \"targetQueue\": null,
  \"targetCommand\": null,
  \"targetPayload\": null,
  \"requireFeature\": null,
  \"isEnabled\": true,
  \"sourceType\": \"user\",
  \"sourceModule\": null
}"
```

## PUT `/scheduler/jobs`

Update scheduledjob

Updates an existing scheduled job by ID.

Requires features: scheduler.jobs.manage

**Tags:** Scheduler

**Requires authentication.**

**Features:** scheduler.jobs.manage

### Request Body

Content-Type: `application/json`

```json
{
  "id": "string",
  "description": null,
  "targetQueue": null,
  "targetCommand": null,
  "targetPayload": null,
  "requireFeature": null
}
```

### Responses

**200** – ScheduledJob updated

Content-Type: `application/json`

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

### Example

```bash
curl -X PUT "https://crm.nf-hotel.pl/scheduler/jobs" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"string\",
  \"description\": null,
  \"targetQueue\": null,
  \"targetCommand\": null,
  \"targetPayload\": null,
  \"requireFeature\": null
}"
```

## GET `/scheduler/jobs/{id}/executions`

Get execution history for a schedule

Fetch recent executions from BullMQ for a scheduled job. Requires QUEUE_STRATEGY=async.

**Tags:** Scheduler

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

### Responses

**200** – Execution history

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "string",
      "scheduleId": "00000000-0000-4000-8000-000000000000",
      "startedAt": "string",
      "finishedAt": null,
      "status": "running",
      "triggerType": "scheduled",
      "triggeredByUserId": null,
      "errorMessage": null,
      "errorStack": null,
      "durationMs": null,
      "queueJobId": "string",
      "queueName": "string",
      "attemptsMade": 1,
      "result": null
    }
  ],
  "total": 1,
  "page": 1,
  "pageSize": 1
}
```

**400** – Local strategy not supported

Content-Type: `application/json`

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

**401** – Unauthorized

Content-Type: `application/json`

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

**403** – Access denied

Content-Type: `application/json`

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

**404** – Schedule not found

Content-Type: `application/json`

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

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/scheduler/jobs/:id/executions?pageSize=20" \
  -H "Accept: application/json"
```

## GET `/scheduler/queue-jobs/{jobId}`

Get BullMQ job details and logs

Fetch detailed information and logs for a queue job. Requires QUEUE_STRATEGY=async.

**Tags:** Scheduler

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| jobId | path | any | Required |
| queue | query | any | Required |

### Responses

**200** – Job details and logs

Content-Type: `application/json`

```json
{
  "id": "string",
  "name": "string",
  "state": "waiting",
  "progress": null,
  "returnvalue": null,
  "failedReason": null,
  "stacktrace": null,
  "attemptsMade": 1,
  "processedOn": null,
  "finishedOn": null,
  "logs": [
    "string"
  ]
}
```

**400** – Invalid request or local strategy not supported

Content-Type: `application/json`

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

**401** – Unauthorized

Content-Type: `application/json`

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

**403** – Access denied

Content-Type: `application/json`

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

**404** – Job not found

Content-Type: `application/json`

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

**500** – Internal server error

Content-Type: `application/json`

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

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/scheduler/queue-jobs/:jobId?queue=string" \
  -H "Accept: application/json"
```

## GET `/scheduler/targets`

List available queues and commands

Returns all registered queue names (from module workers) and command IDs (from the command registry) that can be used as schedule targets.

**Tags:** Scheduler

### Responses

**200** – Available targets

Content-Type: `application/json`

```json
{
  "queues": [
    {
      "value": "string",
      "label": "string"
    }
  ],
  "commands": [
    {
      "value": "string",
      "label": "string"
    }
  ]
}
```

**401** – Unauthorized

Content-Type: `application/json`

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

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/scheduler/targets" \
  -H "Accept: application/json"
```

## POST `/scheduler/trigger`

Manually trigger a schedule

Executes a scheduled job immediately, bypassing the scheduled time. Only works with async queue strategy.

**Tags:** Scheduler

### Request Body

Content-Type: `application/json`

```json
{
  "id": "string"
}
```

### Responses

**200** – Schedule triggered successfully

Content-Type: `application/json`

```json
{
  "ok": true,
  "jobId": "string",
  "message": "string"
}
```

**400** – Invalid request or local strategy not supported

Content-Type: `application/json`

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

**401** – Unauthorized

Content-Type: `application/json`

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

**403** – Access denied

Content-Type: `application/json`

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

**404** – Schedule not found

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/scheduler/trigger" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"string\"
}"
```

## GET `/staff_attendance/calendar`

List staff attendance calendar events

Returns read-only employee attendance calendar entries aggregated from staff availability, planner unavailability rules, and staff leave requests.

Requires features: staff_attendance.view

**Tags:** Staff attendance

**Requires authentication.**

**Features:** staff_attendance.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| from | query | any | Required |
| to | query | any | Required |
| teamId | query | any | Optional |
| memberId | query | any | Optional |
| status | query | any | Optional |
| summaryYear | query | any | Optional |

### Responses

**200** – Attendance calendar payload.

Content-Type: `application/json`

```json
{
  "range": {
    "from": "string",
    "to": "string"
  },
  "days": [
    {
      "date": "string",
      "dayOfMonth": 1,
      "weekday": "string",
      "isWeekend": true,
      "isHoliday": true,
      "holidayName": null
    }
  ],
  "rows": [
    {
      "member": {
        "id": "00000000-0000-4000-8000-000000000000",
        "name": "string",
        "teamId": null,
        "teamName": null,
        "excludedFromAbsenceExport": true
      },
      "cells": [
        {
          "id": "string",
          "date": "string",
          "memberId": "00000000-0000-4000-8000-000000000000",
          "computedStatus": null,
          "manualStatus": null,
          "status": null,
          "reason": null,
          "comment": null,
          "isDuty": true,
          "source": null,
          "isManual": true,
          "entryId": null,
          "approvedLeave": true
        }
      ]
    }
  ],
  "items": [
    {
      "id": "string",
      "title": "string",
      "start": "string",
      "end": "string",
      "allDay": true,
      "status": "string",
      "memberId": "00000000-0000-4000-8000-000000000000",
      "memberName": "string",
      "teamId": null,
      "teamName": null,
      "reason": null,
      "source": "availability",
      "locked": true
    }
  ],
  "members": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "name": "string",
      "teamId": null,
      "teamName": null,
      "excludedFromAbsenceExport": true
    }
  ],
  "teams": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "name": "string"
    }
  ],
  "birthdays": {
    "key": "string"
  },
  "summary": {
    "key": 1
  },
  "statusDefinitions": [
    {
      "id": "string",
      "code": "string",
      "label": "string",
      "shortLabel": "string",
      "category": "string",
      "tone": "string",
      "icon": "string",
      "sortOrder": 1,
      "isSystem": true,
      "isManualSelectable": true,
      "includeInLeaveSummary": true,
      "deductsVacationEntitlement": true
    }
  ],
  "leaveSummary": [
    {
      "memberId": "00000000-0000-4000-8000-000000000000",
      "memberName": "string",
      "teamName": null,
      "entitlementDays": null,
      "presentDays": 1,
      "leaveStatusDays": {
        "key": 1
      },
      "vacationUsedDays": 1,
      "sickLeaveDays": 1,
      "vacationRemainingDays": null
    }
  ],
  "permissions": {
    "canManage": true
  }
}
```

**400** – Invalid date range.

Content-Type: `application/json`

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

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/staff_attendance/calendar?from=string&to=string" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## PUT `/staff_attendance/comments`

Update day comment

Creates, updates, or clears an encrypted comment for one employee/day attendance calendar cell.

Requires features: staff_attendance.manage

**Tags:** Staff attendance

**Requires authentication.**

**Features:** staff_attendance.manage

### Request Body

Content-Type: `application/json`

```json
{
  "memberId": "00000000-0000-4000-8000-000000000000",
  "date": "string",
  "comment": null
}
```

### Responses

**200** – Attendance comment was updated.

Content-Type: `application/json`

```json
{
  "ok": true,
  "entryId": null,
  "comment": null
}
```

**400** – Invalid payload.

Content-Type: `application/json`

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

**404** – Employee was not found.

Content-Type: `application/json`

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

### Example

```bash
curl -X PUT "https://crm.nf-hotel.pl/staff_attendance/comments" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"memberId\": \"00000000-0000-4000-8000-000000000000\",
  \"date\": \"string\",
  \"comment\": null
}"
```

## PUT `/staff_attendance/leave-entitlements`

Update leave entitlement

Creates, updates, or clears yearly vacation entitlement for a staff member.

Requires features: staff_attendance.manage

**Tags:** Staff attendance

**Requires authentication.**

**Features:** staff_attendance.manage

### Request Body

Content-Type: `application/json`

```json
{
  "memberId": "00000000-0000-4000-8000-000000000000",
  "year": 1,
  "annualVacationDays": null
}
```

### Responses

**200** – Leave entitlement was updated.

Content-Type: `application/json`

```json
{
  "ok": true,
  "entitlement": {
    "memberId": "00000000-0000-4000-8000-000000000000",
    "year": 1,
    "annualVacationDays": null
  }
}
```

**400** – Invalid payload.

Content-Type: `application/json`

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

**404** – Employee was not found.

Content-Type: `application/json`

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

**503** – Leave entitlement storage is not ready.

Content-Type: `application/json`

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

### Example

```bash
curl -X PUT "https://crm.nf-hotel.pl/staff_attendance/leave-entitlements" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"memberId\": \"00000000-0000-4000-8000-000000000000\",
  \"year\": 1,
  \"annualVacationDays\": null
}"
```

## POST `/staff_attendance/statuses/bulk`

Bulk update attendance statuses

Creates, updates, or clears manual attendance status overrides for employee/day cells.

Requires features: staff_attendance.manage

**Tags:** Staff attendance

**Requires authentication.**

**Features:** staff_attendance.manage

### Request Body

Content-Type: `application/json`

```json
{
  "entries": [
    {
      "memberId": "00000000-0000-4000-8000-000000000000",
      "date": "string",
      "status": null
    }
  ]
}
```

### Responses

**200** – Manual attendance statuses were updated.

Content-Type: `application/json`

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

**400** – Invalid payload.

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/staff_attendance/statuses/bulk" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"entries\": [
    {
      \"memberId\": \"00000000-0000-4000-8000-000000000000\",
      \"date\": \"string\",
      \"status\": null
    }
  ]
}"
```

## DELETE `/staff/activities`

Delete teammemberactivity

Deletes a team member activity.

Requires features: staff.manage_team

**Tags:** Staff

**Requires authentication.**

**Features:** staff.manage_team

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – TeamMemberActivity deleted

Content-Type: `application/json`

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

### Example

```bash
curl -X DELETE "https://crm.nf-hotel.pl/staff/activities" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/staff/activities`

List teammemberactivitys

Returns a paginated collection of teammemberactivitys scoped to the authenticated organization.

Requires features: staff.view

**Tags:** Staff

**Requires authentication.**

**Features:** staff.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| page | query | any | Optional |
| pageSize | query | any | Optional |
| entityId | query | any | Optional |
| sortField | query | any | Optional |
| sortDir | query | any | Optional |
| ids | query | any | Optional. Comma-separated list of record UUIDs to filter by (max 200). |

### Responses

**200** – Paginated teammemberactivitys

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "member_id": null,
      "activity_type": null,
      "subject": null,
      "body": null,
      "occurred_at": null,
      "author_user_id": null,
      "appearance_icon": null,
      "appearance_color": null,
      "organization_id": null,
      "tenant_id": null,
      "created_at": null,
      "updated_at": null
    }
  ],
  "total": 1,
  "totalPages": 1
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/staff/activities?page=1&pageSize=50" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/staff/activities`

Create teammemberactivity

Adds an activity to a team member timeline.

Requires features: staff.manage_team

**Tags:** Staff

**Requires authentication.**

**Features:** staff.manage_team

### Request Body

Content-Type: `application/json`

```json
{
  "tenantId": "00000000-0000-4000-8000-000000000000",
  "organizationId": "00000000-0000-4000-8000-000000000000",
  "entityId": "00000000-0000-4000-8000-000000000000",
  "activityType": "string",
  "appearanceIcon": null,
  "appearanceColor": null
}
```

### Responses

**201** – TeamMemberActivity created

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/staff/activities" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"tenantId\": \"00000000-0000-4000-8000-000000000000\",
  \"organizationId\": \"00000000-0000-4000-8000-000000000000\",
  \"entityId\": \"00000000-0000-4000-8000-000000000000\",
  \"activityType\": \"string\",
  \"appearanceIcon\": null,
  \"appearanceColor\": null
}"
```

## PUT `/staff/activities`

Update teammemberactivity

Updates a team member activity.

Requires features: staff.manage_team

**Tags:** Staff

**Requires authentication.**

**Features:** staff.manage_team

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – TeamMemberActivity updated

Content-Type: `application/json`

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

### Example

```bash
curl -X PUT "https://crm.nf-hotel.pl/staff/activities" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\",
  \"appearanceIcon\": null,
  \"appearanceColor\": null
}"
```

## DELETE `/staff/addresses`

Delete teammemberaddress

Deletes a team member address.

Requires features: staff.manage_team

**Tags:** Staff

**Requires authentication.**

**Features:** staff.manage_team

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – TeamMemberAddress deleted

Content-Type: `application/json`

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

### Example

```bash
curl -X DELETE "https://crm.nf-hotel.pl/staff/addresses" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/staff/addresses`

List teammemberaddresss

Returns a paginated collection of teammemberaddresss scoped to the authenticated organization.

Requires features: staff.view

**Tags:** Staff

**Requires authentication.**

**Features:** staff.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| page | query | any | Optional |
| pageSize | query | any | Optional |
| entityId | query | any | Optional |
| sortField | query | any | Optional |
| sortDir | query | any | Optional |
| ids | query | any | Optional. Comma-separated list of record UUIDs to filter by (max 200). |

### Responses

**200** – Paginated teammemberaddresss

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "member_id": null,
      "name": null,
      "purpose": null,
      "company_name": null,
      "address_line1": null,
      "address_line2": null,
      "building_number": null,
      "flat_number": null,
      "city": null,
      "region": null,
      "postal_code": null,
      "country": null,
      "latitude": null,
      "longitude": null,
      "is_primary": null,
      "organization_id": null,
      "tenant_id": null,
      "created_at": null,
      "updated_at": null
    }
  ],
  "total": 1,
  "totalPages": 1
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/staff/addresses?page=1&pageSize=50" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/staff/addresses`

Create teammemberaddress

Adds a team member address.

Requires features: staff.manage_team

**Tags:** Staff

**Requires authentication.**

**Features:** staff.manage_team

### Request Body

Content-Type: `application/json`

```json
{
  "tenantId": "00000000-0000-4000-8000-000000000000",
  "organizationId": "00000000-0000-4000-8000-000000000000",
  "entityId": "00000000-0000-4000-8000-000000000000",
  "addressLine1": "string"
}
```

### Responses

**201** – TeamMemberAddress created

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/staff/addresses" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"tenantId\": \"00000000-0000-4000-8000-000000000000\",
  \"organizationId\": \"00000000-0000-4000-8000-000000000000\",
  \"entityId\": \"00000000-0000-4000-8000-000000000000\",
  \"addressLine1\": \"string\"
}"
```

## PUT `/staff/addresses`

Update teammemberaddress

Updates a team member address.

Requires features: staff.manage_team

**Tags:** Staff

**Requires authentication.**

**Features:** staff.manage_team

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – TeamMemberAddress updated

Content-Type: `application/json`

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

### Example

```bash
curl -X PUT "https://crm.nf-hotel.pl/staff/addresses" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## DELETE `/staff/comments`

Delete teammembercomment

Deletes a team member note.

Requires features: staff.manage_team

**Tags:** Staff

**Requires authentication.**

**Features:** staff.manage_team

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – TeamMemberComment deleted

Content-Type: `application/json`

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

### Example

```bash
curl -X DELETE "https://crm.nf-hotel.pl/staff/comments" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/staff/comments`

List teammembercomments

Returns a paginated collection of teammembercomments scoped to the authenticated organization.

Requires features: staff.view

**Tags:** Staff

**Requires authentication.**

**Features:** staff.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| page | query | any | Optional |
| pageSize | query | any | Optional |
| entityId | query | any | Optional |
| sortField | query | any | Optional |
| sortDir | query | any | Optional |
| ids | query | any | Optional. Comma-separated list of record UUIDs to filter by (max 200). |

### Responses

**200** – Paginated teammembercomments

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "member_id": null,
      "body": null,
      "author_user_id": null,
      "appearance_icon": null,
      "appearance_color": null,
      "organization_id": null,
      "tenant_id": null,
      "created_at": null,
      "updated_at": null
    }
  ],
  "total": 1,
  "totalPages": 1
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/staff/comments?page=1&pageSize=50" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/staff/comments`

Create teammembercomment

Adds a note to a team member timeline.

Requires features: staff.manage_team

**Tags:** Staff

**Requires authentication.**

**Features:** staff.manage_team

### Request Body

Content-Type: `application/json`

```json
{
  "tenantId": "00000000-0000-4000-8000-000000000000",
  "organizationId": "00000000-0000-4000-8000-000000000000",
  "entityId": "00000000-0000-4000-8000-000000000000",
  "body": "string",
  "appearanceIcon": null,
  "appearanceColor": null
}
```

### Responses

**201** – TeamMemberComment created

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/staff/comments" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"tenantId\": \"00000000-0000-4000-8000-000000000000\",
  \"organizationId\": \"00000000-0000-4000-8000-000000000000\",
  \"entityId\": \"00000000-0000-4000-8000-000000000000\",
  \"body\": \"string\",
  \"appearanceIcon\": null,
  \"appearanceColor\": null
}"
```

## PUT `/staff/comments`

Update teammembercomment

Updates a team member note.

Requires features: staff.manage_team

**Tags:** Staff

**Requires authentication.**

**Features:** staff.manage_team

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – TeamMemberComment updated

Content-Type: `application/json`

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

### Example

```bash
curl -X PUT "https://crm.nf-hotel.pl/staff/comments" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\",
  \"appearanceIcon\": null,
  \"appearanceColor\": null
}"
```

## DELETE `/staff/job-histories`

Delete teammemberjobhistory

Deletes a team member job history entry.

Requires features: staff.manage_team

**Tags:** Staff

**Requires authentication.**

**Features:** staff.manage_team

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – TeamMemberJobHistory deleted

Content-Type: `application/json`

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

### Example

```bash
curl -X DELETE "https://crm.nf-hotel.pl/staff/job-histories" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/staff/job-histories`

List teammemberjobhistorys

Returns a paginated collection of teammemberjobhistorys scoped to the authenticated organization.

Requires features: staff.view

**Tags:** Staff

**Requires authentication.**

**Features:** staff.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| page | query | any | Optional |
| pageSize | query | any | Optional |
| entityId | query | any | Optional |
| sortField | query | any | Optional |
| sortDir | query | any | Optional |
| ids | query | any | Optional. Comma-separated list of record UUIDs to filter by (max 200). |

### Responses

**200** – Paginated teammemberjobhistorys

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "member_id": null,
      "name": null,
      "company_name": null,
      "description": null,
      "start_date": null,
      "end_date": null,
      "organization_id": null,
      "tenant_id": null,
      "created_at": null,
      "updated_at": null
    }
  ],
  "total": 1,
  "totalPages": 1
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/staff/job-histories?page=1&pageSize=50" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/staff/job-histories`

Create teammemberjobhistory

Adds a team member job history entry.

Requires features: staff.manage_team

**Tags:** Staff

**Requires authentication.**

**Features:** staff.manage_team

### Request Body

Content-Type: `application/json`

```json
{
  "tenantId": "00000000-0000-4000-8000-000000000000",
  "organizationId": "00000000-0000-4000-8000-000000000000",
  "entityId": "00000000-0000-4000-8000-000000000000",
  "name": "string",
  "companyName": null,
  "description": null,
  "startDate": "2025-01-01T00:00:00.000Z",
  "endDate": null
}
```

### Responses

**201** – TeamMemberJobHistory created

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/staff/job-histories" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"tenantId\": \"00000000-0000-4000-8000-000000000000\",
  \"organizationId\": \"00000000-0000-4000-8000-000000000000\",
  \"entityId\": \"00000000-0000-4000-8000-000000000000\",
  \"name\": \"string\",
  \"companyName\": null,
  \"description\": null,
  \"startDate\": \"2025-01-01T00:00:00.000Z\",
  \"endDate\": null
}"
```

## PUT `/staff/job-histories`

Update teammemberjobhistory

Updates a team member job history entry.

Requires features: staff.manage_team

**Tags:** Staff

**Requires authentication.**

**Features:** staff.manage_team

### Request Body

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "companyName": null,
  "description": null,
  "endDate": null
}
```

### Responses

**200** – TeamMemberJobHistory updated

Content-Type: `application/json`

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

### Example

```bash
curl -X PUT "https://crm.nf-hotel.pl/staff/job-histories" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\",
  \"companyName\": null,
  \"description\": null,
  \"endDate\": null
}"
```

## DELETE `/staff/leave-requests`

Delete leave request

Deletes a leave request by id.

**Tags:** Staff

**Requires authentication.**

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – Leave request deleted

Content-Type: `application/json`

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

### Example

```bash
curl -X DELETE "https://crm.nf-hotel.pl/staff/leave-requests" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/staff/leave-requests`

List leave requests

Returns a paginated collection of leave requests scoped to the authenticated organization.

**Tags:** Staff

**Requires authentication.**

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| page | query | any | Optional |
| pageSize | query | any | Optional |
| search | query | any | Optional |
| status | query | any | Optional |
| memberId | query | any | Optional |
| ids | query | any | Optional. Comma-separated list of record UUIDs to filter by (max 200). |
| sortField | query | any | Optional |
| sortDir | query | any | Optional |

### Responses

**200** – Paginated leave requests

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": null,
      "organization_id": null,
      "tenant_id": null,
      "member_id": null,
      "start_date": null,
      "end_date": null,
      "timezone": null,
      "status": null,
      "unavailability_reason_entry_id": null,
      "unavailability_reason_value": null,
      "note": null,
      "decision_comment": null,
      "submitted_by_user_id": null,
      "decided_by_user_id": null,
      "decided_at": null,
      "created_at": null,
      "updated_at": null,
      "member": null
    }
  ],
  "total": 1,
  "totalPages": 1
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/staff/leave-requests?page=1&pageSize=50" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/staff/leave-requests`

Create leave request

Creates a leave request for a staff member.

**Tags:** Staff

**Requires authentication.**

### Request Body

Content-Type: `application/json`

```json
{
  "tenantId": "00000000-0000-4000-8000-000000000000",
  "organizationId": "00000000-0000-4000-8000-000000000000",
  "memberId": "00000000-0000-4000-8000-000000000000",
  "timezone": "string",
  "startDate": "2025-01-01T00:00:00.000Z",
  "endDate": "2025-01-01T00:00:00.000Z",
  "unavailabilityReasonEntryId": null,
  "unavailabilityReasonValue": null,
  "note": null,
  "submittedByUserId": null
}
```

### Responses

**201** – Leave request created

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/staff/leave-requests" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"tenantId\": \"00000000-0000-4000-8000-000000000000\",
  \"organizationId\": \"00000000-0000-4000-8000-000000000000\",
  \"memberId\": \"00000000-0000-4000-8000-000000000000\",
  \"timezone\": \"string\",
  \"startDate\": \"2025-01-01T00:00:00.000Z\",
  \"endDate\": \"2025-01-01T00:00:00.000Z\",
  \"unavailabilityReasonEntryId\": null,
  \"unavailabilityReasonValue\": null,
  \"note\": null,
  \"submittedByUserId\": null
}"
```

## PUT `/staff/leave-requests`

Update leave request

Updates a leave request by id.

**Tags:** Staff

**Requires authentication.**

### Request Body

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "unavailabilityReasonEntryId": null,
  "unavailabilityReasonValue": null,
  "note": null
}
```

### Responses

**200** – Leave request updated

Content-Type: `application/json`

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

### Example

```bash
curl -X PUT "https://crm.nf-hotel.pl/staff/leave-requests" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\",
  \"unavailabilityReasonEntryId\": null,
  \"unavailabilityReasonValue\": null,
  \"note\": null
}"
```

## POST `/staff/leave-requests/accept`

Approve leave request

Approves a leave request and adds unavailability rules for the staff member.

Requires features: staff.leave_requests.manage

**Tags:** Staff

**Requires authentication.**

**Features:** staff.leave_requests.manage

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – Leave request approved

Content-Type: `application/json`

```json
{
  "ok": true,
  "id": null
}
```

**400** – Invalid payload

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/staff/leave-requests/accept" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\",
  \"decisionComment\": null,
  \"decidedByUserId\": null
}"
```

## POST `/staff/leave-requests/reject`

Reject leave request

Rejects a leave request with an optional comment.

Requires features: staff.leave_requests.manage

**Tags:** Staff

**Requires authentication.**

**Features:** staff.leave_requests.manage

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – Leave request rejected

Content-Type: `application/json`

```json
{
  "ok": true,
  "id": null
}
```

**400** – Invalid payload

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/staff/leave-requests/reject" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\",
  \"decisionComment\": null,
  \"decidedByUserId\": null
}"
```

## DELETE `/staff/team-members`

Delete team member

Deletes a team member by id.

Requires features: staff.manage_team

**Tags:** Staff

**Requires authentication.**

**Features:** staff.manage_team

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – Team member deleted

Content-Type: `application/json`

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

### Example

```bash
curl -X DELETE "https://crm.nf-hotel.pl/staff/team-members" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/staff/team-members`

List team members

Returns a paginated collection of team members scoped to the authenticated organization.

Requires features: staff.view

**Tags:** Staff

**Requires authentication.**

**Features:** staff.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| page | query | any | Optional |
| pageSize | query | any | Optional |
| search | query | any | Optional |
| isActive | query | any | Optional |
| teamId | query | any | Optional |
| roleId | query | any | Optional |
| ids | query | any | Optional. Comma-separated list of record UUIDs to filter by (max 200). |
| sortField | query | any | Optional |
| sortDir | query | any | Optional |

### Responses

**200** – Paginated team members

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": null,
      "organization_id": null,
      "tenant_id": null,
      "team_id": null,
      "display_name": null,
      "description": null,
      "user_id": null,
      "availability_rule_set_id": null,
      "is_active": null,
      "created_at": null,
      "updated_at": null,
      "user": null,
      "team": null
    }
  ],
  "total": 1,
  "totalPages": 1
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/staff/team-members?page=1&pageSize=50" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/staff/team-members`

Create team member

Creates a team member for staff assignments.

Requires features: staff.manage_team

**Tags:** Staff

**Requires authentication.**

**Features:** staff.manage_team

### Request Body

Content-Type: `application/json`

```json
{
  "tenantId": "00000000-0000-4000-8000-000000000000",
  "organizationId": "00000000-0000-4000-8000-000000000000",
  "teamId": null,
  "displayName": "string",
  "description": null,
  "userId": null,
  "roleIds": [],
  "tags": [],
  "availabilityRuleSetId": null
}
```

### Responses

**201** – Team member created

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/staff/team-members" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"tenantId\": \"00000000-0000-4000-8000-000000000000\",
  \"organizationId\": \"00000000-0000-4000-8000-000000000000\",
  \"teamId\": null,
  \"displayName\": \"string\",
  \"description\": null,
  \"userId\": null,
  \"roleIds\": [],
  \"tags\": [],
  \"availabilityRuleSetId\": null
}"
```

## PUT `/staff/team-members`

Update team member

Updates a team member by id.

Requires features: staff.manage_team

**Tags:** Staff

**Requires authentication.**

**Features:** staff.manage_team

### Request Body

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "teamId": null,
  "description": null,
  "userId": null,
  "availabilityRuleSetId": null
}
```

### Responses

**200** – Team member updated

Content-Type: `application/json`

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

### Example

```bash
curl -X PUT "https://crm.nf-hotel.pl/staff/team-members" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\",
  \"teamId\": null,
  \"description\": null,
  \"userId\": null,
  \"availabilityRuleSetId\": null
}"
```

## GET `/staff/team-members/assignable`

List staff members that can be assigned from customer flows

Returns active staff members linked to auth users. Access requires either customers.roles.manage or customers.activities.manage. Owned by the staff module; consumed from customer flows via this canonical URL. Replaces the deprecated /api/customers/assignable-staff route.

Requires features: customers.roles.view

**Tags:** Staff

**Requires authentication.**

**Features:** customers.roles.view

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

### Responses

**200** – Assignable staff members

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "teamMemberId": "00000000-0000-4000-8000-000000000000",
      "userId": "00000000-0000-4000-8000-000000000000",
      "displayName": "string",
      "email": null,
      "teamName": null,
      "user": null,
      "team": null
    }
  ],
  "total": 1,
  "totalPages": 1
}
```

**400** – Invalid request

Content-Type: `application/json`

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

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/staff/team-members/assignable?page=1&pageSize=24" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/staff/team-members/self`

Get current user team member profile

Returns the staff team member linked to the current user, if any.

Requires features: staff.leave_requests.send

**Tags:** Staff

**Requires authentication.**

**Features:** staff.leave_requests.send

### Responses

**200** – Team member profile

Content-Type: `application/json`

```json
{
  "member": null
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/staff/team-members/self" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/staff/team-members/self`

Create current user team member profile

Creates a team member profile for the signed-in user.

Requires features: staff.leave_requests.send

**Tags:** Staff

**Requires authentication.**

**Features:** staff.leave_requests.send

### Request Body

Content-Type: `application/json`

```json
{
  "tenantId": "00000000-0000-4000-8000-000000000000",
  "organizationId": "00000000-0000-4000-8000-000000000000",
  "displayName": "string",
  "description": null
}
```

### Responses

**201** – Team member created

Content-Type: `application/json`

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

**400** – Invalid payload

Content-Type: `application/json`

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

**409** – Already exists

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/staff/team-members/self" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"tenantId\": \"00000000-0000-4000-8000-000000000000\",
  \"organizationId\": \"00000000-0000-4000-8000-000000000000\",
  \"displayName\": \"string\",
  \"description\": null
}"
```

## POST `/staff/team-members/tags/assign`

Assign team member tag

Assigns a tag to a staff team member.

Requires features: staff.manage_team

**Tags:** Staff

**Requires authentication.**

**Features:** staff.manage_team

### Request Body

Content-Type: `application/json`

```json
{
  "tenantId": "00000000-0000-4000-8000-000000000000",
  "organizationId": "00000000-0000-4000-8000-000000000000",
  "memberId": "00000000-0000-4000-8000-000000000000",
  "tag": "string"
}
```

### Responses

**201** – Tag assignment created

Content-Type: `application/json`

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

**400** – Invalid payload

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/staff/team-members/tags/assign" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"tenantId\": \"00000000-0000-4000-8000-000000000000\",
  \"organizationId\": \"00000000-0000-4000-8000-000000000000\",
  \"memberId\": \"00000000-0000-4000-8000-000000000000\",
  \"tag\": \"string\"
}"
```

## POST `/staff/team-members/tags/unassign`

Unassign team member tag

Removes a tag from a staff team member.

Requires features: staff.manage_team

**Tags:** Staff

**Requires authentication.**

**Features:** staff.manage_team

### Request Body

Content-Type: `application/json`

```json
{
  "tenantId": "00000000-0000-4000-8000-000000000000",
  "organizationId": "00000000-0000-4000-8000-000000000000",
  "memberId": "00000000-0000-4000-8000-000000000000",
  "tag": "string"
}
```

### Responses

**200** – Tag assignment removed

Content-Type: `application/json`

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

**400** – Invalid payload

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/staff/team-members/tags/unassign" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"tenantId\": \"00000000-0000-4000-8000-000000000000\",
  \"organizationId\": \"00000000-0000-4000-8000-000000000000\",
  \"memberId\": \"00000000-0000-4000-8000-000000000000\",
  \"tag\": \"string\"
}"
```

## DELETE `/staff/team-roles`

Delete team role

Deletes a team role by id.

Requires features: staff.manage_team

**Tags:** Staff

**Requires authentication.**

**Features:** staff.manage_team

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – Team role deleted

Content-Type: `application/json`

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

### Example

```bash
curl -X DELETE "https://crm.nf-hotel.pl/staff/team-roles" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/staff/team-roles`

List team roles

Returns a paginated collection of team roles scoped to the authenticated organization.

Requires features: staff.view

**Tags:** Staff

**Requires authentication.**

**Features:** staff.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| page | query | any | Optional |
| pageSize | query | any | Optional |
| search | query | any | Optional |
| ids | query | any | Optional. Comma-separated list of record UUIDs to filter by (max 200). |
| teamId | query | any | Optional |
| sortField | query | any | Optional |
| sortDir | query | any | Optional |

### Responses

**200** – Paginated team roles

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": null,
      "organization_id": null,
      "tenant_id": null,
      "team_id": null,
      "name": null,
      "description": null,
      "appearance_icon": null,
      "appearance_color": null,
      "created_at": null,
      "updated_at": null,
      "team": null,
      "memberCount": null
    }
  ],
  "total": 1,
  "totalPages": 1
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/staff/team-roles?page=1&pageSize=50" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/staff/team-roles`

Create team role

Creates a team role for staff team members.

Requires features: staff.manage_team

**Tags:** Staff

**Requires authentication.**

**Features:** staff.manage_team

### Request Body

Content-Type: `application/json`

```json
{
  "tenantId": "00000000-0000-4000-8000-000000000000",
  "organizationId": "00000000-0000-4000-8000-000000000000",
  "teamId": null,
  "name": "string",
  "description": null,
  "appearanceIcon": null,
  "appearanceColor": null
}
```

### Responses

**201** – Team role created

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/staff/team-roles" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"tenantId\": \"00000000-0000-4000-8000-000000000000\",
  \"organizationId\": \"00000000-0000-4000-8000-000000000000\",
  \"teamId\": null,
  \"name\": \"string\",
  \"description\": null,
  \"appearanceIcon\": null,
  \"appearanceColor\": null
}"
```

## PUT `/staff/team-roles`

Update team role

Updates a team role by id.

Requires features: staff.manage_team

**Tags:** Staff

**Requires authentication.**

**Features:** staff.manage_team

### Request Body

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "teamId": null,
  "description": null,
  "appearanceIcon": null,
  "appearanceColor": null
}
```

### Responses

**200** – Team role updated

Content-Type: `application/json`

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

### Example

```bash
curl -X PUT "https://crm.nf-hotel.pl/staff/team-roles" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\",
  \"teamId\": null,
  \"description\": null,
  \"appearanceIcon\": null,
  \"appearanceColor\": null
}"
```

## DELETE `/staff/teams`

Delete team

Deletes a staff team by id.

Requires features: staff.manage_team

**Tags:** Staff

**Requires authentication.**

**Features:** staff.manage_team

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – Team deleted

Content-Type: `application/json`

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

### Example

```bash
curl -X DELETE "https://crm.nf-hotel.pl/staff/teams" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/staff/teams`

List teams

Returns a paginated collection of teams scoped to the authenticated organization.

Requires features: staff.view

**Tags:** Staff

**Requires authentication.**

**Features:** staff.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| page | query | any | Optional |
| pageSize | query | any | Optional |
| search | query | any | Optional |
| ids | query | any | Optional. Comma-separated list of record UUIDs to filter by (max 200). |
| isActive | query | any | Optional |
| sortField | query | any | Optional |
| sortDir | query | any | Optional |

### Responses

**200** – Paginated teams

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": null,
      "organization_id": null,
      "tenant_id": null,
      "name": null,
      "description": null,
      "is_active": null,
      "created_at": null,
      "updated_at": null,
      "memberCount": null
    }
  ],
  "total": 1,
  "totalPages": 1
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/staff/teams?page=1&pageSize=50" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/staff/teams`

Create team

Creates a staff team.

Requires features: staff.manage_team

**Tags:** Staff

**Requires authentication.**

**Features:** staff.manage_team

### Request Body

Content-Type: `application/json`

```json
{
  "tenantId": "00000000-0000-4000-8000-000000000000",
  "organizationId": "00000000-0000-4000-8000-000000000000",
  "name": "string",
  "description": null
}
```

### Responses

**201** – Team created

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/staff/teams" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"tenantId\": \"00000000-0000-4000-8000-000000000000\",
  \"organizationId\": \"00000000-0000-4000-8000-000000000000\",
  \"name\": \"string\",
  \"description\": null
}"
```

## PUT `/staff/teams`

Update team

Updates a staff team by id.

Requires features: staff.manage_team

**Tags:** Staff

**Requires authentication.**

**Features:** staff.manage_team

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – Team updated

Content-Type: `application/json`

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

### Example

```bash
curl -X PUT "https://crm.nf-hotel.pl/staff/teams" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\",
  \"description\": null
}"
```

## GET `/staff/timesheets/my-projects`

List assigned time project memberships for the current user

Returns staff_time_project_members where the authenticated user is an active member. Used by the My Timesheets grid to resolve assigned project IDs (spec N+1 mitigation query 1).

Requires features: staff.timesheets.view

**Tags:** Staff

**Requires authentication.**

**Features:** staff.timesheets.view

### Responses

**200** – Assigned project memberships

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "time_project_id": "00000000-0000-4000-8000-000000000000",
      "staff_member_id": "00000000-0000-4000-8000-000000000000",
      "role": null,
      "status": null,
      "assigned_start_date": null,
      "assigned_end_date": null,
      "show_in_grid": true
    }
  ],
  "total": 1
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/staff/timesheets/my-projects" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## PATCH `/staff/timesheets/my-projects/{projectId}`

Toggle grid visibility for one of the caller's assigned time projects

Self-service endpoint. Only the authenticated user can toggle `show_in_grid` on their own active membership for the given project. Does not require the admin-only `staff.timesheets.projects.manage` feature.

Requires features: staff.timesheets.manage_own

**Tags:** Staff

**Requires authentication.**

**Features:** staff.timesheets.manage_own

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

### Request Body

Content-Type: `application/json`

```json
{
  "showInGrid": true
}
```

### Responses

**200** – Visibility updated

Content-Type: `application/json`

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

**400** – Invalid input

Content-Type: `application/json`

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

**404** – Project membership not found

Content-Type: `application/json`

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

### Example

```bash
curl -X PATCH "https://crm.nf-hotel.pl/staff/timesheets/my-projects/:projectId" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"showInGrid\": true
}"
```

## GET `/staff/timesheets/projects/kpis`

Aggregate KPIs for the timesheets Projects page

Returns a role-aware KPI payload. Users with `staff.timesheets.projects.manage` get the PM shape (portfolio totals, monthly team hours, active team member count). Other viewers get the Collaborator shape scoped to their own memberships and hours.

Requires features: staff.timesheets.projects.view

**Tags:** Staff

**Requires authentication.**

**Features:** staff.timesheets.projects.view

### Responses

**200** – PM or Collaborator KPIs

Content-Type: `application/json`

```json
{
  "role": "pm",
  "totals": {
    "total": 1,
    "active": 1,
    "onHold": 1,
    "completed": 1
  },
  "hoursWeek": {
    "current": 1,
    "previous": 1,
    "deltaPct": null
  },
  "hoursMonth": {
    "current": 1,
    "previous": 1,
    "deltaPct": null
  },
  "teamActive": {
    "count": 1
  },
  "assignedToMe": {
    "total": 1,
    "active": 1
  }
}
```

**500** – Aggregation failure

Content-Type: `application/json`

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

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/staff/timesheets/projects/kpis" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## DELETE `/staff/timesheets/time-entries`

Delete timeentry

Deletes a time entry by id.

Requires features: staff.timesheets.manage_own

**Tags:** Staff

**Requires authentication.**

**Features:** staff.timesheets.manage_own

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – TimeEntry deleted

Content-Type: `application/json`

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

### Example

```bash
curl -X DELETE "https://crm.nf-hotel.pl/staff/timesheets/time-entries" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/staff/timesheets/time-entries`

List timeentries

Returns a paginated collection of timeentries scoped to the authenticated organization.

Requires features: staff.timesheets.view

**Tags:** Staff

**Requires authentication.**

**Features:** staff.timesheets.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| page | query | any | Optional |
| pageSize | query | any | Optional |
| staffMemberId | query | any | Optional |
| from | query | any | Optional |
| to | query | any | Optional |
| projectId | query | any | Optional |
| ids | query | any | Optional. Comma-separated list of record UUIDs to filter by (max 200). |
| running | query | any | Optional |
| sortField | query | any | Optional |
| sortDir | query | any | Optional |

### Responses

**200** – Paginated timeentries

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": null,
      "organization_id": null,
      "tenant_id": null,
      "staff_member_id": null,
      "date": null,
      "duration_minutes": null,
      "started_at": null,
      "ended_at": null,
      "notes": null,
      "time_project_id": null,
      "customer_id": null,
      "deal_id": null,
      "order_id": null,
      "source": null,
      "created_at": null,
      "updated_at": null
    }
  ],
  "total": 1,
  "totalPages": 1
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/staff/timesheets/time-entries?page=1&pageSize=50" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/staff/timesheets/time-entries`

Create timeentry

Creates a time entry for a staff member.

Requires features: staff.timesheets.manage_own

**Tags:** Staff

**Requires authentication.**

**Features:** staff.timesheets.manage_own

### Request Body

Content-Type: `application/json`

```json
{
  "tenantId": "00000000-0000-4000-8000-000000000000",
  "organizationId": "00000000-0000-4000-8000-000000000000",
  "staffMemberId": "00000000-0000-4000-8000-000000000000",
  "date": "2025-01-01T00:00:00.000Z",
  "durationMinutes": 1,
  "startedAt": null,
  "endedAt": null,
  "timeProjectId": null,
  "customerId": null,
  "dealId": null,
  "orderId": null,
  "notes": null,
  "source": "manual"
}
```

### Responses

**201** – TimeEntry created

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/staff/timesheets/time-entries" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"tenantId\": \"00000000-0000-4000-8000-000000000000\",
  \"organizationId\": \"00000000-0000-4000-8000-000000000000\",
  \"staffMemberId\": \"00000000-0000-4000-8000-000000000000\",
  \"date\": \"2025-01-01T00:00:00.000Z\",
  \"durationMinutes\": 1,
  \"startedAt\": null,
  \"endedAt\": null,
  \"timeProjectId\": null,
  \"customerId\": null,
  \"dealId\": null,
  \"orderId\": null,
  \"notes\": null,
  \"source\": \"manual\"
}"
```

## PUT `/staff/timesheets/time-entries`

Update timeentry

Updates a time entry by id.

Requires features: staff.timesheets.manage_own

**Tags:** Staff

**Requires authentication.**

**Features:** staff.timesheets.manage_own

### Request Body

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "timeProjectId": null,
  "customerId": null,
  "dealId": null,
  "orderId": null,
  "notes": null
}
```

### Responses

**200** – TimeEntry updated

Content-Type: `application/json`

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

### Example

```bash
curl -X PUT "https://crm.nf-hotel.pl/staff/timesheets/time-entries" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\",
  \"timeProjectId\": null,
  \"customerId\": null,
  \"dealId\": null,
  \"orderId\": null,
  \"notes\": null
}"
```

## POST `/staff/timesheets/time-entries/{id}/segments`

Add a segment to a time entry

Creates a new work or break segment for the specified time entry.

Requires features: staff.timesheets.manage_own

**Tags:** Staff

**Requires authentication.**

**Features:** staff.timesheets.manage_own

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

### Request Body

Content-Type: `application/json`

```json
{
  "tenantId": "00000000-0000-4000-8000-000000000000",
  "organizationId": "00000000-0000-4000-8000-000000000000",
  "timeEntryId": "00000000-0000-4000-8000-000000000000",
  "startedAt": "2025-01-01T00:00:00.000Z",
  "endedAt": null,
  "segmentType": "work"
}
```

### Responses

**201** – Segment created

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "timeEntryId": "00000000-0000-4000-8000-000000000000",
  "startedAt": "string",
  "endedAt": null,
  "segmentType": "work",
  "createdAt": "string"
}
```

**400** – Invalid payload

Content-Type: `application/json`

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

**404** – Time entry not found

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/staff/timesheets/time-entries/:id/segments" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"tenantId\": \"00000000-0000-4000-8000-000000000000\",
  \"organizationId\": \"00000000-0000-4000-8000-000000000000\",
  \"timeEntryId\": \"00000000-0000-4000-8000-000000000000\",
  \"startedAt\": \"2025-01-01T00:00:00.000Z\",
  \"endedAt\": null,
  \"segmentType\": \"work\"
}"
```

## PATCH `/staff/timesheets/time-entries/{id}/segments/{segmentId}`

Update a time entry segment

Updates fields on an existing time entry segment (startedAt, endedAt, segmentType).

Requires features: staff.timesheets.manage_own

**Tags:** Staff

**Requires authentication.**

**Features:** staff.timesheets.manage_own

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

### Request Body

Content-Type: `application/json`

```json
{
  "endedAt": null
}
```

### Responses

**200** – Segment updated successfully

Content-Type: `application/json`

```json
{
  "ok": true,
  "item": {
    "id": "string",
    "timeEntryId": "string",
    "startedAt": "string",
    "endedAt": null,
    "segmentType": "work",
    "createdAt": "string",
    "updatedAt": "string"
  }
}
```

**400** – Invalid payload or missing segment id

Content-Type: `application/json`

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

**404** – Segment not found

Content-Type: `application/json`

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

### Example

```bash
curl -X PATCH "https://crm.nf-hotel.pl/staff/timesheets/time-entries/:id/segments/:segmentId" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"endedAt\": null
}"
```

## POST `/staff/timesheets/time-entries/{id}/timer-start`

Start timer for a time entry

Starts the timer on a time entry by setting startedAt and creating an initial work segment.

Requires features: staff.timesheets.manage_own

**Tags:** Staff

**Requires authentication.**

**Features:** staff.timesheets.manage_own

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

### Responses

**200** – Timer started

Content-Type: `application/json`

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

**404** – Time entry not found

Content-Type: `application/json`

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

**409** – Timer already started, or another timer is already running for this staff member

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/staff/timesheets/time-entries/:id/timer-start" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/staff/timesheets/time-entries/{id}/timer-stop`

Stop timer for a time entry

Stops the active timer segment, recalculates total work duration in minutes, and updates the time entry.

Requires features: staff.timesheets.manage_own

**Tags:** Staff

**Requires authentication.**

**Features:** staff.timesheets.manage_own

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

### Responses

**200** – Timer stopped

Content-Type: `application/json`

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

**404** – Time entry not found

Content-Type: `application/json`

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

**409** – No active timer segment

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/staff/timesheets/time-entries/:id/timer-stop" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/staff/timesheets/time-entries/bulk`

Bulk save time entries

Creates, updates, or soft-deletes multiple time entries in a single request. Entries with durationMinutes=0 and an existing id are soft-deleted.

Requires features: staff.timesheets.manage_own

**Tags:** Staff

**Requires authentication.**

**Features:** staff.timesheets.manage_own

### Request Body

Content-Type: `application/json`

```json
{
  "entries": [
    {
      "id": null,
      "date": "2025-01-01T00:00:00.000Z",
      "timeProjectId": "00000000-0000-4000-8000-000000000000",
      "durationMinutes": 1,
      "notes": null
    }
  ]
}
```

### Responses

**200** – Bulk save completed

Content-Type: `application/json`

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

**422** – Validation error

Content-Type: `application/json`

```json
{
  "ok": false,
  "errors": [
    {
      "path": "string",
      "message": "string"
    }
  ]
}
```

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/staff/timesheets/time-entries/bulk" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"entries\": [
    {
      \"id\": null,
      \"date\": \"2025-01-01T00:00:00.000Z\",
      \"timeProjectId\": \"00000000-0000-4000-8000-000000000000\",
      \"durationMinutes\": 1,
      \"notes\": null
    }
  ]
}"
```

## POST `/staff/timesheets/time-entries/start-timer`

Start a timesheet timer

Atomically creates a timer-sourced time entry and starts it (sets startedAt and creates the initial work segment) in a single transaction, so a partial failure cannot leave an orphaned, unstarted entry.

Requires features: staff.timesheets.manage_own

**Tags:** Staff

**Requires authentication.**

**Features:** staff.timesheets.manage_own

### Request Body

Content-Type: `application/json`

```json
{
  "tenantId": "00000000-0000-4000-8000-000000000000",
  "organizationId": "00000000-0000-4000-8000-000000000000",
  "staffMemberId": "00000000-0000-4000-8000-000000000000",
  "date": "2025-01-01T00:00:00.000Z",
  "timeProjectId": null,
  "notes": null
}
```

### Responses

**201** – Timer started

Content-Type: `application/json`

```json
{
  "ok": true,
  "id": null
}
```

**400** – Invalid payload

Content-Type: `application/json`

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

**409** – Another timer is already running for this staff member

Content-Type: `application/json`

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

**422** – Referenced time project not found or out of scope

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/staff/timesheets/time-entries/start-timer" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"tenantId\": \"00000000-0000-4000-8000-000000000000\",
  \"organizationId\": \"00000000-0000-4000-8000-000000000000\",
  \"staffMemberId\": \"00000000-0000-4000-8000-000000000000\",
  \"date\": \"2025-01-01T00:00:00.000Z\",
  \"timeProjectId\": null,
  \"notes\": null
}"
```

## DELETE `/staff/timesheets/time-projects`

Delete timeproject

Soft-deletes a time project by id.

Requires features: staff.timesheets.projects.manage

**Tags:** Staff

**Requires authentication.**

**Features:** staff.timesheets.projects.manage

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – TimeProject deleted

Content-Type: `application/json`

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

### Example

```bash
curl -X DELETE "https://crm.nf-hotel.pl/staff/timesheets/time-projects" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/staff/timesheets/time-projects`

List time projects

Returns a paginated collection of time projects scoped to the authenticated organization.

Requires features: staff.timesheets.projects.view

**Tags:** Staff

**Requires authentication.**

**Features:** staff.timesheets.projects.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| page | query | any | Optional |
| pageSize | query | any | Optional |
| q | query | any | Optional |
| ids | query | any | Optional. Comma-separated list of record UUIDs to filter by (max 200). |
| projectType | query | any | Optional |
| status | query | any | Optional |
| customerId | query | any | Optional |
| mine | query | any | Optional |
| include | query | any | Optional |
| sortField | query | any | Optional |
| sortDir | query | any | Optional |

### Responses

**200** – Paginated time projects

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": null,
      "organization_id": null,
      "tenant_id": null,
      "name": null,
      "code": null,
      "description": null,
      "project_type": null,
      "color": null,
      "status": null,
      "customer_id": null,
      "owner_user_id": null,
      "cost_center": null,
      "start_date": null,
      "created_at": null,
      "updated_at": null
    }
  ],
  "total": 1,
  "totalPages": 1
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/staff/timesheets/time-projects?page=1&pageSize=50" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/staff/timesheets/time-projects`

Create timeproject

Creates a time project.

Requires features: staff.timesheets.projects.manage

**Tags:** Staff

**Requires authentication.**

**Features:** staff.timesheets.projects.manage

### Request Body

Content-Type: `application/json`

```json
{
  "tenantId": "00000000-0000-4000-8000-000000000000",
  "organizationId": "00000000-0000-4000-8000-000000000000",
  "name": "string",
  "customerId": null,
  "code": "string",
  "description": null,
  "projectType": null,
  "color": null,
  "status": "active",
  "ownerUserId": null,
  "costCenter": null,
  "startDate": null
}
```

### Responses

**201** – TimeProject created

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/staff/timesheets/time-projects" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"tenantId\": \"00000000-0000-4000-8000-000000000000\",
  \"organizationId\": \"00000000-0000-4000-8000-000000000000\",
  \"name\": \"string\",
  \"customerId\": null,
  \"code\": \"string\",
  \"description\": null,
  \"projectType\": null,
  \"color\": null,
  \"status\": \"active\",
  \"ownerUserId\": null,
  \"costCenter\": null,
  \"startDate\": null
}"
```

## PUT `/staff/timesheets/time-projects`

Update timeproject

Updates a time project by id.

Requires features: staff.timesheets.projects.manage

**Tags:** Staff

**Requires authentication.**

**Features:** staff.timesheets.projects.manage

### Request Body

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "customerId": null,
  "description": null,
  "projectType": null,
  "color": null,
  "ownerUserId": null,
  "costCenter": null,
  "startDate": null
}
```

### Responses

**200** – TimeProject updated

Content-Type: `application/json`

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

### Example

```bash
curl -X PUT "https://crm.nf-hotel.pl/staff/timesheets/time-projects" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\",
  \"customerId\": null,
  \"description\": null,
  \"projectType\": null,
  \"color\": null,
  \"ownerUserId\": null,
  \"costCenter\": null,
  \"startDate\": null
}"
```

## DELETE `/staff/timesheets/time-projects/{id}/employees`

Delete timeprojectmember

Unassigns an employee from a time project.

Requires features: staff.timesheets.projects.manage

**Tags:** Staff

**Requires authentication.**

**Features:** staff.timesheets.projects.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** – TimeProjectMember deleted

Content-Type: `application/json`

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

### Example

```bash
curl -X DELETE "https://crm.nf-hotel.pl/staff/timesheets/time-projects/:id/employees" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/staff/timesheets/time-projects/{id}/employees`

List time project members

Returns a paginated collection of time project members scoped to the authenticated organization.

Requires features: staff.timesheets.projects.view

**Tags:** Staff

**Requires authentication.**

**Features:** staff.timesheets.projects.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| id | path | any | Required |
| page | query | any | Optional |
| pageSize | query | any | Optional |
| timeProjectId | query | any | Optional |
| status | query | any | Optional |
| sortField | query | any | Optional |
| sortDir | query | any | Optional |
| ids | query | any | Optional. Comma-separated list of record UUIDs to filter by (max 200). |

### Responses

**200** – Paginated time project members

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": null,
      "organization_id": null,
      "tenant_id": null,
      "time_project_id": null,
      "staff_member_id": null,
      "role": null,
      "status": null,
      "assigned_start_date": null,
      "assigned_end_date": null,
      "created_at": null,
      "updated_at": null
    }
  ],
  "total": 1,
  "totalPages": 1
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/staff/timesheets/time-projects/:id/employees?page=1&pageSize=50" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/staff/timesheets/time-projects/{id}/employees`

Create timeprojectmember

Assigns an employee to a time project.

Requires features: staff.timesheets.projects.manage

**Tags:** Staff

**Requires authentication.**

**Features:** staff.timesheets.projects.manage

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

### Request Body

Content-Type: `application/json`

```json
{
  "tenantId": "00000000-0000-4000-8000-000000000000",
  "organizationId": "00000000-0000-4000-8000-000000000000",
  "timeProjectId": "00000000-0000-4000-8000-000000000000",
  "staffMemberId": "00000000-0000-4000-8000-000000000000",
  "role": null,
  "status": "active",
  "assignedStartDate": "2025-01-01T00:00:00.000Z",
  "assignedEndDate": null
}
```

### Responses

**201** – TimeProjectMember created

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/staff/timesheets/time-projects/:id/employees" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"tenantId\": \"00000000-0000-4000-8000-000000000000\",
  \"organizationId\": \"00000000-0000-4000-8000-000000000000\",
  \"timeProjectId\": \"00000000-0000-4000-8000-000000000000\",
  \"staffMemberId\": \"00000000-0000-4000-8000-000000000000\",
  \"role\": null,
  \"status\": \"active\",
  \"assignedStartDate\": \"2025-01-01T00:00:00.000Z\",
  \"assignedEndDate\": null
}"
```

## GET `/stats/analiza`

Analizy portfela

Należności w koszykach wiekowych (stan na dziś), usługi kończące się w najbliższych 30/60/90 dniach wraz z miesięczną wartością w ryzyku, najwięksi Klienci okresu oraz wartość portfela (MRR) w rozbiciu na pakiety. Zakres widoczności rozstrzyga serwer.

**Tags:** Stats

**Requires authentication.**

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| from | query | any | Required |
| to | query | any | Required |
| scope | query | any | Optional |
| currency | query | any | Optional |
| issuerOrgIds | query | any | Optional |
| productIds | query | any | Optional |
| teamIds | query | any | Optional |
| userIds | query | any | Optional |
| paymentStatus | query | any | Optional |
| amountBasis | query | any | Optional |

### Responses

**200** – Analizy.

Content-Type: `application/json`

```json
{
  "wystawcy": [
    {
      "issuerOrgId": null,
      "nazwa": "string",
      "sprzedaz": "string",
      "faktur": 1,
      "nieoplacone": "string",
      "uslugTrwajacych": 1,
      "mrr": "string"
    }
  ],
  "naleznosci": [
    {
      "kosz": "biezace",
      "kwota": "string",
      "faktur": 1
    }
  ],
  "naleznosciRazem": {
    "kwota": "string",
    "faktur": 1
  },
  "odnowienia": [
    {
      "dni": 30,
      "uslug": 1,
      "obiektow": 1,
      "mrr": "string"
    }
  ],
  "topKlienci": [
    {
      "propertyId": null,
      "companyEntityId": null,
      "nazwa": "string",
      "firmaNazwa": null,
      "sprzedaz": "string",
      "faktur": 1,
      "nieoplacone": "string"
    }
  ],
  "portfel": {
    "mrr": "string",
    "pakiety": [
      {
        "productId": null,
        "nazwa": "string",
        "uslug": 1,
        "mrr": "string"
      }
    ]
  }
}
```

**400** – Błędne parametry.

Content-Type: `application/json`

**403** – Brak uprawnień stats.view.*.

Content-Type: `application/json`

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/stats/analiza?from=string&to=string" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/stats/breakdown`

Rozbicie per osoba i per zespół

Sprzedaż, faktury, rozmowy i realizacja targetu per pracownik oraz per zespół, za wskazany okres i przy zadanych filtrach. Sprzedaż bez przypisanej osoby (automat, import) wraca osobno jako `bezAutora`, żeby suma kolumny zgadzała się z kafelkami. Zakres widoczności rozstrzyga serwer.

**Tags:** Stats

**Requires authentication.**

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| from | query | any | Required |
| to | query | any | Required |
| scope | query | any | Optional |
| currency | query | any | Optional |
| issuerOrgIds | query | any | Optional |
| productIds | query | any | Optional |
| teamIds | query | any | Optional |
| userIds | query | any | Optional |
| paymentStatus | query | any | Optional |
| amountBasis | query | any | Optional |

### Responses

**200** – Rozbicie.

Content-Type: `application/json`

```json
{
  "osoby": [
    {
      "userId": null,
      "nazwa": "string",
      "teamId": null,
      "teamNazwa": null,
      "sprzedaz": "string",
      "sprzedazNetto": "string",
      "faktur": 1,
      "fakturOplaconych": 1,
      "fakturNieoplaconych": 1,
      "rozmowy": 1,
      "czasRozmowSek": 1,
      "target": null,
      "targetZrodlo": null,
      "realizacja": null
    }
  ],
  "zespoly": [
    {
      "nazwa": "string",
      "teamId": null,
      "sprzedaz": "string",
      "sprzedazNetto": "string",
      "faktur": 1,
      "fakturOplaconych": 1,
      "fakturNieoplaconych": 1,
      "rozmowy": 1,
      "czasRozmowSek": 1,
      "target": null,
      "targetZrodlo": null,
      "realizacja": null,
      "osob": 1
    }
  ],
  "bezAutora": {
    "sprzedaz": "string",
    "faktur": 1
  }
}
```

**400** – Błędne parametry.

Content-Type: `application/json`

**403** – Brak uprawnień stats.view.*.

Content-Type: `application/json`

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/stats/breakdown?from=string&to=string" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/stats/dictionaries`

Kategorie i produkty do wyboru w regule

Requires features: stats.rules.manage

**Tags:** Stats

**Requires authentication.**

**Features:** stats.rules.manage

### Responses

**200** – Słowniki.

Content-Type: `application/json`

```json
{
  "kategorie": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "nazwa": "string"
    }
  ],
  "produkty": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "nazwa": "string",
      "sku": null
    }
  ]
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/stats/dictionaries" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/stats/dzial`

Szczegóły działu

Rozbicie przychodu POJEDYNCZEGO działu: wkład każdego pracownika (pozycje faktur przypisane do reguły, grupowane po autorze faktury), produkty składające się na przychód, przebieg w czasie oraz realizacja targetu zespołu przypisanego do działu wraz z kwotą, której brakuje. Zakres widoczności rozstrzyga serwer.

Requires features: stats.view.own

**Tags:** Stats

**Requires authentication.**

**Features:** stats.view.own

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| from | query | any | Required |
| to | query | any | Required |
| scope | query | any | Optional |
| currency | query | any | Optional |
| issuerOrgIds | query | any | Optional |
| productIds | query | any | Optional |
| teamIds | query | any | Optional |
| userIds | query | any | Optional |
| paymentStatus | query | any | Optional |
| amountBasis | query | any | Optional |
| ruleId | query | any | Required |
| granularity | query | any | Optional |

### Responses

**200** – Wynik.

Content-Type: `application/json`

```json
{
  "ruleId": "00000000-0000-4000-8000-000000000000",
  "nazwa": "string",
  "opis": null,
  "teamId": null,
  "teamNazwa": null,
  "zFaktur": "string",
  "pozycji": 1,
  "zWpisow": "string",
  "wpisow": 1,
  "razem": "string",
  "target": null,
  "realizacja": null,
  "brakuje": null,
  "osoby": [
    {
      "userId": null,
      "nazwa": "string",
      "kwota": "string",
      "pozycji": 1,
      "udzial": 1
    }
  ],
  "bezAutora": {
    "kwota": "string",
    "pozycji": 1
  },
  "produkty": [
    {
      "nazwa": "string",
      "sku": null,
      "kwota": "string",
      "pozycji": 1,
      "udzial": 1
    }
  ],
  "szereg": [
    {
      "okres": "string",
      "kwota": "string"
    }
  ]
}
```

**400** – Błędne parametry.

Content-Type: `application/json`

**404** – Nie ma takiej reguły.

Content-Type: `application/json`

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/stats/dzial?from=string&to=string&ruleId=00000000-0000-4000-8000-000000000000&granularity=day" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/stats/kategorie`

Sprzedaż wg kategorii produktów

Odpowiada na pytanie „na czym zarabiamy" — inaczej niż reguły działów, które mówią „ile zarobił dział" po wykluczeniach. Kategoria rozstrzygana tą samą kaskadą (pozycja → produkt → nagłówek → usługa), więc obie liczby są spójne. Zawiera udział procentowy i wartość z poprzedniego, równie długiego okresu.

**Tags:** Stats

**Requires authentication.**

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| from | query | any | Required |
| to | query | any | Required |
| scope | query | any | Optional |
| currency | query | any | Optional |
| issuerOrgIds | query | any | Optional |
| productIds | query | any | Optional |
| teamIds | query | any | Optional |
| userIds | query | any | Optional |
| paymentStatus | query | any | Optional |
| amountBasis | query | any | Optional |

### Responses

**200** – Rozbicie na kategorie.

Content-Type: `application/json`

```json
{
  "razem": "string",
  "kategorie": [
    {
      "categoryId": null,
      "nazwa": "string",
      "sprzedaz": "string",
      "pozycji": 1,
      "udzial": 1,
      "poprzednio": null,
      "produkty": [
        {
          "productId": null,
          "nazwa": "string",
          "sku": null,
          "sprzedaz": "string",
          "pozycji": 1,
          "udzialWKategorii": 1
        }
      ]
    }
  ]
}
```

**400** – Błędne parametry.

Content-Type: `application/json`

**403** – Brak uprawnień stats.view.*.

Content-Type: `application/json`

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/stats/kategorie?from=string&to=string" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## DELETE `/stats/manual-entries`

Usuń wpis

Przychód rozpoznany datą wykonania, a nie datą faktury — tak wchodzi budowa strony, która liczy się dopiero po ukończeniu. Opcjonalne powiązanie z fakturą jest wyłącznie śladem; żeby nie liczyć przychodu dwa razy, produkty rozpoznawane ręcznie trzeba wykluczyć z reguły.

Requires features: stats.rules.manage

**Tags:** Stats

**Requires authentication.**

**Features:** stats.rules.manage

### Responses

**200** – Usunięto.

Content-Type: `application/json`

### Example

```bash
curl -X DELETE "https://crm.nf-hotel.pl/stats/manual-entries" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/stats/manual-entries`

Lista wpisów

Przychód rozpoznany datą wykonania, a nie datą faktury — tak wchodzi budowa strony, która liczy się dopiero po ukończeniu. Opcjonalne powiązanie z fakturą jest wyłącznie śladem; żeby nie liczyć przychodu dwa razy, produkty rozpoznawane ręcznie trzeba wykluczyć z reguły.

Requires features: stats.view.own

**Tags:** Stats

**Requires authentication.**

**Features:** stats.view.own

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| from | query | any | Optional |
| to | query | any | Optional |
| ruleId | query | any | Optional |

### Responses

**200** – Wpisy.

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "ruleId": "00000000-0000-4000-8000-000000000000",
      "recognizedAt": "string",
      "description": "string",
      "amountNet": "string",
      "currencyCode": "string",
      "userId": null,
      "invoiceId": null,
      "invoiceNumber": null
    }
  ]
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/stats/manual-entries" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/stats/manual-entries`

Dodaj wpis

Przychód rozpoznany datą wykonania, a nie datą faktury — tak wchodzi budowa strony, która liczy się dopiero po ukończeniu. Opcjonalne powiązanie z fakturą jest wyłącznie śladem; żeby nie liczyć przychodu dwa razy, produkty rozpoznawane ręcznie trzeba wykluczyć z reguły.

Requires features: stats.rules.manage

**Tags:** Stats

**Requires authentication.**

**Features:** stats.rules.manage

### Responses

**201** – Utworzono.

Content-Type: `application/json`

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/stats/manual-entries" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## PUT `/stats/manual-entries`

Zmień wpis

Przychód rozpoznany datą wykonania, a nie datą faktury — tak wchodzi budowa strony, która liczy się dopiero po ukończeniu. Opcjonalne powiązanie z fakturą jest wyłącznie śladem; żeby nie liczyć przychodu dwa razy, produkty rozpoznawane ręcznie trzeba wykluczyć z reguły.

Requires features: stats.rules.manage

**Tags:** Stats

**Requires authentication.**

**Features:** stats.rules.manage

### Responses

**200** – Zapisano.

Content-Type: `application/json`

### Example

```bash
curl -X PUT "https://crm.nf-hotel.pl/stats/manual-entries" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/stats/my-invoices`

Moje faktury ze statusem płatności

Realizuje wymaganie „pracownik widzi, który Klient już zapłacił fakturę, którą wystawił". Domyślnie zakres `own`; szerszy wymaga odpowiedniego uprawnienia i i tak jest zawężany serwerowo.

**Tags:** Stats

**Requires authentication.**

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| from | query | any | Required |
| to | query | any | Required |
| scope | query | any | Optional |
| userId | query | any | Optional |
| onlyUnpaid | query | any | Optional |
| page | query | any | Optional |
| pageSize | query | any | Optional |

### Responses

**200** – Strona wyników.

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "numer": null,
      "nabywca": null,
      "propertyId": null,
      "propertyNazwa": null,
      "companyEntityId": null,
      "dataWystawienia": null,
      "terminPlatnosci": null,
      "brutto": "string",
      "statusPlatnosci": "string",
      "dataZaplaty": null
    }
  ],
  "totalCount": 1,
  "page": 1,
  "pageSize": 1
}
```

**400** – Błędne parametry.

Content-Type: `application/json`

**403** – Brak uprawnień stats.view.*.

Content-Type: `application/json`

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/stats/my-invoices?from=string&to=string&page=1&pageSize=25" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/stats/owners`

Zespoły i pracownicy do wyboru

Lista zespołów oraz członków mających powiązane konto użytkownika. Zwraca też liczbę członków BEZ konta — takim nie da się przypisać targetu, bo autorstwo dokumentów wskazuje na użytkownika.

Requires features: stats.view.own

**Tags:** Stats

**Requires authentication.**

**Features:** stats.view.own

### Responses

**200** – Kandydaci.

Content-Type: `application/json`

```json
{
  "zespoly": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "nazwa": "string"
    }
  ],
  "pracownicy": [
    {
      "userId": "00000000-0000-4000-8000-000000000000",
      "nazwa": "string",
      "teamId": null
    }
  ],
  "nieprzypisani": 1,
  "wystawcy": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "nazwa": "string"
    }
  ]
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/stats/owners" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/stats/rentownosc`

Przychód działów

Przychód liczony z POZYCJI faktur (jedna faktura potrafi mieszać kategorie), z kategorią rozstrzyganą kaskadą: kategoria pozycji → produkt pozycji → nagłówek faktury → kategoria usługi → produkt usługi. Do tego ręczne wpisy przychodu rozpoznane datą wykonania. Zwraca też kwoty poza regułami i bez kategorii, żeby suma działów dała się pogodzić z sumą sprzedaży.

**Tags:** Stats

**Requires authentication.**

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| from | query | any | Required |
| to | query | any | Required |
| scope | query | any | Optional |
| currency | query | any | Optional |
| issuerOrgIds | query | any | Optional |
| productIds | query | any | Optional |
| teamIds | query | any | Optional |
| userIds | query | any | Optional |
| paymentStatus | query | any | Optional |
| amountBasis | query | any | Optional |

### Responses

**200** – Wynik.

Content-Type: `application/json`

```json
{
  "reguly": [
    {
      "ruleId": "00000000-0000-4000-8000-000000000000",
      "nazwa": "string",
      "zFaktur": "string",
      "pozycji": 1,
      "zWpisow": "string",
      "wpisow": 1,
      "razem": "string"
    }
  ],
  "pozaRegulami": {
    "kwota": "string",
    "pozycji": 1
  },
  "bezKategorii": {
    "kwota": "string",
    "pozycji": 1
  }
}
```

**400** – Błędne parametry.

Content-Type: `application/json`

**403** – Brak uprawnień stats.view.*.

Content-Type: `application/json`

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/stats/rentownosc?from=string&to=string" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## DELETE `/stats/rules`

Usuń regułę

Definicje działów: które kategorie produktów wchodzą, które produkty i wzorce SKU są wykluczone (domeny, serwery, pozycje rozpoznawane ręcznie) oraz czy doliczać ręczne wpisy przychodu.

Requires features: stats.rules.manage

**Tags:** Stats

**Requires authentication.**

**Features:** stats.rules.manage

### Responses

**200** – Usunięto.

Content-Type: `application/json`

### Example

```bash
curl -X DELETE "https://crm.nf-hotel.pl/stats/rules" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/stats/rules`

Lista reguł

Definicje działów: które kategorie produktów wchodzą, które produkty i wzorce SKU są wykluczone (domeny, serwery, pozycje rozpoznawane ręcznie) oraz czy doliczać ręczne wpisy przychodu.

Requires features: stats.view.own

**Tags:** Stats

**Requires authentication.**

**Features:** stats.view.own

### Responses

**200** – Reguły.

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "name": "string",
      "teamId": null,
      "teamName": null,
      "description": null,
      "includeCategoryIds": [
        "00000000-0000-4000-8000-000000000000"
      ],
      "excludeProductIds": [
        "00000000-0000-4000-8000-000000000000"
      ],
      "excludeSkuPatterns": [
        "string"
      ],
      "includeManualEntries": true,
      "isActive": true
    }
  ]
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/stats/rules" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/stats/rules`

Dodaj regułę

Definicje działów: które kategorie produktów wchodzą, które produkty i wzorce SKU są wykluczone (domeny, serwery, pozycje rozpoznawane ręcznie) oraz czy doliczać ręczne wpisy przychodu.

Requires features: stats.rules.manage

**Tags:** Stats

**Requires authentication.**

**Features:** stats.rules.manage

### Responses

**201** – Utworzono.

Content-Type: `application/json`

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/stats/rules" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## PUT `/stats/rules`

Zmień regułę

Definicje działów: które kategorie produktów wchodzą, które produkty i wzorce SKU są wykluczone (domeny, serwery, pozycje rozpoznawane ręcznie) oraz czy doliczać ręczne wpisy przychodu.

Requires features: stats.rules.manage

**Tags:** Stats

**Requires authentication.**

**Features:** stats.rules.manage

### Responses

**200** – Zapisano.

Content-Type: `application/json`

### Example

```bash
curl -X PUT "https://crm.nf-hotel.pl/stats/rules" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/stats/rules/defaults`

Załóż domyślne działy

Tworzy startowy zestaw reguł, dopasowując je po nazwie do istniejących zespołów, kategorii i produktów. Idempotentny — dział o istniejącej nazwie jest pomijany, nigdy nadpisywany, więc ponowne uruchomienie nie kasuje ręcznych zmian.

Requires features: stats.rules.manage

**Tags:** Stats

**Requires authentication.**

**Features:** stats.rules.manage

### Responses

**200** – Co utworzono, a co pominięto.

Content-Type: `application/json`

```json
{
  "utworzone": [
    "string"
  ],
  "pominiete": [
    "string"
  ]
}
```

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/stats/rules/defaults" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/stats/series`

Szereg czasowy sprzedaży

Sprzedaż brutto albo netto (`amountBasis`, domyślnie brutto; wystawiona i opłacona) oraz liczba faktur w kubełkach dziennych, tygodniowych lub miesięcznych. Zakres widoczności rozstrzyga serwer, tak samo jak w /api/stats/summary.

**Tags:** Stats

**Requires authentication.**

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| from | query | any | Required |
| to | query | any | Required |
| scope | query | any | Optional |
| currency | query | any | Optional |
| issuerOrgIds | query | any | Optional |
| productIds | query | any | Optional |
| teamIds | query | any | Optional |
| userIds | query | any | Optional |
| paymentStatus | query | any | Optional |
| amountBasis | query | any | Optional |
| granularity | query | any | Optional |

### Responses

**200** – Punkty szeregu.

Content-Type: `application/json`

```json
{
  "granularity": "day",
  "punkty": [
    {
      "okres": "string",
      "wystawiona": 1,
      "oplacona": 1,
      "faktur": 1
    }
  ]
}
```

**400** – Błędne parametry.

Content-Type: `application/json`

**403** – Brak uprawnień stats.view.*.

Content-Type: `application/json`

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/stats/series?from=string&to=string&granularity=day" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/stats/summary`

Podsumowanie statystyk

Kafelki panelu statystyk: sprzedaż brutto albo netto (`amountBasis`, domyślnie brutto; wystawiona/opłacona/do zapłaty), liczby faktur wg statusu płatności, aktywne usługi z podziałem na pakiety oraz nowi Klienci i rezygnacje. Target i jego realizacja są zawsze netto. Bez parametru `currency` kwoty liczone są w walucie dominującej w okresie. Zakres widoczności (own/team/all) rozstrzyga serwer na podstawie uprawnień; parametr `scope` może go wyłącznie zawęzić.

**Tags:** Stats

**Requires authentication.**

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| from | query | any | Required |
| to | query | any | Required |
| scope | query | any | Optional |
| currency | query | any | Optional |
| issuerOrgIds | query | any | Optional |
| productIds | query | any | Optional |
| teamIds | query | any | Optional |
| userIds | query | any | Optional |
| paymentStatus | query | any | Optional |
| amountBasis | query | any | Optional |

### Responses

**200** – Podsumowanie za wskazany okres.

Content-Type: `application/json`

```json
{
  "okres": {
    "from": "string",
    "to": "string"
  },
  "waluta": "string",
  "podstawa": "gross",
  "waluty": [
    {
      "kod": "string",
      "faktur": 1
    }
  ],
  "zakres": "own",
  "sprzedaz": {
    "wystawiona": "string",
    "oplacona": "string",
    "nieoplacona": "string",
    "fakturWystawionych": 1,
    "fakturOplaconych": 1,
    "fakturNieoplaconych": 1,
    "fakturPrzeterminowanych": 1
  },
  "aktywneUslugi": {
    "razem": 1,
    "obiektow": 1,
    "firm": 1,
    "pakiety": [
      {
        "productId": null,
        "nazwa": "string",
        "ile": 1
      }
    ]
  },
  "klienci": {
    "nowi": 1,
    "rezygnacje": 1
  },
  "target": {
    "kwota": null,
    "realizacja": null
  },
  "poprzedni": null
}
```

**400** – Błędne parametry zakresu dat.

Content-Type: `application/json`

**403** – Brak któregokolwiek z uprawnień stats.view.*.

Content-Type: `application/json`

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/stats/summary?from=string&to=string" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## DELETE `/stats/targets`

Usuń target

Target kwotowy (sprzedaż netto) ustawiany na zespół ALBO na pracownika. Wpis obowiązuje od swojego miesiąca w przód, aż do kolejnego wpisu tego samego właściciela, a osoba bez własnego targetu dziedziczy równą część targetu zespołu (po odjęciu targetów imiennych). Odczyt wymaga dostępu do panelu, zapis osobnego uprawnienia stats.targets.manage.

Requires features: stats.targets.manage

**Tags:** Stats

**Requires authentication.**

**Features:** stats.targets.manage

### Responses

**200** – Usunięto.

Content-Type: `application/json`

### Example

```bash
curl -X DELETE "https://crm.nf-hotel.pl/stats/targets" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/stats/targets`

Targety obowiązujące w danym miesiącu (z dziedziczonymi)

Target kwotowy (sprzedaż netto) ustawiany na zespół ALBO na pracownika. Wpis obowiązuje od swojego miesiąca w przód, aż do kolejnego wpisu tego samego właściciela, a osoba bez własnego targetu dziedziczy równą część targetu zespołu (po odjęciu targetów imiennych). Odczyt wymaga dostępu do panelu, zapis osobnego uprawnienia stats.targets.manage.

Requires features: stats.view.own

**Tags:** Stats

**Requires authentication.**

**Features:** stats.view.own

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

### Responses

**200** – Lista targetów.

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": null,
      "teamId": null,
      "teamName": null,
      "userId": null,
      "periodMonth": "string",
      "metric": "string",
      "amount": "string",
      "currencyCode": "string",
      "zrodlo": "wlasny",
      "zMiesiaca": "string"
    }
  ]
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/stats/targets?month=string" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/stats/targets`

Ustaw target na miesiąc (upsert)

Target kwotowy (sprzedaż netto) ustawiany na zespół ALBO na pracownika. Wpis obowiązuje od swojego miesiąca w przód, aż do kolejnego wpisu tego samego właściciela, a osoba bez własnego targetu dziedziczy równą część targetu zespołu (po odjęciu targetów imiennych). Odczyt wymaga dostępu do panelu, zapis osobnego uprawnienia stats.targets.manage.

Requires features: stats.targets.manage

**Tags:** Stats

**Requires authentication.**

**Features:** stats.targets.manage

### Responses

**200** – Nadpisano istniejący wpis.

Content-Type: `application/json`

**201** – Utworzono.

Content-Type: `application/json`

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/stats/targets" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## PUT `/stats/targets`

Zmień target

Target kwotowy (sprzedaż netto) ustawiany na zespół ALBO na pracownika. Wpis obowiązuje od swojego miesiąca w przód, aż do kolejnego wpisu tego samego właściciela, a osoba bez własnego targetu dziedziczy równą część targetu zespołu (po odjęciu targetów imiennych). Odczyt wymaga dostępu do panelu, zapis osobnego uprawnienia stats.targets.manage.

Requires features: stats.targets.manage

**Tags:** Stats

**Requires authentication.**

**Features:** stats.targets.manage

### Responses

**200** – Zapisano.

Content-Type: `application/json`

### Example

```bash
curl -X PUT "https://crm.nf-hotel.pl/stats/targets" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## DELETE `/tags/categories`

Delete an empty, non-system category

A category groups tags for one purpose (records, call feedback, …). Stored as data so pointing tagging at a new entity needs no migration. `code` is immutable once created because tags reference it by value.

Requires features: tags.manage

**Tags:** Tags

**Requires authentication.**

**Features:** tags.manage

### Responses

**200** – Deleted.

Content-Type: `application/json`

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

**400** – Category is a system one or still holds tags.

Content-Type: `application/json`

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

### Example

```bash
curl -X DELETE "https://crm.nf-hotel.pl/tags/categories" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/tags/categories`

List tag categories with their tag counts

A category groups tags for one purpose (records, call feedback, …). Stored as data so pointing tagging at a new entity needs no migration. `code` is immutable once created because tags reference it by value.

Requires features: tags.view

**Tags:** Tags

**Requires authentication.**

**Features:** tags.view

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

### Responses

**200** – Categories.

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "code": "string",
      "label": "string",
      "description": null,
      "isSystem": true,
      "position": 1,
      "tagCount": 1,
      "deletable": true
    }
  ],
  "total": 1
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/tags/categories?page=1&pageSize=100" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/tags/categories`

Create a category

A category groups tags for one purpose (records, call feedback, …). Stored as data so pointing tagging at a new entity needs no migration. `code` is immutable once created because tags reference it by value.

Requires features: tags.manage

**Tags:** Tags

**Requires authentication.**

**Features:** tags.manage

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – Created.

Content-Type: `application/json`

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

**400** – Duplicate code.

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/tags/categories" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"code\": \"string\",
  \"label\": \"string\"
}"
```

## PUT `/tags/categories`

Rename a category (code is immutable)

A category groups tags for one purpose (records, call feedback, …). Stored as data so pointing tagging at a new entity needs no migration. `code` is immutable once created because tags reference it by value.

Requires features: tags.manage

**Tags:** Tags

**Requires authentication.**

**Features:** tags.manage

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – Updated.

Content-Type: `application/json`

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

**400** – Invalid payload.

Content-Type: `application/json`

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

### Example

```bash
curl -X PUT "https://crm.nf-hotel.pl/tags/categories" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## DELETE `/tags/tags`

Delete a tag

CRUD for the reusable tag catalog. List supports `search` for autocomplete.

Requires features: tags.manage

**Tags:** Tags

**Requires authentication.**

**Features:** tags.manage

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

### Responses

**200** – Deleted.

Content-Type: `application/json`

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

**404** – Not found.

Content-Type: `application/json`

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

### Example

```bash
curl -X DELETE "https://crm.nf-hotel.pl/tags/tags?id=00000000-0000-4000-8000-000000000000" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/tags/tags`

List/search tags

CRUD for the reusable tag catalog. List supports `search` for autocomplete.

Requires features: tags.view

**Tags:** Tags

**Requires authentication.**

**Features:** tags.view

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

### Responses

**200** – Tags.

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "slug": "string",
      "label": "string",
      "color": null,
      "description": null
    }
  ],
  "total": 1
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/tags/tags?page=1&pageSize=100" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/tags/tags`

Create a tag

CRUD for the reusable tag catalog. List supports `search` for autocomplete.

Requires features: tags.manage

**Tags:** Tags

**Requires authentication.**

**Features:** tags.manage

### Request Body

Content-Type: `application/json`

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

### Responses

**201** – 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://crm.nf-hotel.pl/tags/tags" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"slug\": \"string\",
  \"label\": \"string\"
}"
```

## PUT `/tags/tags`

Update a tag

CRUD for the reusable tag catalog. List supports `search` for autocomplete.

Requires features: tags.manage

**Tags:** Tags

**Requires authentication.**

**Features:** tags.manage

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – Updated.

Content-Type: `application/json`

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

**404** – Not found.

Content-Type: `application/json`

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

### Example

```bash
curl -X PUT "https://crm.nf-hotel.pl/tags/tags" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## POST `/tags/tags/assign`

Attach a tag to a polymorphic target

Links a tag to (entityType, entityId) within the validated tenant/organization scope. Idempotent.

Requires features: tags.manage

**Tags:** Tags

**Requires authentication.**

**Features:** tags.manage

### Request Body

Content-Type: `application/json`

```json
{
  "organizationId": "00000000-0000-4000-8000-000000000000",
  "tenantId": "00000000-0000-4000-8000-000000000000",
  "tagId": "00000000-0000-4000-8000-000000000000",
  "entityId": "00000000-0000-4000-8000-000000000000",
  "entityType": "string"
}
```

### Responses

**201** – Tag assigned.

Content-Type: `application/json`

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

**400** – Validation or assignment failed.

Content-Type: `application/json`

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

**404** – Tag not found in scope.

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/tags/tags/assign" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"organizationId\": \"00000000-0000-4000-8000-000000000000\",
  \"tenantId\": \"00000000-0000-4000-8000-000000000000\",
  \"tagId\": \"00000000-0000-4000-8000-000000000000\",
  \"entityId\": \"00000000-0000-4000-8000-000000000000\",
  \"entityType\": \"string\"
}"
```

## GET `/tags/tags/for-entity`

List tags assigned to a polymorphic target

Given entityType and entityId, returns the assigned tags as TagOption[] within the tenant/organization scope.

Requires features: tags.view

**Tags:** Tags

**Requires authentication.**

**Features:** tags.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| entityType | query | any | Required |
| entityId | query | any | Required |

### Responses

**200** – Assigned tags.

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "label": "string",
      "color": null
    }
  ]
}
```

**400** – Invalid query.

Content-Type: `application/json`

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

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/tags/tags/for-entity?entityType=string&entityId=00000000-0000-4000-8000-000000000000" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/tags/tags/unassign`

Remove a tag from a polymorphic target

Detaches a tag from (entityType, entityId) within the validated tenant/organization scope. Idempotent.

Requires features: tags.manage

**Tags:** Tags

**Requires authentication.**

**Features:** tags.manage

### Request Body

Content-Type: `application/json`

```json
{
  "organizationId": "00000000-0000-4000-8000-000000000000",
  "tenantId": "00000000-0000-4000-8000-000000000000",
  "tagId": "00000000-0000-4000-8000-000000000000",
  "entityId": "00000000-0000-4000-8000-000000000000",
  "entityType": "string"
}
```

### Responses

**200** – Tag detached.

Content-Type: `application/json`

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

**400** – Validation or detach failed.

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/tags/tags/unassign" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"organizationId\": \"00000000-0000-4000-8000-000000000000\",
  \"tenantId\": \"00000000-0000-4000-8000-000000000000\",
  \"tagId\": \"00000000-0000-4000-8000-000000000000\",
  \"entityId\": \"00000000-0000-4000-8000-000000000000\",
  \"entityType\": \"string\"
}"
```

## GET `/user_status/status`

Status for the given users (or all deactivated when no ids passed)

Requires features: user_status.view

**Tags:** User account status

**Requires authentication.**

**Features:** user_status.view

### Responses

**200** – Per-user status.

Content-Type: `application/json`

```json
{
  "items": [
    {
      "userId": "00000000-0000-4000-8000-000000000000",
      "active": true,
      "deactivatedAt": null
    }
  ]
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/user_status/status" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/user_status/status`

Activate or deactivate a user account

Requires features: user_status.manage

**Tags:** User account status

**Requires authentication.**

**Features:** user_status.manage

### Responses

**200** – Applied.

Content-Type: `application/json`

```json
{
  "ok": true,
  "userId": "00000000-0000-4000-8000-000000000000",
  "active": true
}
```

**422** – Invalid payload or self-deactivation.

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/user_status/status" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/user_status/users`

Users by status

Requires features: user_status.view

**Tags:** User account status

**Requires authentication.**

**Features:** user_status.view

### Responses

**200** – Paginated users with status.

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "string",
      "email": null,
      "name": null,
      "displayName": "string",
      "active": true,
      "deactivatedAt": null,
      "centralkaPhones": [
        "string"
      ],
      "slackUserId": null,
      "slackUserIdRaw": null
    }
  ],
  "total": 1,
  "totalPages": 1,
  "page": 1,
  "pageSize": 1
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/user_status/users" \
  -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://crm.nf-hotel.pl/version" \
  -H "Accept: application/json"
```

## DELETE `/wfirma_accounts/accounts`

Delete wFirma account

Requires features: wfirma_accounts.manage

**Tags:** wFirma accounts

**Requires authentication.**

**Features:** wfirma_accounts.manage

### Responses

**200** – Account deleted.

Content-Type: `application/json`

### Example

```bash
curl -X DELETE "https://crm.nf-hotel.pl/wfirma_accounts/accounts" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/wfirma_accounts/accounts`

List wFirma accounts

Requires features: wfirma_accounts.view

**Tags:** wFirma accounts

**Requires authentication.**

**Features:** wfirma_accounts.view

### Responses

**200** – Accounts returned.

Content-Type: `application/json`

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/wfirma_accounts/accounts" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/wfirma_accounts/accounts`

Create wFirma account

Requires features: wfirma_accounts.manage

**Tags:** wFirma accounts

**Requires authentication.**

**Features:** wfirma_accounts.manage

### Request Body

Content-Type: `application/json`

```json
{
  "name": "string",
  "issuerOrganizationId": null,
  "companyId": null,
  "appKey": null,
  "accessKey": null,
  "secretKey": null,
  "baseUrl": null,
  "blockIssue": false,
  "issueAsDraft": false,
  "autoIssue": false,
  "autoSend": false,
  "autoVatFromProforma": false,
  "isDefault": false,
  "isActive": true,
  "vatStatus": "payer",
  "vatExemptionBasis": null,
  "sellerLegalName": null,
  "sellerNip": null,
  "sellerStreet": null,
  "sellerZip": null,
  "sellerCity": null,
  "sellerCountry": "PL",
  "bankAccountNumber": null,
  "bankName": null,
  "sellerPhone": null,
  "sellerEmail": null,
  "sellerWebsite": null,
  "logoAttachmentId": null,
  "accentColor": null
}
```

### Responses

**200** – Account created.

Content-Type: `application/json`

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/wfirma_accounts/accounts" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"name\": \"string\",
  \"issuerOrganizationId\": null,
  \"companyId\": null,
  \"appKey\": null,
  \"accessKey\": null,
  \"secretKey\": null,
  \"baseUrl\": null,
  \"blockIssue\": false,
  \"issueAsDraft\": false,
  \"autoIssue\": false,
  \"autoSend\": false,
  \"autoVatFromProforma\": false,
  \"isDefault\": false,
  \"isActive\": true,
  \"vatStatus\": \"payer\",
  \"vatExemptionBasis\": null,
  \"sellerLegalName\": null,
  \"sellerNip\": null,
  \"sellerStreet\": null,
  \"sellerZip\": null,
  \"sellerCity\": null,
  \"sellerCountry\": \"PL\",
  \"bankAccountNumber\": null,
  \"bankName\": null,
  \"sellerPhone\": null,
  \"sellerEmail\": null,
  \"sellerWebsite\": null,
  \"logoAttachmentId\": null,
  \"accentColor\": null
}"
```

## PUT `/wfirma_accounts/accounts`

Update wFirma account

Requires features: wfirma_accounts.manage

**Tags:** wFirma accounts

**Requires authentication.**

**Features:** wfirma_accounts.manage

### Request Body

Content-Type: `application/json`

```json
{
  "issuerOrganizationId": null,
  "companyId": null,
  "appKey": null,
  "accessKey": null,
  "secretKey": null,
  "baseUrl": null,
  "blockIssue": false,
  "issueAsDraft": false,
  "autoIssue": false,
  "autoSend": false,
  "autoVatFromProforma": false,
  "isDefault": false,
  "isActive": true,
  "vatStatus": "payer",
  "vatExemptionBasis": null,
  "sellerLegalName": null,
  "sellerNip": null,
  "sellerStreet": null,
  "sellerZip": null,
  "sellerCity": null,
  "sellerCountry": "PL",
  "bankAccountNumber": null,
  "bankName": null,
  "sellerPhone": null,
  "sellerEmail": null,
  "sellerWebsite": null,
  "logoAttachmentId": null,
  "accentColor": null,
  "id": "00000000-0000-4000-8000-000000000000"
}
```

### Responses

**200** – Account updated.

Content-Type: `application/json`

### Example

```bash
curl -X PUT "https://crm.nf-hotel.pl/wfirma_accounts/accounts" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"issuerOrganizationId\": null,
  \"companyId\": null,
  \"appKey\": null,
  \"accessKey\": null,
  \"secretKey\": null,
  \"baseUrl\": null,
  \"blockIssue\": false,
  \"issueAsDraft\": false,
  \"autoIssue\": false,
  \"autoSend\": false,
  \"autoVatFromProforma\": false,
  \"isDefault\": false,
  \"isActive\": true,
  \"vatStatus\": \"payer\",
  \"vatExemptionBasis\": null,
  \"sellerLegalName\": null,
  \"sellerNip\": null,
  \"sellerStreet\": null,
  \"sellerZip\": null,
  \"sellerCity\": null,
  \"sellerCountry\": \"PL\",
  \"bankAccountNumber\": null,
  \"bankName\": null,
  \"sellerPhone\": null,
  \"sellerEmail\": null,
  \"sellerWebsite\": null,
  \"logoAttachmentId\": null,
  \"accentColor\": null,
  \"id\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## GET `/wfirma_accounts/accounts/{id}/webhooks`

Read webhook registration status

Registers, removes and test-triggers the wFirma webhooks (invoice_ksefs/change_state, payments/add) for a single issuer account. Registration is idempotent — webhooks/find runs before webhooks/add.

Requires features: wfirma_accounts.view

**Tags:** wFirma accounts

**Requires authentication.**

**Features:** wfirma_accounts.view

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

### Responses

**200** – Status per supported event.

Content-Type: `application/json`

```json
{
  "publicBaseUrl": null,
  "canRegister": true,
  "rows": [
    {
      "event": "string",
      "health": "missing",
      "wfirmaWebhookId": null,
      "registeredAt": null,
      "lastEventAt": null,
      "lastError": null
    }
  ]
}
```

**404** – Account not found.

Content-Type: `application/json`

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

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/wfirma_accounts/accounts/:id/webhooks" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/wfirma_accounts/accounts/{id}/webhooks`

Register, unregister or test a webhook

Registers, removes and test-triggers the wFirma webhooks (invoice_ksefs/change_state, payments/add) for a single issuer account. Registration is idempotent — webhooks/find runs before webhooks/add.

Requires features: wfirma_accounts.manage

**Tags:** wFirma accounts

**Requires authentication.**

**Features:** wfirma_accounts.manage

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

### Request Body

Content-Type: `application/json`

```json
{
  "action": "register",
  "event": "string"
}
```

### Responses

**200** – Action applied; returns fresh status.

Content-Type: `application/json`

```json
{
  "publicBaseUrl": null,
  "canRegister": true,
  "rows": [
    {
      "event": "string",
      "health": "missing",
      "wfirmaWebhookId": null,
      "registeredAt": null,
      "lastEventAt": null,
      "lastError": null
    }
  ]
}
```

**400** – Invalid request or unsupported event.

Content-Type: `application/json`

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

**404** – Account not found.

Content-Type: `application/json`

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

**502** – wFirma refused the operation.

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/wfirma_accounts/accounts/:id/webhooks" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"action\": \"register\",
  \"event\": \"string\"
}"
```

## GET `/wfirma_accounts/accounts/resolve`

Resolve issuing account for an organization

Returns the account derived from the organization (org default → org's only active account → unassigned tenant default), or null when none applies.

Requires features: wfirma_accounts.view

**Tags:** wFirma accounts

**Requires authentication.**

**Features:** wfirma_accounts.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| organizationId | query | any | Optional |

### Responses

**200** – Resolved account or null.

Content-Type: `application/json`

```json
{
  "account": null
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/wfirma_accounts/accounts/resolve" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/wfirma_accounts/issuer-defaults`

Read issuer defaults with the resolved rule applied

Requires features: wfirma_accounts.view

**Tags:** wFirma accounts

**Requires authentication.**

**Features:** wfirma_accounts.view

### Responses

**200** – Stored settings, resolved values and the tenant organizations.

Content-Type: `application/json`

```json
{
  "defaultIssuerOrganizationId": null,
  "excludedIssuerOrganizationIds": null,
  "resolved": {
    "defaultIssuerOrganizationId": null,
    "excludedIssuerOrganizationIds": [
      "00000000-0000-4000-8000-000000000000"
    ]
  },
  "organizations": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "name": "string",
      "parentId": null,
      "hasAccount": true
    }
  ]
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/wfirma_accounts/issuer-defaults" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## PUT `/wfirma_accounts/issuer-defaults`

Update the default issuing organization and the exclusion list

Requires features: wfirma_accounts.manage

**Tags:** wFirma accounts

**Requires authentication.**

**Features:** wfirma_accounts.manage

### Request Body

Content-Type: `application/json`

```json
{
  "defaultIssuerOrganizationId": null,
  "excludedIssuerOrganizationIds": null
}
```

### Responses

**200** – Saved settings with the resolved values.

Content-Type: `application/json`

```json
{
  "defaultIssuerOrganizationId": null,
  "excludedIssuerOrganizationIds": null,
  "resolved": {
    "defaultIssuerOrganizationId": null,
    "excludedIssuerOrganizationIds": [
      "00000000-0000-4000-8000-000000000000"
    ]
  },
  "organizations": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "name": "string",
      "parentId": null,
      "hasAccount": true
    }
  ]
}
```

**400** – Invalid payload, unknown organization, or default listed as excluded.

Content-Type: `application/json`

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

### Example

```bash
curl -X PUT "https://crm.nf-hotel.pl/wfirma_accounts/issuer-defaults" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"defaultIssuerOrganizationId\": null,
  \"excludedIssuerOrganizationIds\": null
}"
```

## GET `/wfirma/bank-imports`

List bank statement imports (newest first)

Upload a CSV/XML bank statement; operations are deduplicated and stored for matching against invoices.

Requires features: wfirma.bank.view

**Tags:** wFirma bank

**Requires authentication.**

**Features:** wfirma.bank.view

### Responses

**200** – Paged import list.

Content-Type: `application/json`

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/wfirma/bank-imports" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/wfirma/bank-imports`

Upload and parse a bank statement (multipart: file, optional issuerAccountId)

Upload a CSV/XML bank statement; operations are deduplicated and stored for matching against invoices.

Requires features: wfirma.bank.manage

**Tags:** wFirma bank

**Requires authentication.**

**Features:** wfirma.bank.manage

### Responses

**200** – Import summary.

Content-Type: `application/json`

```json
{
  "ok": true,
  "importId": "00000000-0000-4000-8000-000000000000",
  "duplicateHomeImportId": null,
  "imported": 1,
  "duplicates": 1,
  "ignoredOutgoing": 1,
  "issuerAccountId": null
}
```

**400** – Bad request / unsupported format / no operations.

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/wfirma/bank-imports" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## DELETE `/wfirma/bank-imports/{id}`

Soft-delete an import and its operations

Soft-deletes the import with its operations and suggestions. Refused when any operation is already matched.

Requires features: wfirma.bank.manage

**Tags:** wFirma bank

**Requires authentication.**

**Features:** wfirma.bank.manage

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

### Responses

**200** – Import removed.

Content-Type: `application/json`

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

**404** – Import not found.

Content-Type: `application/json`

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

**409** – Import has matched operations.

Content-Type: `application/json`

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

### Example

```bash
curl -X DELETE "https://crm.nf-hotel.pl/wfirma/bank-imports/:id" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/wfirma/bank-operations`

List bank operations (newest first) with suggested/confirmed matches

Requires features: wfirma.bank.view

**Tags:** wFirma bank

**Requires authentication.**

**Features:** wfirma.bank.view

### Responses

**200** – Paged operation list.

Content-Type: `application/json`

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/wfirma/bank-operations" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/wfirma/bank-operations/{id}/confirm`

Confirm match and apply payments

Applies the assigned amounts to the invoices via the shared mark-paid path (partial accumulation + wFirma payment registration). A wFirma failure does not roll back OM; re-POST retries only the wFirma registration.

Requires features: wfirma.bank.manage, wfirma.invoices.pay

**Tags:** wFirma bank

**Requires authentication.**

**Features:** wfirma.bank.manage, wfirma.invoices.pay

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

### Request Body

Content-Type: `application/json`

```json
{
  "matches": [
    {
      "invoiceId": "00000000-0000-4000-8000-000000000000",
      "amount": 1
    }
  ]
}
```

### Responses

**200** – Applied; per-invoice wFirma sync results.

Content-Type: `application/json`

```json
{
  "ok": true,
  "results": [
    {
      "invoiceId": "00000000-0000-4000-8000-000000000000",
      "paymentStatus": "string",
      "wfirmaSynced": null,
      "wfirmaError": null
    }
  ],
  "vatDraftsCreated": 1,
  "vatDraftIds": [
    "00000000-0000-4000-8000-000000000000"
  ],
  "vatQueuedForIssue": 1
}
```

**400** – Bad request / amounts exceed operation.

Content-Type: `application/json`

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

**404** – Operation or invoice not found.

Content-Type: `application/json`

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

**409** – Already matched; one of the invoices is already settled (code: overpayment, re-send with allowOverpayment); or the transfer currency differs from the invoice currency and amountInInvoiceCurrency was not supplied (code: currency_mismatch_unconfirmed).

Content-Type: `application/json`

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

**422** – The exchange rate implied by this transfer is out of range (code: implied_rate_out_of_range) — most likely the wrong invoice.

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/wfirma/bank-operations/:id/confirm" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"matches\": [
    {
      \"invoiceId\": \"00000000-0000-4000-8000-000000000000\",
      \"amount\": 1
    }
  ]
}"
```

## POST `/wfirma/bank-operations/{id}/ignore`

Toggle the ignored status of a bank operation

Requires features: wfirma.bank.manage

**Tags:** wFirma bank

**Requires authentication.**

**Features:** wfirma.bank.manage

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

### Request Body

Content-Type: `application/json`

```json
{
  "ignored": true
}
```

### Responses

**200** – Status updated.

Content-Type: `application/json`

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

**400** – Bad request.

Content-Type: `application/json`

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

**404** – Operation not found.

Content-Type: `application/json`

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

**409** – Matched operations cannot be ignored.

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/wfirma/bank-operations/:id/ignore" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"ignored\": true
}"
```

## POST `/wfirma/bank-operations/{id}/rematch`

Recompute engine suggestions (user decisions untouched)

Requires features: wfirma.bank.manage

**Tags:** wFirma bank

**Requires authentication.**

**Features:** wfirma.bank.manage

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

### Responses

**200** – Suggestions recomputed.

Content-Type: `application/json`

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

**404** – Operation not found.

Content-Type: `application/json`

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

**409** – Operation is matched/ignored/outgoing.

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/wfirma/bank-operations/:id/rematch" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/wfirma/bank-operations/{id}/suggestions`

Operation details with stored match candidates and invoice info

Requires features: wfirma.bank.view

**Tags:** wFirma bank

**Requires authentication.**

**Features:** wfirma.bank.view

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

### Responses

**200** – Operation + matches. When there are no candidates, `emptyReason` says why: `currency_mismatch` (no unpaid invoice shares the operation currency), `no_unpaid` (nothing left to settle) or `no_number_match`.

Content-Type: `application/json`

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/wfirma/bank-operations/:id/suggestions" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/wfirma/billing-runs`

List worker runs (newest first)

Requires features: wfirma.invoices.view

**Tags:** wFirma

**Requires authentication.**

**Features:** wfirma.invoices.view

### Responses

**200** – Paged run list.

Content-Type: `application/json`

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/wfirma/billing-runs" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/wfirma/integration/invoice-form`

Get invoice dialog data for a property

Everything the SaaS needs to render the dialog before calling `POST /api/wfirma/integration/invoices`. Authenticates with an API key placed in the ISSUING COMPANY's organization (the seller is always that company). The property is identified by `propertyId` or `propertyCode`. A code shared by several properties resolves to the property marked as the NFHotel account primary in the CRM (SPEC-071; the account holds only properties of the same company as the primary): the dialog data (buyer, contacts, currency) and the invoice issued by the following POST belong to that property; without a primary it yields `ambiguous_property`. `buyer` is the company linked to the property (`buyer.companyId` = the default company id); `currency` is the property's default currency and cannot be chosen by the SaaS; amounts sent later are GROSS at `vatRate`. `emailRecipients` are the addresses the invoice will be e-mailed to (property contacts: invoice role → primary → all). `services` lists the allowed services in display order (inactive catalog products are omitted). Non-empty `blockers` means the POST will be refused and nothing will be saved; `warnings` means the invoice will be issued but the e-mail will not go out (`no_email_recipient`) or wFirma will create an unnumbered draft (`issuer_test_mode`). `defaultKind` is `proforma` for a property marked "pro forma only" in the CRM, otherwise `vat` (SPEC-078); `kinds` always offers both — choosing `vat` there is allowed and the POST answers with the `property_proforma_only` warning. Responses are never cached.

Requires features: wfirma.invoices.integrate_create

**Tags:** wFirma integration

**Requires authentication.**

**Features:** wfirma.invoices.integrate_create

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| propertyId | query | any | Optional |
| propertyCode | query | any | Optional |

### Responses

**200** – Buyer, currency, e-mail recipients, services, blockers and warnings.

Content-Type: `application/json`

```json
{
  "property": {
    "id": "00000000-0000-4000-8000-000000000000",
    "code": null,
    "name": "string"
  },
  "buyer": null,
  "recipient": null,
  "currency": "string",
  "vatRate": "string",
  "emailRecipients": [
    "string"
  ],
  "services": [
    {
      "code": "NFD-H-0013",
      "name": "string"
    }
  ],
  "kinds": [
    "vat"
  ],
  "defaultKind": "vat",
  "blockers": [
    "no_company"
  ],
  "warnings": [
    "no_email_recipient"
  ]
}
```

**400** – `property_param_required` (exactly one of propertyCode / propertyId) or `bad_request`.

Content-Type: `application/json`

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

**404** – `property_not_found`.

Content-Type: `application/json`

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

**409** – `ambiguous_property` — several properties share the code and none is the account primary; `candidates[]` lists them.

Content-Type: `application/json`

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

**429** – `rate_limited` — per-key limit; see `Retry-After`.

Content-Type: `application/json`

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

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/wfirma/integration/invoice-form" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/wfirma/integration/invoices`

List invoices of a property

Read-only feed for an external system authenticating with an API key (`X-API-Key`). The issuing company is the ORGANIZATION THE KEY BELONGS TO — a key placed in an organization without an issuer account (or in no organization at all) is refused with `not_an_issuer`. The property is identified by `propertyCode` (its ID in the SaaS) or `propertyId`. A code shared by several properties is ONE NFHotel account when exactly one of them is marked as the account primary in the CRM (SPEC-071): the response then covers the invoices, summary and payment links of ALL properties of the account (`properties[]`), and `property` is the primary. The account holds only the properties of the SAME company as the primary — a property with the same code but another company (a property that changed owners) is never merged in. Without a primary it yields `ambiguous_property` with the candidates, so the caller can retry with `propertyId`. `propertyId` of the primary covers the whole account too; `propertyId` of any other property covers only that property. `property.paymentDeferredUntil` is the property-wide payment deferral agreed with the customer (`YYYY-MM-DD`, inclusive) or null when none or already past — purely informational: statuses, days overdue and links are not affected, the SaaS pauses its own access-blocking counter. Each item carries the document, its lines, KSeF number, days overdue, a PDF address (same API key) and a perpetual payment link (`/zaplac/<token>`, a Przelewy24 session is created only when the client clicks). WHETHER A DOCUMENT IS TO BE PAID IS DECIDED ONLY BY `payable` (SPEC-065) — not by the status, the due date, the amount or `payUrl`. `payable` is true exactly for the documents counted in `summary.outstanding`, `openCount`, `overdueCount`, `paymentTitle`, `maxDaysOverdue` and `payAllUrl`; it does not depend on whether a link could be built (`payUrl` may be null with `payLinkReason` such as `no_payer_email` while the document stays payable and is counted). When `payable` is false, `amountDue` is 0, `daysOverdue` is 0, `payUrl` is null and `notPayableReason` is the first matching of: `correction_document` (a correction — never payable, including an increasing one), `settled_by_final_invoice` (a proforma for which a VAT invoice has been issued, regardless of the proforma payment status), `paid` (paid, or payments cover the amount after corrections), `corrected` (corrections brought the amount to zero), `zero_amount` (a zero document without corrections). There is no cancelled state in Open Mercato. `amountDue` accounts for corrections: original gross + issued corrections of its chain (a correction of a correction included) − payments on the document and on those corrections (738.00 corrected to 500.00 leaves 500.00; the payment link charges the same amount). `corrections[]` / `correctedInvoice` and `sourceProforma` / `finalInvoice` give both directions of the links with number and PDF. The `open`/`unpaid`/`overdue`/`partial` filters match payable kinds by STORED status, so they may return `payable: false` rows (e.g. `corrected`) and `total` may exceed `summary.openCount` — split tabs by `payable`. `issuer` (transfer details) is the account of the most recent open document — the same account the reminder e-mail and PDF for that document use. `summary` is computed from ALL open documents of the property at this issuer — regardless of paging and of the `paymentStatus` filter — and carries the total outstanding per currency, the transfer title covering all of them and one link paying them all. Payment links are created lazily and are idempotent per invoice set; the payer e-mail sent to the operator comes from the property's contact persons (invoice role → primary → all) and is only FILLED IN on a link that has none — a reminder already sent to a specific person keeps its address. Without one, links are omitted with `no_payer_email`. Responses are never cached.

Requires features: wfirma.invoices.integrate

**Tags:** wFirma integration

**Requires authentication.**

**Features:** wfirma.invoices.integrate

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| propertyCode | query | any | Optional |
| propertyId | query | any | Optional |
| paymentStatus | query | any | Optional |
| page | query | any | Optional |
| pageSize | query | any | Optional |

### Responses

**200** – A page of invoices with summary and issuer transfer details.

Content-Type: `application/json`

```json
{
  "property": {
    "id": "00000000-0000-4000-8000-000000000000",
    "code": null,
    "name": "string",
    "paymentDeferredUntil": null
  },
  "properties": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "code": null,
      "name": "string"
    }
  ],
  "issuer": {
    "name": null,
    "nip": null,
    "address": {
      "street": null,
      "zip": null,
      "city": null
    },
    "bankAccountNumber": null,
    "bankName": null
  },
  "summary": {
    "openCount": 1,
    "overdueCount": 1,
    "outstanding": [
      {
        "currency": "string",
        "amount": "string"
      }
    ],
    "paymentTitle": null,
    "payAllUrl": null,
    "payAllReason": null,
    "maxDaysOverdue": 1,
    "oldestOverdueInvoiceId": null
  },
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "number": null,
      "kind": "vat",
      "issueDate": null,
      "saleDate": null,
      "dueDate": null,
      "servicePeriod": null,
      "paymentStatus": "unpaid",
      "daysOverdue": 1,
      "currency": "string",
      "totalNet": "string",
      "totalGross": "string",
      "paidAmount": "string",
      "amountDue": "string",
      "payable": true,
      "notPayableReason": null,
      "paidAt": null,
      "ksefNumber": null,
      "ksefStatus": null,
      "paymentTitle": "string",
      "payUrl": null,
      "payLinkReason": null,
      "pdfUrl": null,
      "lines": [
        {
          "name": "string",
          "quantity": "string",
          "unit": null,
          "unitNet": "string",
          "unitGross": null,
          "vatRate": "string",
          "vatCode": null,
          "subscriptionId": null
        }
      ],
      "correctedInvoiceId": null,
      "sourceProformaId": null,
      "corrections": [
        {
          "id": "00000000-0000-4000-8000-000000000000",
          "number": null,
          "pdfUrl": null
        }
      ],
      "correctedInvoice": null,
      "sourceProforma": null,
      "finalInvoice": null,
      "buyer": {
        "name": null,
        "nip": null
      }
    }
  ],
  "page": 1,
  "pageSize": 1,
  "total": 1
}
```

**400** – `property_param_required` (exactly one of propertyCode / propertyId) or `bad_request` (validation).

Content-Type: `application/json`

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

**404** – `property_not_found`.

Content-Type: `application/json`

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

**409** – `ambiguous_property` — several properties share this code and none is marked as the NFHotel account primary; `candidates[]` lists them.

Content-Type: `application/json`

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

**429** – `rate_limited` — per-key limit; see `Retry-After`.

Content-Type: `application/json`

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

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/wfirma/integration/invoices?page=1&pageSize=50" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/wfirma/integration/invoices`

Create, issue and e-mail an invoice for a property (SPEC-056)

`kind` is optional: without it the document type follows the property — `proforma` for a property marked "pro forma only" in the CRM, otherwise `vat` (SPEC-078). An explicit `kind: "vat"` for such a property is NOT refused: the invoice is issued and the response carries `warnings: ["property_proforma_only"]`. One request = the whole flow: the invoice is created, issued in wFirma/KSeF by the company the API key belongs to, and queued for e-mailing to the property contacts (the e-mail leaves after KSeF assigns the number). Requires the `Idempotency-Key` header — generate ONE key per click of "issue invoice" and reuse it on every retry of that click: a retry returns the same invoice (`replayed: true`) instead of issuing a second one to KSeF, and while the first request is still issuing it answers `409 issue_in_progress` with `Retry-After` — retry later with the same key. The same key with a different body is refused (`idempotency_key_reused`). If the wFirma call itself fails (`502 wfirma_issue_failed`), the document MAY exist in wFirma, so it is never re-sent automatically: retries return `409 issue_interrupted` until an operator checks it in the CRM (then a retry returns the issued invoice). Do NOT create the same invoice again with a new key. `notes` is one optional operator note (max 2000 characters after trimming, whitespace-only counts as none) that is ALWAYS printed on the document — it becomes the wFirma `description`, joined with the reverse-charge annotation when there is one; there is no internal-note mode, and it is PART OF THE BODY COVERED BY THE IDEMPOTENCY KEY, so the same key with a changed note is refused with `idempotency_key_reused`. The response carries `invoice.notes` as stored on the DOCUMENT, also on a replay. A note may not START with `netfactory:` (400 `bad_request`): that prefix is reserved for invoices migrated from the legacy CRM and would mislabel the document as pre-KSeF. `emailMessage` is one optional line for the invoice E-MAIL (max 1000 characters after trimming, whitespace-only counts as none; over the limit → 400 `bad_request` with `issues[].path = "emailMessage"`): it is NEVER printed on the document nor sent to wFirma/KSeF — it is stored with the invoice and filled into the `{{additionalInfo}}` variable of the "invoice issued" e-mail template (without that variable: as a paragraph after the first paragraph of the body), as plain text: HTML is escaped and line breaks are kept. It belongs to the invoice: every "invoice issued" e-mail of this single invoice carries it — the one queued by this request, a CRM re-send or retry — unless an operator edits the e-mail body by hand (the edited body is sent as written); payment reminders never carry it. Like `notes`, it is PART OF THE BODY COVERED BY THE IDEMPOTENCY KEY (a changed text with the same key → `idempotency_key_reused`), and the response carries `invoice.emailMessage` as stored on the invoice, also on a replay. Lines accept only the listed services; `amount` is GROSS at 23% VAT, quantity 1. The net price is authoritative (wFirma receives net prices), so for some amounts the gross on the document differs by 0.01 from the requested one (e.g. 600.00 → 599.99) — show `invoice.totalGross` / `lines[].gross` from the response. The currency is the property default. Refusals listed as `blockers` (see `GET /api/wfirma/integration/invoice-form`) save nothing.

Requires features: wfirma.invoices.integrate_create

**Tags:** wFirma integration

**Requires authentication.**

**Features:** wfirma.invoices.integrate_create

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| Idempotency-Key | header | any | Required. One key per user action; reuse it on retries. |

### Request Body

Content-Type: `application/json`

```json
{
  "createdBy": "string",
  "notes": null,
  "emailMessage": null,
  "lines": [
    {
      "service": "NFD-H-0013",
      "amount": "string"
    }
  ]
}
```

### Responses

**200** – Retry with an already used `Idempotency-Key` — the invoice created by the first request (`replayed: true`).

Content-Type: `application/json`

```json
{
  "invoice": {
    "id": "00000000-0000-4000-8000-000000000000",
    "number": null,
    "kind": "vat",
    "status": "string",
    "ksefStatus": null,
    "issueDate": null,
    "dueDate": null,
    "currency": "string",
    "totalNet": "string",
    "totalGross": "string",
    "notes": null,
    "emailMessage": null,
    "lines": [
      {
        "service": "string",
        "name": "string",
        "requestedAmount": "string",
        "net": "string",
        "gross": "string",
        "vatRate": "string"
      }
    ],
    "pdfUrl": null
  },
  "email": {
    "status": "scheduled",
    "recipients": [
      "string"
    ],
    "reason": null
  },
  "replayed": true,
  "warnings": [
    "no_email_recipient"
  ]
}
```

**201** – Invoice created and issued.

Content-Type: `application/json`

```json
{
  "invoice": {
    "id": "00000000-0000-4000-8000-000000000000",
    "number": null,
    "kind": "vat",
    "status": "string",
    "ksefStatus": null,
    "issueDate": null,
    "dueDate": null,
    "currency": "string",
    "totalNet": "string",
    "totalGross": "string",
    "notes": null,
    "emailMessage": null,
    "lines": [
      {
        "service": "string",
        "name": "string",
        "requestedAmount": "string",
        "net": "string",
        "gross": "string",
        "vatRate": "string"
      }
    ],
    "pdfUrl": null
  },
  "email": {
    "status": "scheduled",
    "recipients": [
      "string"
    ],
    "reason": null
  },
  "replayed": true,
  "warnings": [
    "no_email_recipient"
  ]
}
```

**400** – `idempotency_key_required`, `property_param_required` or `bad_request` (validation, `issues[]`).

Content-Type: `application/json`

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

**404** – `property_not_found`.

Content-Type: `application/json`

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

**409** – `ambiguous_property` (`candidates[]`; several properties share the code and none is the account primary — with a primary the invoice is issued on it, SPEC-071), `idempotency_key_reused`, `invoice_deleted` (the invoice of this key was deleted in the CRM), `issue_in_progress` (the first request with this key is still issuing — retry after `Retry-After` seconds with the same key), `issue_interrupted` (a previous attempt may have created the document in wFirma — needs a check in the CRM; `invoiceId`).

Content-Type: `application/json`

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

**422** – `service_unavailable` (`services[]`), `not_issuable` (`blockers[]` — nothing saved unless `invoiceId` is present), `issue_rejected` (wFirma rejected the document; `invoiceId`).

Content-Type: `application/json`

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

**423** – `issue_blocked` — the issuer account is in test mode (issuing blocked).

Content-Type: `application/json`

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

**429** – `rate_limited` — per-key limit; see `Retry-After`.

Content-Type: `application/json`

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

**502** – `wfirma_issue_failed` — the wFirma call failed and the document may or may not exist there; the invoice (`invoiceId`) waits for a check in the CRM. Retries with the same key return `issue_interrupted` until then.

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/wfirma/integration/invoices" \
  -H "Accept: application/json" \
  -H "Idempotency-Key: string" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"createdBy\": \"string\",
  \"notes\": null,
  \"emailMessage\": null,
  \"lines\": [
    {
      \"service\": \"NFD-H-0013\",
      \"amount\": \"string\"
    }
  ]
}"
```

## GET `/wfirma/integration/invoices/{id}/pdf`

Download invoice PDF

The `pdfUrl` from the invoice list. Same API key and scope as the list: only issued documents of the issuing company the key belongs to. Optional `propertyId` / `propertyCode` restricts access to ONE property (SPEC-065): a document of another property answers 404, exactly like a document of another company. A document belongs to the property when its own property matches, or — when it has none (documents pulled from wFirma for a company with several properties) — when the document it is linked to does (root of the correction chain, source proforma, or the VAT invoice issued from the proforma). Without the parameter the behaviour is unchanged. `pdfUrl` values returned by the list already carry `?propertyId=`. Documents that exist in wFirma are downloaded from there; pre-KSeF documents migrated from the old CRM are rendered locally. Error bodies carry a stable `error` code and a human `message`.

Requires features: wfirma.invoices.integrate

**Tags:** wFirma integration

**Requires authentication.**

**Features:** wfirma.invoices.integrate

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| id | path | any | Required |
| propertyCode | query | any | Optional |
| propertyId | query | any | Optional |

### Responses

**200** – PDF stream (`application/pdf`, inline).

Content-Type: `application/json`

**400** – `bad_request` (invalid id), `property_param_required` (both propertyId and propertyCode given) or `not_configured` (no wFirma credentials for this issuer).

Content-Type: `application/json`

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

**404** – `not_found` — no such issued invoice at this issuer (or, with a property parameter, at this property); `property_not_found`.

Content-Type: `application/json`

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

**409** – `issuer_incomplete` — locally rendered document, but the issuer data is incomplete (`missing[]`); `ambiguous_property` (`candidates[]`).

Content-Type: `application/json`

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

**429** – `rate_limited` — per-key limit; see `Retry-After`.

Content-Type: `application/json`

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

**502** – `wfirma_download_failed` — wFirma did not return the document; details are in the server log.

Content-Type: `application/json`

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

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/wfirma/integration/invoices/:id/pdf" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## DELETE `/wfirma/invoices`

Delete an invoice draft

CRUD for CRM invoice drafts. Legal issuance (number, KSeF, JPK, PDF) happens in wFirma via the issue endpoint.

Requires features: wfirma.invoices.delete

**Tags:** wFirma invoices

**Requires authentication.**

**Features:** wfirma.invoices.delete

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

### Responses

**200** – Deleted.

Content-Type: `application/json`

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

**400** – Invalid identifier.

Content-Type: `application/json`

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

### Example

```bash
curl -X DELETE "https://crm.nf-hotel.pl/wfirma/invoices?id=00000000-0000-4000-8000-000000000000" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/wfirma/invoices`

List invoices

CRUD for CRM invoice drafts. Legal issuance (number, KSeF, JPK, PDF) happens in wFirma via the issue endpoint.

Requires features: wfirma.invoices.view

**Tags:** wFirma invoices

**Requires authentication.**

**Features:** wfirma.invoices.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| page | query | any | Optional |
| pageSize | query | any | Optional |
| sort | query | any | Optional |
| order | query | any | Optional |
| companyId | query | any | Optional |
| propertyId | query | any | Optional |
| withoutProperty | query | any | Required |
| status | query | any | Optional |
| id | query | any | Optional |
| createdByUserId | query | any | Optional |
| months | query | any | Optional |
| issuerAccountId | query | any | Optional |
| paymentStatus | query | any | Optional |
| from | query | any | Optional |
| to | query | any | Optional |
| dueState | query | any | Optional |
| search | query | any | Optional |

### Responses

**200** – Invoices for the current scope. Passing `id` returns the full detail shape, which adds `buyer`, `recipient`, `lines` and the heavy columns the list omits. Each entry of `lines` then carries two subscription fields: `subscriptionId` — the stored stamp written when the line was billed from a Usługa (null on invoices saved before per-line stamping and on hand-typed lines) — and `resolvedSubscriptionId`, a READ-ONLY, server-computed link target derived from the stamp, from the Usługi this invoice seeded, or from the invoice header when the invoice has exactly one line. Never send `resolvedSubscriptionId` back on write: it is an inference, not a fact. Every item (list and detail) also carries `amountDueAfterCorrections` — a READ-ONLY decimal string: gross plus all issued corrections of the chain minus payments on the document and on those corrections, never below zero; `"0.00"` for corrections and documents with payment status `paid`. It is the amount the mark-paid overpayment gate accepts; omitted when the correction chain could not be read.

Content-Type: `application/json`

```json
{
  "items": [],
  "total": 1
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/wfirma/invoices?page=1&pageSize=50" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/wfirma/invoices`

Create an invoice draft

CRUD for CRM invoice drafts. Legal issuance (number, KSeF, JPK, PDF) happens in wFirma via the issue endpoint.

Requires features: wfirma.invoices.create

**Tags:** wFirma invoices

**Requires authentication.**

**Features:** wfirma.invoices.create

### Request Body

Content-Type: `application/json`

```json
{
  "companyEntityId": "00000000-0000-4000-8000-000000000000",
  "issuerOrganizationId": null,
  "issuerAccountId": null,
  "issuerName": null,
  "propertyId": null,
  "subscriptionId": null,
  "subjectCategoryId": null,
  "kind": "vat",
  "priceEntryMode": "net",
  "reverseCharge": false,
  "dueDate": null,
  "paymentDueDate": null,
  "serviceStartDate": null,
  "serviceEndDate": null,
  "buyer": null,
  "recipient": null,
  "lines": [
    {
      "name": "string",
      "quantity": 1,
      "unitNet": 1,
      "vatRate": 1,
      "unit": null,
      "productId": null,
      "sku": null,
      "subscriptionId": null,
      "pkwiu": null,
      "unitGross": null,
      "vatCode": null,
      "discountPercent": null
    }
  ],
  "notes": null,
  "correctedInvoiceId": null,
  "correctionReason": null,
  "exchangeRate": null,
  "exchangeRateDate": null
}
```

### Responses

**201** – Draft created.

Content-Type: `application/json`

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

**400** – Invalid payload.

Content-Type: `application/json`

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

**404** – Company not found.

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/wfirma/invoices" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"companyEntityId\": \"00000000-0000-4000-8000-000000000000\",
  \"issuerOrganizationId\": null,
  \"issuerAccountId\": null,
  \"issuerName\": null,
  \"propertyId\": null,
  \"subscriptionId\": null,
  \"subjectCategoryId\": null,
  \"kind\": \"vat\",
  \"priceEntryMode\": \"net\",
  \"reverseCharge\": false,
  \"dueDate\": null,
  \"paymentDueDate\": null,
  \"serviceStartDate\": null,
  \"serviceEndDate\": null,
  \"buyer\": null,
  \"recipient\": null,
  \"lines\": [
    {
      \"name\": \"string\",
      \"quantity\": 1,
      \"unitNet\": 1,
      \"vatRate\": 1,
      \"unit\": null,
      \"productId\": null,
      \"sku\": null,
      \"subscriptionId\": null,
      \"pkwiu\": null,
      \"unitGross\": null,
      \"vatCode\": null,
      \"discountPercent\": null
    }
  ],
  \"notes\": null,
  \"correctedInvoiceId\": null,
  \"correctionReason\": null,
  \"exchangeRate\": null,
  \"exchangeRateDate\": null
}"
```

## PUT `/wfirma/invoices`

Update an invoice draft

Editing requires `wfirma.invoices.edit.limited` — the entry threshold for everyone who edits invoices, which `wfirma.invoices.manage` declares as a dependency. `manage` then WIDENS the verdict from "unpaid proformas and the caller's own unissued drafts" to "any editable document". Corrections additionally require `wfirma.invoices.correct`, whatever else the caller holds. Note the two distinct refusals — 403 means the CALLER may not touch this document at all, 409 means THIS DOCUMENT is not editable, and the body then carries a `reason`.

Requires features: wfirma.invoices.edit.limited

**Tags:** wFirma invoices

**Requires authentication.**

**Features:** wfirma.invoices.edit.limited

### Request Body

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "issuerOrganizationId": null,
  "issuerAccountId": null,
  "issuerName": null,
  "propertyId": null,
  "subscriptionId": null,
  "subjectCategoryId": null,
  "dueDate": null,
  "paymentDueDate": null,
  "serviceStartDate": null,
  "serviceEndDate": null,
  "buyer": null,
  "recipient": null,
  "notes": null,
  "correctedInvoiceId": null,
  "correctionReason": null,
  "exchangeRate": null,
  "exchangeRateDate": null,
  "paidAt": null,
  "ksefNumber": null,
  "ksefSyncedAt": null,
  "autopayUrl": null
}
```

### Responses

**200** – Draft updated.

Content-Type: `application/json`

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

**409** – Document is not editable: `ksef`, `accounting`, `vatIssued`, `paid`, `noExternalDoc`, `correctionForbidden` or `notAllowedForRole`.

Content-Type: `application/json`

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

### Example

```bash
curl -X PUT "https://crm.nf-hotel.pl/wfirma/invoices" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\",
  \"issuerOrganizationId\": null,
  \"issuerAccountId\": null,
  \"issuerName\": null,
  \"propertyId\": null,
  \"subscriptionId\": null,
  \"subjectCategoryId\": null,
  \"dueDate\": null,
  \"paymentDueDate\": null,
  \"serviceStartDate\": null,
  \"serviceEndDate\": null,
  \"buyer\": null,
  \"recipient\": null,
  \"notes\": null,
  \"correctedInvoiceId\": null,
  \"correctionReason\": null,
  \"exchangeRate\": null,
  \"exchangeRateDate\": null,
  \"paidAt\": null,
  \"ksefNumber\": null,
  \"ksefSyncedAt\": null,
  \"autopayUrl\": null
}"
```

## POST `/wfirma/invoices/{id}/create-correction`

Create a linked correction draft

Copies the invoice's buyer, lines and issuer into a new draft correction linked via correctedInvoiceId. With mode=zero the quantities are set to 0 ("korekta do zera"). The draft is then issued normally.

Requires features: wfirma.invoices.correct

**Tags:** wFirma invoices

**Requires authentication.**

**Features:** wfirma.invoices.correct

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

### Responses

**200** – Draft created.

Content-Type: `application/json`

```json
{
  "ok": true,
  "invoiceId": "00000000-0000-4000-8000-000000000000"
}
```

**400** – Invalid id or body.

Content-Type: `application/json`

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

**404** – Invoice not found.

Content-Type: `application/json`

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

**409** – Not an issued VAT invoice, or it does not exist in wFirma.

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/wfirma/invoices/:id/create-correction" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/wfirma/invoices/{id}/create-vat`

Create linked VAT draft from a proforma

Copies the proforma's buyer, lines and issuer into a new draft VAT invoice linked via sourceProformaId. The draft is then issued normally.

Requires features: wfirma.invoices.create

**Tags:** wFirma invoices

**Requires authentication.**

**Features:** wfirma.invoices.create

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

### Responses

**200** – Draft created.

Content-Type: `application/json`

```json
{
  "ok": true,
  "invoiceId": "00000000-0000-4000-8000-000000000000"
}
```

**404** – Proforma not found.

Content-Type: `application/json`

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

**409** – Not an issued proforma, or a VAT invoice already exists.

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/wfirma/invoices/:id/create-vat" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/wfirma/invoices/{id}/issue`

Issue invoice via wFirma

Pushes the local invoice draft to wFirma which assigns the legal number, handles KSeF/JPK and PDF. Stores the response back on the draft.

Requires features: wfirma.invoices.issue

**Tags:** wFirma invoices

**Requires authentication.**

**Features:** wfirma.invoices.issue

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

### Responses

**200** – Invoice issued.

Content-Type: `application/json`

```json
{
  "ok": true,
  "invoiceNumber": null,
  "ksefStatus": null,
  "paymentSyncWarning": null
}
```

**400** – Not configured / missing buyer.

Content-Type: `application/json`

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

**404** – Invoice not found.

Content-Type: `application/json`

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

**409** – Already issued.

Content-Type: `application/json`

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

**502** – wFirma issuance failed.

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/wfirma/invoices/:id/issue" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/wfirma/invoices/{id}/mark-paid`

Mark invoice paid/unpaid

Sets the local payment status and, when marking paid, registers the payment in wFirma so the operator does not have to open wFirma. Returns whether the wFirma sync succeeded.

Requires features: wfirma.invoices.manage, wfirma.invoices.pay

**Tags:** wFirma invoices

**Requires authentication.**

**Features:** wfirma.invoices.manage, wfirma.invoices.pay

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

### Request Body

Content-Type: `application/json`

```json
{
  "paid": true
}
```

### Responses

**200** – Payment status updated.

Content-Type: `application/json`

```json
{
  "ok": true,
  "wfirmaSynced": null,
  "wfirmaError": null,
  "vatDraftInvoiceId": null,
  "vatQueuedForIssue": true
}
```

**400** – Bad request.

Content-Type: `application/json`

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

**404** – Invoice not found.

Content-Type: `application/json`

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

**409** – Payment would duplicate an existing one (code: overpayment). Re-send with allowOverpayment to confirm.

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/wfirma/invoices/:id/mark-paid" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"paid\": true
}"
```

## GET `/wfirma/invoices/{id}/payments`

List invoice payment history

Every applied payment (including partials) with who applied it, when, how much and from which source — manual, bank statement, payment gateway, wFirma sync or the pre-history legacy backfill. Reversed entries are returned with their reversal stamp instead of being hidden.

Requires features: wfirma.invoices.view

**Tags:** wFirma invoices

**Requires authentication.**

**Features:** wfirma.invoices.view

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

### Responses

**200** – Payment history, oldest first.

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "amount": "string",
      "currencyCode": "string",
      "sourceAmount": null,
      "sourceCurrencyCode": null,
      "exchangeRate": null,
      "paidAt": "string",
      "appliedAt": "string",
      "appliedByUserId": null,
      "appliedByName": null,
      "source": "string",
      "gatewayProviderKey": null,
      "gatewayTransactionId": null,
      "gatewayReference": null,
      "operationId": null,
      "operationExecDate": null,
      "paymentMethod": "string",
      "isOverpayment": true,
      "reversedAt": null,
      "reversedByUserId": null,
      "reversedByName": null
    }
  ]
}
```

**400** – Bad request.

Content-Type: `application/json`

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

**404** – Invoice not found.

Content-Type: `application/json`

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

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/wfirma/invoices/:id/payments" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/wfirma/invoices/{id}/pdf`

Download invoice PDF

Requires features: wfirma.invoices.view

**Tags:** wFirma invoices

**Requires authentication.**

**Features:** wfirma.invoices.view

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

### Responses

**200** – PDF stream.

Content-Type: `application/json`

**400** – Not configured.

Content-Type: `application/json`

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

**404** – PDF unavailable.

Content-Type: `application/json`

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

**502** – wFirma download failed.

Content-Type: `application/json`

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

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/wfirma/invoices/:id/pdf" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## PUT `/wfirma/invoices/{id}/property`

Set the invoice property — allowed on issued invoices, unlike the general update

Requires features: wfirma.invoices.manage

**Tags:** wFirma invoices

**Requires authentication.**

**Features:** wfirma.invoices.manage

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

### Request Body

Content-Type: `application/json`

```json
{
  "propertyId": null
}
```

### Responses

**200** – Property assigned.

Content-Type: `application/json`

```json
{
  "ok": true,
  "propertyId": null
}
```

**400** – Bad request, or the property belongs to another company.

Content-Type: `application/json`

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

**404** – Invoice or property was not found.

Content-Type: `application/json`

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

### Example

```bash
curl -X PUT "https://crm.nf-hotel.pl/wfirma/invoices/:id/property" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"propertyId\": null
}"
```

## GET `/wfirma/invoices/{id}/reminders`

Read the reminder state (paused, reason, last reminder) of one invoice

Requires features: invoice_notifications.reminders.view

**Tags:** wFirma invoices

**Requires authentication.**

**Features:** invoice_notifications.reminders.view

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

### Responses

**200** – Current reminder state.

Content-Type: `application/json`

```json
{
  "canManage": true,
  "inScope": true,
  "outOfScopeReason": null,
  "remindersPausedAt": null,
  "remindersPauseReason": null,
  "remindersPausedByUserId": null,
  "lastReminderAt": null,
  "reminderCount": 1
}
```

**404** – Invoice was not found.

Content-Type: `application/json`

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

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/wfirma/invoices/:id/reminders" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/wfirma/invoices/{id}/reminders`

Pause payment reminders (with a reason) or resume them

Requires features: invoice_notifications.reminders.manage

**Tags:** wFirma invoices

**Requires authentication.**

**Features:** invoice_notifications.reminders.manage

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

### Request Body

Content-Type: `application/json`

```json
{
  "action": "pause",
  "reason": "string"
}
```

### Responses

**200** – Current pause state after the change.

Content-Type: `application/json`

```json
{
  "ok": true,
  "remindersPausedAt": null,
  "remindersPauseReason": null
}
```

**400** – Bad request (missing reason when pausing).

Content-Type: `application/json`

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

**404** – Invoice was not found.

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/wfirma/invoices/:id/reminders" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"action\": \"pause\",
  \"reason\": \"string\"
}"
```

## POST `/wfirma/invoices/{id}/send`

Send invoice to customer by email and/or SMS

Requires features: wfirma.invoices.send

**Tags:** wFirma invoices

**Requires authentication.**

**Features:** wfirma.invoices.send

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

### Request Body

Content-Type: `application/json`

```json
{
  "templateId": null,
  "type": "invoice_created",
  "channels": [
    "email"
  ],
  "email": null,
  "phone": null,
  "subject": null,
  "body": null,
  "smsBody": null
}
```

### Responses

**200** – Delivery rows created and attempted.

Content-Type: `application/json`

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/wfirma/invoices/:id/send" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"templateId\": null,
  \"type\": \"invoice_created\",
  \"channels\": [
    \"email\"
  ],
  \"email\": null,
  \"phone\": null,
  \"subject\": null,
  \"body\": null,
  \"smsBody\": null
}"
```

## POST `/wfirma/invoices/{id}/send-preview`

Preview interpolated subject/body and list selectable templates

Requires features: wfirma.invoices.send

**Tags:** wFirma invoices

**Requires authentication.**

**Features:** wfirma.invoices.send

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

### Request Body

Content-Type: `application/json`

```json
{
  "type": "invoice_created",
  "channel": "email",
  "templateId": null
}
```

### Responses

**200** – Interpolated preview and template list.

Content-Type: `application/json`

```json
{
  "subject": "string",
  "body": "string",
  "bodyIsHtml": true,
  "resolvedType": "invoice_created",
  "derivedType": null,
  "resolvedTemplateId": null,
  "templates": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "name": "string",
      "type": "invoice_created",
      "channel": "email",
      "hasSubject": true
    }
  ],
  "linkedVatNotice": null
}
```

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/wfirma/invoices/:id/send-preview" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"type\": \"invoice_created\",
  \"channel\": \"email\",
  \"templateId\": null
}"
```

## POST `/wfirma/invoices/{id}/sync-status`

Synchronize KSeF and payment status from wFirma

Requires features: wfirma.invoices.view

**Tags:** wFirma invoices

**Requires authentication.**

**Features:** wfirma.invoices.view

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

### Responses

**200** – Invoice status synchronized.

Content-Type: `application/json`

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/wfirma/invoices/:id/sync-status" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/wfirma/invoices/bulk-delete`

Bulk delete draft invoices and proformas (partial result)

Requires features: wfirma.invoices.manage

**Tags:** wFirma invoices

**Requires authentication.**

**Features:** wfirma.invoices.manage

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – Deleted ids + rejected ids with reasons.

Content-Type: `application/json`

```json
{
  "ok": true,
  "deleted": [
    "00000000-0000-4000-8000-000000000000"
  ],
  "rejected": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "reason": "ksef",
      "message": "string"
    }
  ]
}
```

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/wfirma/invoices/bulk-delete" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"ids\": [
    \"00000000-0000-4000-8000-000000000000\"
  ]
}"
```

## POST `/wfirma/invoices/bulk-send`

Queue selected invoices for e-mail/SMS delivery (partial result)

Requires features: wfirma.invoices.send.bulk

**Tags:** wFirma invoices

**Requires authentication.**

**Features:** wfirma.invoices.send.bulk

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – Queued count plus skipped/blocked counts with reasons.

Content-Type: `application/json`

```json
{
  "ok": true,
  "queued": 1,
  "skipped": 1,
  "blocked": 1,
  "deliveries": 1,
  "reasons": {
    "key": 1
  }
}
```

**400** – Invalid payload.

Content-Type: `application/json`

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

**409** – Send-queue schedule missing or disabled — nothing was queued.

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/wfirma/invoices/bulk-send" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"ids\": [
    \"00000000-0000-4000-8000-000000000000\"
  ],
  \"channels\": [
    \"email\"
  ]
}"
```

## POST `/wfirma/invoices/bulk-send/batch`

Queue hundreds of invoices for e-mail delivery, tracked by a progress job

Requires features: wfirma.invoices.send.bulk

**Tags:** wFirma invoices

**Requires authentication.**

**Features:** wfirma.invoices.send.bulk

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – Nothing was eligible; no job was created.

Content-Type: `application/json`

```json
{
  "ok": true,
  "progressJobId": null,
  "sendable": 1,
  "total": 1
}
```

**202** – Job queued. Track it with progressJobId.

Content-Type: `application/json`

```json
{
  "ok": true,
  "progressJobId": null,
  "sendable": 1,
  "total": 1
}
```

**400** – Invalid payload or no organization selected.

Content-Type: `application/json`

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

**409** – Send-queue schedule missing or disabled — nothing was queued.

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/wfirma/invoices/bulk-send/batch" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"ids\": [
    \"00000000-0000-4000-8000-000000000000\"
  ],
  \"channels\": [
    \"email\"
  ]
}"
```

## POST `/wfirma/invoices/bulk-send/preview`

Count how many of the given invoices would actually be sent, and why the rest would not

Requires features: wfirma.invoices.send.bulk

**Tags:** wFirma invoices

**Requires authentication.**

**Features:** wfirma.invoices.send.bulk

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – Counts only. Nothing is written.

Content-Type: `application/json`

```json
{
  "ok": true,
  "total": 1,
  "sendable": 1,
  "reasons": {
    "key": 1
  }
}
```

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/wfirma/invoices/bulk-send/preview" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"ids\": [
    \"00000000-0000-4000-8000-000000000000\"
  ],
  \"channels\": [
    \"email\"
  ]
}"
```

## GET `/wfirma/invoices/by-company`

List companies with at least minUnpaid unpaid issued invoices

Requires features: wfirma.invoices.view

**Tags:** wFirma invoices

**Requires authentication.**

**Features:** wfirma.invoices.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| createdByUserId | query | any | Optional |
| months | query | any | Optional |
| issuerAccountId | query | any | Optional |
| paymentStatus | query | any | Optional |
| from | query | any | Optional |
| to | query | any | Optional |
| dueState | query | any | Optional |
| search | query | any | Optional |
| minUnpaid | query | any | Optional |

### Responses

**200** – Companies with unpaid/overdue counts and outstanding per currency.

Content-Type: `application/json`

```json
{
  "companies": [
    {
      "companyEntityId": "string",
      "companyName": null,
      "unpaidCount": 1,
      "overdueCount": 1,
      "invoiceCount": 1,
      "outstanding": [
        {
          "currency": "string",
          "amount": "string"
        }
      ]
    }
  ],
  "truncated": true
}
```

**400** – Bad request.

Content-Type: `application/json`

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

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/wfirma/invoices/by-company?minUnpaid=3" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/wfirma/invoices/filter-options`

Users (creators) and sellers (issuer accounts) present in invoices

Requires features: wfirma.invoices.view

**Tags:** wFirma invoices

**Requires authentication.**

**Features:** wfirma.invoices.view

### Responses

**200** – Filter option lists.

Content-Type: `application/json`

```json
{
  "users": [
    {
      "id": "string",
      "name": "string"
    }
  ],
  "sellers": [
    {
      "id": "string",
      "name": "string"
    }
  ]
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/wfirma/invoices/filter-options" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/wfirma/invoices/ghosts`

List issued invoices that no longer exist in wFirma

Requires features: wfirma.invoices.manage

**Tags:** wFirma invoices

**Requires authentication.**

**Features:** wfirma.invoices.manage

### Responses

**200** – Ghost candidates for review.

Content-Type: `application/json`

```json
{
  "ghosts": [
    {
      "id": "00000000-0000-4000-8000-000000000000",
      "invoiceNumber": null,
      "wfirmaInvoiceId": "string",
      "companyName": null,
      "issueDate": null,
      "totalGrossAmount": null,
      "currencyCode": null,
      "state": "missing"
    }
  ],
  "scanned": 1,
  "existing": 1
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/wfirma/invoices/ghosts" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/wfirma/invoices/issue-problems`

List invoices stuck with an issue error, an interrupted attempt, or left as a wFirma draft

Requires features: wfirma.invoices.view

**Tags:** wFirma invoices

**Requires authentication.**

**Features:** wfirma.invoices.view

### Responses

**200** – Paginated problem invoices.

Content-Type: `application/json`

```json
{
  "items": [
    {
      "id": "string",
      "invoiceNumber": null,
      "kind": "string",
      "buyerName": null,
      "problem": "issue_error",
      "lastError": null,
      "totalGrossAmount": null,
      "currencyCode": null,
      "createdAt": null
    }
  ],
  "total": 1,
  "totalPages": 1,
  "page": 1,
  "pageSize": 1
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/wfirma/invoices/issue-problems" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/wfirma/invoices/pdf-bulk`

Queue combined invoices PDF

Selects issued invoices of the given issuer organization (or all accessible issuers) in the date range and queues a background job that downloads each PDF from wFirma and merges them into one file. Track the job with GET /pdf-bulk/{jobId}; download with GET /pdf-bulk/{jobId}/file.

Requires features: wfirma.invoices.view

**Tags:** wFirma invoices

**Requires authentication.**

**Features:** wfirma.invoices.view

### Request Body

Content-Type: `application/json`

```json
{
  "issuerOrganizationId": "00000000-0000-4000-8000-000000000000",
  "from": null,
  "to": null,
  "paymentStatus": "all"
}
```

### Responses

**200** – No matching invoices; no job was created.

Content-Type: `application/json`

```json
{
  "ok": true,
  "progressJobId": null,
  "total": 1
}
```

**202** – Job queued. Track it with progressJobId.

Content-Type: `application/json`

```json
{
  "ok": true,
  "progressJobId": null,
  "total": 1
}
```

**400** – Invalid payload, no organization, or more than 2000 invoices.

Content-Type: `application/json`

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

**500** – The job could not be queued.

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/wfirma/invoices/pdf-bulk" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"issuerOrganizationId\": \"00000000-0000-4000-8000-000000000000\",
  \"from\": null,
  \"to\": null,
  \"paymentStatus\": \"all\"
}"
```

## GET `/wfirma/invoices/pdf-bulk/{jobId}`

Combined invoices PDF job state

Requires features: wfirma.invoices.view

**Tags:** wFirma invoices

**Requires authentication.**

**Features:** wfirma.invoices.view

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

### Responses

**200** – Job state and, once completed, the result summary.

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "status": "pending",
  "processedCount": 1,
  "totalCount": null,
  "progressPercent": 1,
  "errorMessage": null,
  "result": null
}
```

**400** – Invalid job id.

Content-Type: `application/json`

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

**404** – No such job for the caller.

Content-Type: `application/json`

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

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/wfirma/invoices/pdf-bulk/00000000-0000-4000-8000-000000000000" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/wfirma/invoices/pdf-bulk/{jobId}/file`

Combined invoices PDF part

Requires features: wfirma.invoices.view

**Tags:** wFirma invoices

**Requires authentication.**

**Features:** wfirma.invoices.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| jobId | path | any | Required |
| part | query | any | Optional |

### Responses

**200** – The merged PDF part (≤ 10 MB) as an attachment.

Content-Type: `application/json`

**400** – Invalid job id or part number.

Content-Type: `application/json`

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

**404** – No such job or part for the caller, not finished yet (code not_ready), or the file expired (code expired).

Content-Type: `application/json`

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

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/wfirma/invoices/pdf-bulk/00000000-0000-4000-8000-000000000000/file?part=1" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/wfirma/invoices/pdf-bulk/{jobId}/zip`

Combined invoices PDF parts as ZIP

Requires features: wfirma.invoices.view

**Tags:** wFirma invoices

**Requires authentication.**

**Features:** wfirma.invoices.view

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

### Responses

**200** – ZIP (stored, no compression) with every PDF part.

Content-Type: `application/json`

**400** – Invalid job id.

Content-Type: `application/json`

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

**404** – No such job for the caller, not finished yet (code not_ready), or a file expired (code expired).

Content-Type: `application/json`

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

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/wfirma/invoices/pdf-bulk/00000000-0000-4000-8000-000000000000/zip" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/wfirma/invoices/summary`

Amount + count per bucket: all / paid / unpaid / overdue / due

Requires features: wfirma.invoices.view

**Tags:** wFirma invoices

**Requires authentication.**

**Features:** wfirma.invoices.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| companyId | query | any | Optional |
| propertyId | query | any | Optional |
| withoutProperty | query | any | Required |
| createdByUserId | query | any | Optional |
| months | query | any | Optional |
| issuerAccountId | query | any | Optional |
| paymentStatus | query | any | Optional |
| from | query | any | Optional |
| to | query | any | Optional |
| dueState | query | any | Optional |
| search | query | any | Optional |

### Responses

**200** – Aggregated summary.

Content-Type: `application/json`

```json
{
  "all": {
    "count": 1,
    "totals": [
      {
        "currency": "string",
        "amount": "string"
      }
    ]
  },
  "paid": {
    "count": 1,
    "totals": [
      {
        "currency": "string",
        "amount": "string"
      }
    ]
  },
  "unpaid": {
    "count": 1,
    "totals": [
      {
        "currency": "string",
        "amount": "string"
      }
    ]
  },
  "overdue": {
    "count": 1,
    "totals": [
      {
        "currency": "string",
        "amount": "string"
      }
    ]
  },
  "due": {
    "count": 1,
    "totals": [
      {
        "currency": "string",
        "amount": "string"
      }
    ]
  },
  "partial": {
    "count": 1,
    "totals": [
      {
        "currency": "string",
        "amount": "string"
      }
    ]
  }
}
```

**400** – Bad request.

Content-Type: `application/json`

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

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/wfirma/invoices/summary" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/wfirma/pay-links/{token}/start-anyway`

Start a payment for a link despite an overlapping payment in progress

Public endpoint used by the „pay anyway" form on the payment page. Bypasses the guard that blocks a second concurrent payment covering the same invoice. POST on purpose: the guard exists because link scanners open payment URLs, and a GET escape hatch would be walked by the very crawlers the guard protects against. Answers 303 to the gateway, or back to the payment page when the payment cannot be started.

**Tags:** wFirma payment links

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

### Responses

**201** – Success response

Content-Type: `application/json`

**303** – Redirect to the payment gateway, or back to the payment page

Content-Type: `application/json`

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/wfirma/pay-links/:token/start-anyway" \
  -H "Accept: application/json"
```

## POST `/wfirma/property-transfer`

Move the resolved closure of invoices and services to the target property

Requires features: wfirma.invoices.manage, wfirma.subscriptions.manage

**Tags:** wFirma property transfer

**Requires authentication.**

**Features:** wfirma.invoices.manage, wfirma.subscriptions.manage

### Request Body

Content-Type: `application/json`

```json
{
  "anchor": {
    "kind": "invoice",
    "id": "00000000-0000-4000-8000-000000000000"
  },
  "targetPropertyId": "00000000-0000-4000-8000-000000000000",
  "invoiceIds": [
    "00000000-0000-4000-8000-000000000000"
  ],
  "subscriptionIds": [
    "00000000-0000-4000-8000-000000000000"
  ]
}
```

### Responses

**200** – Records moved.

Content-Type: `application/json`

```json
{
  "ok": true,
  "movedInvoices": 1,
  "movedSubscriptions": 1,
  "sourcePropertyIds": [
    "string"
  ]
}
```

**400** – Bad request, empty selection, or a record belongs to another company.

Content-Type: `application/json`

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

**404** – Anchor record or target property was not found.

Content-Type: `application/json`

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

**409** – The submitted set no longer matches the recomputed closure.

Content-Type: `application/json`

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

**422** – The transfer graph is too large.

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/wfirma/property-transfer" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"anchor\": {
    \"kind\": \"invoice\",
    \"id\": \"00000000-0000-4000-8000-000000000000\"
  },
  \"targetPropertyId\": \"00000000-0000-4000-8000-000000000000\",
  \"invoiceIds\": [
    \"00000000-0000-4000-8000-000000000000\"
  ],
  \"subscriptionIds\": [
    \"00000000-0000-4000-8000-000000000000\"
  ]
}"
```

## POST `/wfirma/property-transfer/preview`

Resolve the transfer closure without writing anything

Requires features: wfirma.invoices.manage, wfirma.subscriptions.manage

**Tags:** wFirma property transfer

**Requires authentication.**

**Features:** wfirma.invoices.manage, wfirma.subscriptions.manage

### Request Body

Content-Type: `application/json`

```json
{
  "anchor": {
    "kind": "invoice",
    "id": "00000000-0000-4000-8000-000000000000"
  },
  "targetPropertyId": "00000000-0000-4000-8000-000000000000"
}
```

### Responses

**200** – Closure resolved.

Content-Type: `application/json`

**400** – Bad request, or the target property has no company.

Content-Type: `application/json`

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

**404** – Anchor record or target property was not found.

Content-Type: `application/json`

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

**422** – The transfer graph is too large to preview.

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/wfirma/property-transfer/preview" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"anchor\": {
    \"kind\": \"invoice\",
    \"id\": \"00000000-0000-4000-8000-000000000000\"
  },
  \"targetPropertyId\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## DELETE `/wfirma/subscriptions`

Delete subscription

CRUD for recurring billing definitions that generate draft wFirma invoices each period.

Requires features: wfirma.subscriptions.manage

**Tags:** wFirma subscriptions

**Requires authentication.**

**Features:** wfirma.subscriptions.manage

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

### Responses

**200** – Deleted.

Content-Type: `application/json`

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

**400** – Invalid id.

Content-Type: `application/json`

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

### Example

```bash
curl -X DELETE "https://crm.nf-hotel.pl/wfirma/subscriptions?id=00000000-0000-4000-8000-000000000000" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## GET `/wfirma/subscriptions`

List subscriptions

CRUD for recurring billing definitions that generate draft wFirma invoices each period.

Requires features: wfirma.subscriptions.view

**Tags:** wFirma subscriptions

**Requires authentication.**

**Features:** wfirma.subscriptions.view

### Parameters
| Name | Location | Type | Description |
| --- | --- | --- | --- |
| page | query | any | Optional |
| pageSize | query | any | Optional |
| sort | query | any | Optional |
| order | query | any | Optional |
| companyId | query | any | Optional |
| propertyId | query | any | Optional |
| status | query | any | Optional |
| state | query | any | Optional |
| contractId | query | any | Optional |
| id | query | any | Optional |
| unbilled | query | any | Optional |

### Responses

**200** – Subscriptions.

Content-Type: `application/json`

```json
{
  "items": [],
  "total": 1
}
```

**400** – unbilled=1 without companyId/propertyId.

Content-Type: `application/json`

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

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/wfirma/subscriptions?page=1&pageSize=50" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/wfirma/subscriptions`

Create subscription

CRUD for recurring billing definitions that generate draft wFirma invoices each period.

Requires features: wfirma.subscriptions.manage

**Tags:** wFirma subscriptions

**Requires authentication.**

**Features:** wfirma.subscriptions.manage

### Request Body

Content-Type: `application/json`

```json
{
  "companyEntityId": "00000000-0000-4000-8000-000000000000",
  "issuerOrganizationId": null,
  "issuerAccountId": null,
  "propertyId": null,
  "subjectCategoryId": null,
  "name": null,
  "productId": null,
  "productName": "string",
  "sku": null,
  "unit": "szt.",
  "quantity": 1,
  "priceEntryMode": "net",
  "unitNet": 1,
  "unitGross": 1,
  "renewalUnitNet": null,
  "renewalUnitGross": null,
  "vatRate": 23,
  "reverseCharge": false,
  "kind": "vat",
  "period": "month",
  "initialTermMonths": null,
  "endDate": null,
  "autoRenew": true,
  "autoRenewEndDate": null,
  "autoInvoice": true,
  "autoSendEmail": true,
  "autoSendSms": false,
  "leadDays": 0,
  "paymentTermDays": 7,
  "buyer": null,
  "recipient": null,
  "recipientMode": "inherit",
  "notes": null,
  "notesOnInvoice": false,
  "correctedInvoiceId": null,
  "correctionReason": null
}
```

### Responses

**201** – Created.

Content-Type: `application/json`

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

**404** – Company not found.

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/wfirma/subscriptions" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"companyEntityId\": \"00000000-0000-4000-8000-000000000000\",
  \"issuerOrganizationId\": null,
  \"issuerAccountId\": null,
  \"propertyId\": null,
  \"subjectCategoryId\": null,
  \"name\": null,
  \"productId\": null,
  \"productName\": \"string\",
  \"sku\": null,
  \"unit\": \"szt.\",
  \"quantity\": 1,
  \"priceEntryMode\": \"net\",
  \"unitNet\": 1,
  \"unitGross\": 1,
  \"renewalUnitNet\": null,
  \"renewalUnitGross\": null,
  \"vatRate\": 23,
  \"reverseCharge\": false,
  \"kind\": \"vat\",
  \"period\": \"month\",
  \"initialTermMonths\": null,
  \"endDate\": null,
  \"autoRenew\": true,
  \"autoRenewEndDate\": null,
  \"autoInvoice\": true,
  \"autoSendEmail\": true,
  \"autoSendSms\": false,
  \"leadDays\": 0,
  \"paymentTermDays\": 7,
  \"buyer\": null,
  \"recipient\": null,
  \"recipientMode\": \"inherit\",
  \"notes\": null,
  \"notesOnInvoice\": false,
  \"correctedInvoiceId\": null,
  \"correctionReason\": null
}"
```

## PUT `/wfirma/subscriptions`

Update subscription

CRUD for recurring billing definitions that generate draft wFirma invoices each period.

Requires features: wfirma.subscriptions.manage

**Tags:** wFirma subscriptions

**Requires authentication.**

**Features:** wfirma.subscriptions.manage

### Request Body

Content-Type: `application/json`

```json
{
  "id": "00000000-0000-4000-8000-000000000000",
  "issuerOrganizationId": null,
  "issuerAccountId": null,
  "propertyId": null,
  "subjectCategoryId": null,
  "name": null,
  "productId": null,
  "sku": null,
  "unit": "szt.",
  "quantity": 1,
  "priceEntryMode": "net",
  "renewalUnitNet": null,
  "renewalUnitGross": null,
  "vatRate": 23,
  "reverseCharge": false,
  "kind": "vat",
  "period": "month",
  "initialTermMonths": null,
  "endDate": null,
  "autoRenew": true,
  "autoRenewEndDate": null,
  "autoInvoice": true,
  "autoSendEmail": true,
  "autoSendSms": false,
  "leadDays": 0,
  "paymentTermDays": 7,
  "buyer": null,
  "recipient": null,
  "notes": null,
  "correctedInvoiceId": null,
  "correctionReason": null,
  "deactivatedAt": null
}
```

### Responses

**200** – Updated.

Content-Type: `application/json`

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

**404** – Not found.

Content-Type: `application/json`

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

### Example

```bash
curl -X PUT "https://crm.nf-hotel.pl/wfirma/subscriptions" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"id\": \"00000000-0000-4000-8000-000000000000\",
  \"issuerOrganizationId\": null,
  \"issuerAccountId\": null,
  \"propertyId\": null,
  \"subjectCategoryId\": null,
  \"name\": null,
  \"productId\": null,
  \"sku\": null,
  \"unit\": \"szt.\",
  \"quantity\": 1,
  \"priceEntryMode\": \"net\",
  \"renewalUnitNet\": null,
  \"renewalUnitGross\": null,
  \"vatRate\": 23,
  \"reverseCharge\": false,
  \"kind\": \"vat\",
  \"period\": \"month\",
  \"initialTermMonths\": null,
  \"endDate\": null,
  \"autoRenew\": true,
  \"autoRenewEndDate\": null,
  \"autoInvoice\": true,
  \"autoSendEmail\": true,
  \"autoSendSms\": false,
  \"leadDays\": 0,
  \"paymentTermDays\": 7,
  \"buyer\": null,
  \"recipient\": null,
  \"notes\": null,
  \"correctedInvoiceId\": null,
  \"correctionReason\": null,
  \"deactivatedAt\": null
}"
```

## POST `/wfirma/subscriptions/{id}/apply-correction-price`

Apply corrected price

Writes the corrected unit price onto the subscription period and, when confirmed, onto the renewal price used by future periods. Leaves a trace on the subscription timeline.

Requires features: wfirma.subscriptions.manage

**Tags:** wFirma subscriptions

**Requires authentication.**

**Features:** wfirma.subscriptions.manage

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

### Request Body

Content-Type: `application/json`

```json
{
  "correctionInvoiceId": "00000000-0000-4000-8000-000000000000",
  "unitNet": 1,
  "unitGross": 1,
  "applyToRenewal": true
}
```

### Responses

**200** – Price applied.

Content-Type: `application/json`

```json
{
  "ok": true,
  "renewalChanged": true,
  "deactivated": true
}
```

**400** – Invalid id or body.

Content-Type: `application/json`

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

**404** – Subscription not found.

Content-Type: `application/json`

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

**409** – Not a correction, it does not concern this subscription, or the subscription never renews.

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/wfirma/subscriptions/:id/apply-correction-price" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"correctionInvoiceId\": \"00000000-0000-4000-8000-000000000000\",
  \"unitNet\": 1,
  \"unitGross\": 1,
  \"applyToRenewal\": true
}"
```

## POST `/wfirma/subscriptions/{id}/generate`

Generate next invoice

Creates a draft invoice for the subscription's next due period and advances it.

Requires features: wfirma.subscriptions.manage

**Tags:** wFirma subscriptions

**Requires authentication.**

**Features:** wfirma.subscriptions.manage

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

### Responses

**200** – Draft created.

Content-Type: `application/json`

```json
{
  "ok": true,
  "invoiceId": "00000000-0000-4000-8000-000000000000"
}
```

**404** – Subscription not found.

Content-Type: `application/json`

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

**409** – Subscription is not active.

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/wfirma/subscriptions/:id/generate" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## DELETE `/wfirma/subscriptions/{id}/pinned-comments`

Unpin a comment from the contract

A subscription row is one billing period; renewal creates a new row. Pinning re-points a comment at the contract and stamps a validity window, so the note stays visible in the periods that follow — including ones created by the automatic renewal. Anchors are period boundaries: pinning applies from the start of the period it was pinned from, unpinning from the end of the period it was unpinned from.

Requires features: wfirma.subscriptions.view, comments.edit

**Tags:** wFirma subscriptions

**Requires authentication.**

**Features:** wfirma.subscriptions.view, comments.edit

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

### Responses

**200** – Unpinned.

Content-Type: `application/json`

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

**400** – Comment is not pinned, or belongs to another contract.

Content-Type: `application/json`

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

**404** – Service or comment not found.

Content-Type: `application/json`

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

### Example

```bash
curl -X DELETE "https://crm.nf-hotel.pl/wfirma/subscriptions/:id/pinned-comments?commentId=00000000-0000-4000-8000-000000000000" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## PUT `/wfirma/subscriptions/{id}/pinned-comments`

Pin a comment to the contract

A subscription row is one billing period; renewal creates a new row. Pinning re-points a comment at the contract and stamps a validity window, so the note stays visible in the periods that follow — including ones created by the automatic renewal. Anchors are period boundaries: pinning applies from the start of the period it was pinned from, unpinning from the end of the period it was unpinned from.

Requires features: wfirma.subscriptions.view, comments.edit

**Tags:** wFirma subscriptions

**Requires authentication.**

**Features:** wfirma.subscriptions.view, comments.edit

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

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – Pinned.

Content-Type: `application/json`

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

**400** – Reply, or a comment from another contract.

Content-Type: `application/json`

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

**404** – Service or comment not found.

Content-Type: `application/json`

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

### Example

```bash
curl -X PUT "https://crm.nf-hotel.pl/wfirma/subscriptions/:id/pinned-comments" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"commentId\": \"00000000-0000-4000-8000-000000000000\"
}"
```

## POST `/wfirma/subscriptions/bulk-dates`

Bulk update subscription dates

Requires features: wfirma.subscriptions.bulk

**Tags:** wFirma subscriptions

**Requires authentication.**

**Features:** wfirma.subscriptions.bulk

### Request Body

Content-Type: `application/json`

```json
{
  "ids": [
    "00000000-0000-4000-8000-000000000000"
  ],
  "startDate": null,
  "endDate": null,
  "nextPeriodStart": null
}
```

### Responses

**200** – Subscriptions updated.

Content-Type: `application/json`

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/wfirma/subscriptions/bulk-dates" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"ids\": [
    \"00000000-0000-4000-8000-000000000000\"
  ],
  \"startDate\": null,
  \"endDate\": null,
  \"nextPeriodStart\": null
}"
```

## POST `/wfirma/subscriptions/bulk-invoice`

Create draft invoices from selected subscriptions

Requires features: wfirma.subscriptions.bulk

**Tags:** wFirma subscriptions

**Requires authentication.**

**Features:** wfirma.subscriptions.bulk

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – Draft invoices created.

Content-Type: `application/json`

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/wfirma/subscriptions/bulk-invoice" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"ids\": [
    \"00000000-0000-4000-8000-000000000000\"
  ],
  \"issuerAccountId\": null
}"
```

## POST `/wfirma/subscriptions/draft-lines`

Preview invoice lines from subscriptions

Maps the given subscriptions into invoice lines for the manual invoice form; does not persist or advance the billing cycle.

Requires features: wfirma.subscriptions.view

**Tags:** wFirma subscriptions

**Requires authentication.**

**Features:** wfirma.subscriptions.view

### Request Body

Content-Type: `application/json`

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

### Responses

**200** – Mapped invoice lines.

Content-Type: `application/json`

```json
{
  "lines": [
    {
      "subscriptionId": "string",
      "name": "string",
      "quantity": 1,
      "unitNet": 1,
      "unitGross": 1,
      "vatRate": 1,
      "vatCode": null,
      "unit": "string",
      "productId": null,
      "sku": null
    }
  ],
  "serviceStartDate": null,
  "serviceEndDate": null,
  "propertyId": null,
  "subscriptionId": null,
  "currencyCode": null,
  "priceEntryMode": null,
  "reverseCharge": true,
  "recipient": null,
  "kind": null,
  "notes": null
}
```

**400** – Bad request.

Content-Type: `application/json`

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

**404** – No subscriptions found.

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/wfirma/subscriptions/draft-lines" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d "{
  \"ids\": [
    \"00000000-0000-4000-8000-000000000000\"
  ]
}"
```

## GET `/wfirma/subscriptions/filter-options`

Users (creators) and issuer organizations present in subscriptions

Requires features: wfirma.subscriptions.view

**Tags:** wFirma subscriptions

**Requires authentication.**

**Features:** wfirma.subscriptions.view

### Responses

**200** – Filter option lists.

Content-Type: `application/json`

```json
{
  "users": [
    {
      "id": "string",
      "name": "string"
    }
  ],
  "issuers": [
    {
      "id": "string",
      "name": "string"
    }
  ]
}
```

### Example

```bash
curl -X GET "https://crm.nf-hotel.pl/wfirma/subscriptions/filter-options" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/wfirma/subscriptions/run`

Generate due draft invoices

Creates draft invoices for every active subscription whose next period is due. Drafts are issued manually afterwards.

Requires features: wfirma.subscriptions.manage

**Tags:** wFirma subscriptions

**Requires authentication.**

**Features:** wfirma.subscriptions.manage

### Responses

**200** – Drafts created.

Content-Type: `application/json`

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

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/wfirma/subscriptions/run" \
  -H "Accept: application/json" \
  -H "authorization: Bearer <token>"
```

## POST `/wfirma/webhooks/{accountId}`

Receive a wFirma webhook event

Public endpoint called by wFirma (Ustawienia → Inne → Webhooks, or webhooks/add via API). Authenticated by a per-account shared secret in the `token` query parameter. Always answers 200 with status.code=OK so wFirma never counts a delivery as failed — wFirma disables a webhook after 10 failed deliveries. Processing happens asynchronously.

**Tags:** wFirma webhooks

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

### Responses

**200** – Acknowledged.

Content-Type: `application/json`

**403** – Unknown account or bad token.

Content-Type: `application/json`

### Example

```bash
curl -X POST "https://crm.nf-hotel.pl/wfirma/webhooks/:accountId" \
  -H "Accept: application/json"
```