openapi: 3.0.3 info: title: Copenhagen Truck Wash API description: | This API provides access to the Copenhagen Truck Wash system, managing orders, bookings, departments, products, customers, and various integrations including e-conomic, Stripe, XLVask, and more. ## Authentication Most endpoints require authentication using a Bearer token obtained from the `/auth/login` or `/auth/employee/login` endpoints. ## Permissions Many endpoints require specific permissions that are assigned to user groups/roles. ## Subusers and customer targeting When authenticated as a subuser, most customer-scoped endpoints require an explicit target customer context. Provide the header `X-Customer-Number: ` to target a specific customer. If omitted, the API attempts to infer the customer from the authenticated user context when possible. Classic user sessions ignore this header. version: 1.0.0 contact: name: Copenhagen Truck Wash email: support@truckwash.dk servers: - url: https://api.truckwash.dk description: Production server (.dk) - url: https://api.truckwash.io description: Production server (.io) - url: http://localhost/api description: Local development server security: - BearerAuth: [] tags: - name: Authentication description: User and employee authentication endpoints - name: Security description: Account security and passkey management endpoints - name: Users description: User management and customer operations - name: Limited Backoffice description: Limited backoffice employee and department management - name: Search description: System-wide search endpoints - name: Orders description: Order creation, management, and retrieval - name: Order Items description: Managing items within orders - name: Bookings description: Booking management for wash services - name: Departments description: Department and location management - name: Products description: Product catalog and pricing - name: Categories description: Product category management - name: Invoices description: Invoice generation and management - name: Payments description: Payment processing and collection - name: Vehicles description: Vehicle registration and management - name: Notifications description: System notifications and alerts - name: Statistics description: Business analytics and reporting - name: Modules description: Third-party integrations and modules - name: Attachments description: File upload and attachment management - name: Forms description: Form submissions and management - name: Worker description: System worker status and maintenance - name: Error Reports description: Authenticated application error reporting - name: Plate Scans description: License plate scanning operations - name: Config description: Module configuration management - name: Release Manager description: Release channel, deployment, and operation management - name: Branding description: Branding options management - name: Roles description: Role and permission management - name: Self-Serve description: Self-serve lane operations and questions - name: Goals description: Department goals management - name: Subusers description: Subuser registration and setup - name: Bird description: Voice Calls via Bird paths: /error-reports: post: tags: - Error Reports summary: Submit an authenticated user error report operationId: submitErrorReport security: - BearerAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ErrorReportSubmissionRequest' responses: '201': description: Error report submitted content: application/json: schema: $ref: '#/components/schemas/ErrorReportResponse' '400': description: Validation error content: application/json: schema: $ref: '#/components/schemas/Error' '401': description: Authentication required content: application/json: schema: $ref: '#/components/schemas/Error' /superuser/error-reports: get: tags: - Error Reports summary: List error reports for superusers operationId: listSuperuserErrorReports security: - BearerAuth: [] parameters: - name: status in: query required: false schema: type: string enum: [open, resolved, all] default: open - name: q in: query required: false schema: type: string - name: limit in: query required: false schema: type: integer minimum: 1 maximum: 200 default: 50 - name: offset in: query required: false schema: type: integer minimum: 0 default: 0 responses: '200': description: Error reports retrieved content: application/json: schema: $ref: '#/components/schemas/ErrorReportListResponse' '403': description: Missing superuser error report permission content: application/json: schema: $ref: '#/components/schemas/Error' /superuser/error-reports/{id}: get: tags: - Error Reports summary: Get an error report detail operationId: getSuperuserErrorReport security: - BearerAuth: [] parameters: - name: id in: path required: true schema: type: integer minimum: 1 responses: '200': description: Error report retrieved content: application/json: schema: $ref: '#/components/schemas/ErrorReportResponse' '404': description: Error report not found content: application/json: schema: $ref: '#/components/schemas/Error' /superuser/error-reports/{id}/status: patch: tags: - Error Reports summary: Mark an error report open or resolved operationId: updateSuperuserErrorReportStatus security: - BearerAuth: [] parameters: - name: id in: path required: true schema: type: integer minimum: 1 requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ErrorReportStatusUpdateRequest' responses: '200': description: Error report status updated content: application/json: schema: $ref: '#/components/schemas/ErrorReportResponse' '400': description: Validation error content: application/json: schema: $ref: '#/components/schemas/Error' '403': description: Missing superuser error report resolve permission content: application/json: schema: $ref: '#/components/schemas/Error' # Bird Voice Calls /bird/voice/calls: post: tags: - Bird summary: Create/place a voice call via Bird operationId: birdCreateVoiceCall parameters: - in: query name: workspaceId schema: type: string format: uuid required: false description: Bird Workspace identifier (falls back to module configuration if omitted) - in: query name: channelId schema: type: string format: uuid required: false description: Bird Channel identifier (falls back to module configuration if omitted) requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/BirdVoiceCallCreateRequest' responses: '200': description: Call created content: application/json: schema: { $ref: '#/components/schemas/BirdVoiceCallSingleResponse' } get: tags: - Bird summary: List voice calls operationId: birdListVoiceCalls parameters: - in: query name: workspaceId schema: type: string format: uuid required: false description: Bird Workspace identifier (falls back to module configuration if omitted) - in: query name: channelId schema: type: string format: uuid required: false description: Bird Channel identifier (falls back to module configuration if omitted) - in: query name: limit schema: type: integer minimum: 1 maximum: 1000 - in: query name: pageToken schema: type: string - in: query name: startAt schema: type: string format: date-time - in: query name: endAt schema: type: string format: date-time - in: query name: status schema: type: string - in: query name: type schema: type: string - in: query name: to schema: type: string - in: query name: from schema: type: string - in: query name: duration schema: type: integer - in: query name: direction schema: type: string - in: query name: id schema: type: string format: uuid - in: query name: tag schema: oneOf: - type: string - type: array items: type: string responses: '200': description: A list of calls content: application/json: schema: { $ref: '#/components/schemas/BirdVoiceCallListResponse' } /bird/voice/calls/log: get: tags: - Bird summary: List workspace call log entries operationId: birdListVoiceCallsLog parameters: - in: query name: workspaceId schema: type: string format: uuid required: false description: Bird Workspace identifier (falls back to module configuration if omitted) - in: query name: limit schema: type: integer minimum: 1 maximum: 1000 - in: query name: pageToken schema: type: string - in: query name: startAt schema: type: string format: date-time - in: query name: endAt schema: type: string format: date-time - in: query name: channelId schema: oneOf: - type: string - type: array items: type: string format: uuid - in: query name: status schema: type: string - in: query name: type schema: type: string - in: query name: to schema: type: string - in: query name: from schema: type: string - in: query name: duration schema: type: integer - in: query name: direction schema: type: string - in: query name: id schema: type: string format: uuid - in: query name: tag schema: oneOf: - type: string - type: array items: type: string responses: '200': description: Workspace call log entries content: application/json: schema: { $ref: '#/components/schemas/BirdVoiceCallsLogResponse' } /bird/voice/calls/{id}: get: tags: - Bird summary: Get a voice call by ID operationId: birdGetVoiceCall parameters: - in: query name: workspaceId schema: type: string format: uuid required: false description: Bird Workspace identifier (falls back to module configuration if omitted) - in: query name: channelId schema: type: string format: uuid required: false description: Bird Channel identifier (falls back to module configuration if omitted) - in: path name: id required: true schema: type: string format: uuid responses: '200': description: Call details content: application/json: schema: { $ref: '#/components/schemas/BirdVoiceCallSingleResponse' } patch: tags: - Bird summary: Update a voice call by ID operationId: birdUpdateVoiceCall parameters: - in: query name: workspaceId schema: type: string format: uuid required: false - in: query name: channelId schema: type: string format: uuid required: false - in: path name: id required: true schema: type: string format: uuid requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/BirdVoiceCallUpdateRequest' responses: '200': description: Call update accepted content: application/json: schema: { $ref: '#/components/schemas/BirdVoiceCallSingleResponse' } /bird/voice/calls/{id}/answer: post: tags: - Bird summary: Answer an incoming voice call by ID operationId: birdAnswerVoiceCall parameters: - in: query name: workspaceId schema: type: string format: uuid required: false - in: query name: channelId schema: type: string format: uuid required: false - in: path name: id required: true schema: type: string format: uuid requestBody: required: false content: application/json: schema: $ref: '#/components/schemas/BirdVoiceCallAnswerRequest' responses: '200': description: Answer command accepted content: application/json: schema: { $ref: '#/components/schemas/BirdVoiceCallCommandResponse' } /bird/voice/calls/{id}/ringing: post: tags: - Bird summary: Mark voice call as ringing by ID operationId: birdRingingVoiceCall parameters: - in: query name: workspaceId schema: type: string format: uuid required: false - in: query name: channelId schema: type: string format: uuid required: false - in: path name: id required: true schema: type: string format: uuid requestBody: required: false content: application/json: schema: $ref: '#/components/schemas/BirdVoiceCallRingingRequest' responses: '200': description: Ringing command accepted content: application/json: schema: { $ref: '#/components/schemas/BirdVoiceCallCommandResponse' } /bird/voice/calls/{id}/hangup: post: tags: - Bird summary: Hang up a voice call by ID operationId: birdHangupVoiceCall parameters: - in: query name: workspaceId schema: type: string format: uuid required: false - in: query name: channelId schema: type: string format: uuid required: false - in: path name: id required: true schema: type: string format: uuid requestBody: required: false content: application/json: schema: $ref: '#/components/schemas/BirdVoiceCallHangupRequest' responses: '200': description: Hangup requested content: application/json: schema: { $ref: '#/components/schemas/BirdVoiceCallCommandResponse' } /bird/voice/calls/{id}/playback: post: tags: - Bird summary: Playback media on a voice call by ID operationId: birdPlaybackVoiceCall parameters: - in: query name: workspaceId schema: type: string format: uuid required: false - in: query name: channelId schema: type: string format: uuid required: false - in: path name: id required: true schema: type: string format: uuid requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/BirdVoiceCallPlaybackRequest' responses: '200': description: Playback command accepted content: application/json: schema: { $ref: '#/components/schemas/BirdVoiceCallCommandResponse' } /bird/voice/calls/{id}/say: post: tags: - Bird summary: Say a message on an active voice call and optionally hang up operationId: birdSayOnVoiceCall parameters: - in: query name: workspaceId schema: type: string format: uuid required: false - in: query name: channelId schema: type: string format: uuid required: false - in: path name: id required: true schema: type: string format: uuid requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/BirdVoiceCallSayRequest' responses: '200': description: Say command accepted content: application/json: schema: { $ref: '#/components/schemas/BirdVoiceCallCommandResponse' } /bird/voice/calls/{id}/gather: post: tags: - Bird summary: Gather input from a voice call by ID operationId: birdGatherVoiceCall parameters: - in: query name: workspaceId schema: type: string format: uuid required: false - in: query name: channelId schema: type: string format: uuid required: false - in: path name: id required: true schema: type: string format: uuid requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/BirdVoiceCallGatherRequest' responses: '200': description: Gather command accepted content: application/json: schema: { $ref: '#/components/schemas/BirdVoiceCallCommandResponse' } /bird/voice/calls/{id}/bridge: post: tags: - Bird summary: Bridge a voice call by ID operationId: birdBridgeVoiceCall parameters: - in: query name: workspaceId schema: type: string format: uuid required: false - in: query name: channelId schema: type: string format: uuid required: false - in: path name: id required: true schema: type: string format: uuid requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/BirdVoiceCallBridgeRequest' responses: '200': description: Bridge command accepted content: application/json: schema: { $ref: '#/components/schemas/BirdVoiceCallBridgeResponse' } /bird/voice/calls/{id}/record: post: tags: - Bird summary: Record call audio by ID operationId: birdRecordVoiceCall parameters: - in: query name: workspaceId schema: type: string format: uuid required: false - in: query name: channelId schema: type: string format: uuid required: false - in: path name: id required: true schema: type: string format: uuid requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/BirdVoiceCallRecordRequest' responses: '200': description: Record command accepted content: application/json: schema: { $ref: '#/components/schemas/BirdVoiceCallCommandResponse' } /bird/voice/calls/{id}/recordings: post: tags: - Bird summary: Create a call recording session operationId: birdCreateVoiceCallRecording parameters: - in: query name: workspaceId schema: type: string format: uuid required: false - in: query name: channelId schema: type: string format: uuid required: false - in: path name: id required: true schema: type: string format: uuid requestBody: required: false content: application/json: schema: $ref: '#/components/schemas/BirdVoiceCallRecordingCreateRequest' responses: '200': description: Recording session created content: application/json: schema: { $ref: '#/components/schemas/BirdVoiceCallRecordingSingleResponse' } get: tags: - Bird summary: List call recordings for a voice call operationId: birdListVoiceCallRecordings parameters: - in: query name: workspaceId schema: type: string format: uuid required: false - in: query name: channelId schema: type: string format: uuid required: false - in: path name: id required: true schema: type: string format: uuid - in: query name: limit schema: type: integer minimum: 1 maximum: 1000 - in: query name: pageToken schema: type: string responses: '200': description: List of call recordings content: application/json: schema: { $ref: '#/components/schemas/BirdVoiceCallRecordingListResponse' } /bird/voice/calls/{id}/recordings/{recordingId}: get: tags: - Bird summary: Get a single call recording operationId: birdGetVoiceCallRecording parameters: - in: query name: workspaceId schema: type: string format: uuid required: false - in: query name: channelId schema: type: string format: uuid required: false - in: path name: id required: true schema: type: string format: uuid - in: path name: recordingId required: true schema: type: string format: uuid responses: '200': description: Call recording details content: application/json: schema: { $ref: '#/components/schemas/BirdVoiceCallRecordingSingleResponse' } patch: tags: - Bird summary: Update call recording state operationId: birdUpdateVoiceCallRecording parameters: - in: query name: workspaceId schema: type: string format: uuid required: false - in: query name: channelId schema: type: string format: uuid required: false - in: path name: id required: true schema: type: string format: uuid - in: path name: recordingId required: true schema: type: string format: uuid requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/BirdVoiceCallRecordingUpdateRequest' responses: '200': description: Recording update accepted content: application/json: schema: { $ref: '#/components/schemas/BirdVoiceCallRecordingSingleResponse' } /bird/voice/calls/{id}/insights: get: tags: - Bird summary: Get voice call insights operationId: birdGetVoiceCallInsights parameters: - in: query name: workspaceId schema: type: string format: uuid required: false - in: query name: channelId schema: type: string format: uuid required: false - in: path name: id required: true schema: type: string format: uuid responses: '200': description: Voice call insights content: application/json: schema: { $ref: '#/components/schemas/BirdVoiceCallInsightsResponse' } /bird/voice/calls/test-outbound: post: tags: - Bird summary: Place a test outbound call and hang up when accepted operationId: birdTestOutboundVoiceCall description: Calls +45 42 33 11 28 and hangs up when the call reaches accepted/ongoing state. parameters: - in: query name: workspaceId schema: type: string format: uuid required: false description: Bird Workspace identifier (falls back to module configuration if omitted) - in: query name: channelId schema: type: string format: uuid required: false description: Bird Channel identifier (falls back to module configuration if omitted) requestBody: required: false content: application/json: schema: $ref: '#/components/schemas/BirdTestOutboundCallRequest' responses: '200': description: Test call created and either hung up or timed out content: application/json: schema: { $ref: '#/components/schemas/BirdTestOutboundCallResponse' } /bird/voice/calls/webhook/inbound: post: tags: - Bird summary: Process inbound Bird voice call lifecycle operationId: birdInboundVoiceCallWebhook description: > Stateful inbound-call webhook that owns department and gate selection for phone-controlled gates. The preferred Bird Flow Builder integration is the native-flow mode: send top-level `callId`, `channelId`, and `workspaceId` to fetch IVR prompt data, then submit the selected DTMF digits as `keys` in a follow-up request. In this mode the webhook returns a plain `200 OK` JSON body with `prompt`, `stage`, and `gather` settings that Bird native voice steps can consume directly. For backward compatibility, the webhook also supports the older `{ payload, request, waitConditions }` contract and returns a raw `202 Accepted` Bird `callCommand` gather envelope that resumes on `call_command_gather_finished`. The Bird Flow itself must answer the inbound call before invoking this HTTP step; the backend answer attempt is only a best-effort fallback. Department options are generated from `department_gates` records with `config.type=PHONE_CALL`, ordered by `departments.order_priority`, and support multi-digit DTMF selections such as `10#`. After a department is chosen, the webhook returns compact gate options, for example `Press 1 for exit` when exit is the only available phone-controlled gate for that department. When a valid gate is confirmed, the webhook opens the gate through the corresponding `department_gates` phone-call record and returns a `200 OK` completion result. parameters: - in: query name: workspaceId schema: type: string format: uuid required: false description: Bird Workspace identifier (fallbacks to payload/state/module configuration) - in: query name: channelId schema: type: string format: uuid required: false description: Bird Channel identifier (fallbacks to payload/state/module configuration) - in: query name: callId schema: type: string required: false description: Bird Call identifier (fallbacks to payload fields) requestBody: required: false content: application/json: schema: $ref: '#/components/schemas/BirdInboundCallWebhookRequest' responses: '200': description: Native-flow gather data or gate action result for this webhook invocation content: application/json: schema: oneOf: - $ref: '#/components/schemas/BirdInboundCallWebhookFlowGatherResponse' - $ref: '#/components/schemas/BirdInboundCallWebhookActionResultResponse' '202': description: Gather command accepted and returned to Bird content: application/json: schema: $ref: '#/components/schemas/BirdInboundCallWebhookGatherAcceptedResponse' '400': description: Malformed Bird webhook payload content: application/json: schema: $ref: '#/components/schemas/BirdInboundCallWebhookTransportErrorResponse' # Bird Numbers /bird/numbers: get: tags: - Bird summary: List your numbers operationId: birdListNumbers parameters: - in: query name: workspaceId required: false schema: type: string format: uuid description: Bird Workspace identifier (optional if configured) - in: query name: page required: false schema: type: integer - in: query name: limit required: false schema: type: integer responses: '200': description: A list of numbers content: application/json: schema: $ref: '#/components/schemas/BirdNumberListResponse' /bird/numbers/{id}: get: tags: - Bird summary: Get a number by ID operationId: birdGetNumber parameters: - in: query name: workspaceId required: false schema: type: string format: uuid description: Bird Workspace identifier (optional if configured) - in: path name: id required: true schema: type: string responses: '200': description: Number details content: application/json: schema: $ref: '#/components/schemas/BirdNumberSingleResponse' delete: tags: - Bird summary: Delete/release a number by ID operationId: birdDeleteNumber parameters: - in: query name: workspaceId required: false schema: type: string format: uuid description: Bird Workspace identifier (optional if configured) - in: path name: id required: true schema: type: string responses: '200': description: Number deletion/release accepted content: application/json: schema: type: object additionalProperties: true # Bird Voice Flash Calling /bird/voice/flash-calls: post: tags: - Bird summary: Create/place a flash call via Bird operationId: birdCreateFlashCall parameters: - in: query name: workspaceId schema: type: string format: uuid required: false description: Bird Workspace identifier (falls back to module configuration if omitted) - in: query name: channelId schema: type: string format: uuid required: false description: Bird Channel identifier (falls back to module configuration if omitted) requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/BirdFlashCallCreateRequest' responses: '200': description: Flash call created content: application/json: schema: $ref: '#/components/schemas/BirdFlashCallSingleResponse' get: tags: - Bird summary: List flash calls operationId: birdListFlashCalls parameters: - in: query name: workspaceId schema: type: string format: uuid required: false description: Bird Workspace identifier (falls back to module configuration if omitted) - in: query name: channelId schema: type: string format: uuid required: false description: Bird Channel identifier (falls back to module configuration if omitted) - in: query name: limit schema: type: integer minimum: 1 maximum: 1000 - in: query name: pageToken schema: type: string - in: query name: startAt schema: type: string format: date-time - in: query name: endAt schema: type: string format: date-time - in: query name: status schema: type: string - in: query name: to schema: type: string - in: query name: from schema: type: string - in: query name: duration schema: type: integer - in: query name: id schema: type: string format: uuid responses: '200': description: A list of flash calls content: application/json: schema: $ref: '#/components/schemas/BirdFlashCallListResponse' /bird/voice/flash-calls/{id}: get: tags: - Bird summary: Get a flash call by ID operationId: birdGetFlashCall parameters: - in: query name: workspaceId schema: type: string format: uuid required: false - in: query name: channelId schema: type: string format: uuid required: false - in: path name: id required: true schema: type: string format: uuid responses: '200': description: Flash call details content: application/json: schema: $ref: '#/components/schemas/BirdFlashCallSingleResponse' post: tags: - Bird summary: Complete/end a flash call by ID operationId: birdEndFlashCall parameters: - in: query name: workspaceId schema: type: string format: uuid required: false - in: query name: channelId schema: type: string format: uuid required: false - in: path name: id required: true schema: type: string format: uuid requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/BirdFlashCallEndRequest' responses: '200': description: Flash call completed content: application/json: schema: $ref: '#/components/schemas/BirdFlashCallSingleResponse' /bird/voice/flash-calls/hangup: post: tags: - Bird summary: Hang up flash calls using payload criteria operationId: birdHangupFlashCall parameters: - in: query name: workspaceId schema: type: string format: uuid required: false - in: query name: channelId schema: type: string format: uuid required: false requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/BirdFlashCallHangupRequest' responses: '200': description: Flash call hangup accepted content: application/json: schema: $ref: '#/components/schemas/BirdFlashCallHangupResponse' /bird/voice/flash-calls/end: post: tags: - Bird summary: Compatibility alias for flash hangup endpoint description: Deprecated alias for `/bird/voice/flash-calls/hangup`. deprecated: true operationId: birdEndFlashCallByNumbers parameters: - in: query name: workspaceId schema: type: string format: uuid required: false - in: query name: channelId schema: type: string format: uuid required: false requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/BirdFlashCallHangupRequest' responses: '200': description: Flash call hangup accepted (alias) content: application/json: schema: $ref: '#/components/schemas/BirdFlashCallHangupResponse' # Subusers (public registration + setup) /superuser/users/{user_id}/subusers: get: tags: - Subusers summary: List subusers for a superuser customer account description: Returns paginated chauffeur accounts for the customer number resolved from the selected user. Each chauffeur appears once; visible customer access grants are returned in `grants`. operationId: listSuperuserUserSubusers security: - BearerAuth: [] parameters: - name: user_id in: path required: true schema: type: integer minimum: 1 - name: page in: query required: false schema: { type: integer, minimum: 1 } - name: limit in: query required: false schema: { type: integer, minimum: 1, maximum: 1000 } - name: search in: query required: false schema: { type: string } - name: include_non_enabled in: query required: false schema: { type: boolean } responses: '200': description: User-scoped subuser list content: application/json: schema: type: array items: $ref: '#/components/schemas/SubuserManagementRow' '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '500': { $ref: '#/components/responses/InternalServerError' } /superuser/users/{user_id}/subusers/summary: get: tags: - Subusers summary: Summarize subusers for a superuser customer account operationId: summarizeSuperuserUserSubusers security: - BearerAuth: [] parameters: - name: user_id in: path required: true schema: type: integer minimum: 1 responses: '200': description: User-scoped subuser summary content: application/json: schema: $ref: '#/components/schemas/SubuserManagementSummary' '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '500': { $ref: '#/components/responses/InternalServerError' } /superuser/users/{user_id}/subusers/invite: post: tags: - Subusers summary: Invite a subuser for a superuser customer account operationId: inviteSuperuserUserSubuser security: - BearerAuth: [] parameters: - name: user_id in: path required: true schema: type: integer minimum: 1 requestBody: required: true content: application/json: schema: type: object required: [name, phone_country_code, phone] properties: name: { type: string, minLength: 3, maxLength: 255 } phone_country_code: { type: integer } phone: { type: integer } note: { type: string, nullable: true, maxLength: 65535 } enabled: { type: boolean, default: true } permission_template_key: type: string enum: [deactivated, driver, booking_coordinator, fleet_admin] permissions: type: array items: { type: string } responses: '200': description: Driver invited or linked '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '500': { $ref: '#/components/responses/InternalServerError' } /superuser/users/{user_id}/subusers/{subuser_id}/invite/resend: post: tags: - Subusers summary: Resend a user-scoped subuser invite operationId: resendSuperuserUserSubuserInvite security: - BearerAuth: [] parameters: - name: user_id in: path required: true schema: { type: integer, minimum: 1 } - name: subuser_id in: path required: true schema: { type: integer, minimum: 1 } responses: '200': description: Invite resent '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '409': { $ref: '#/components/responses/Conflict' } '500': { $ref: '#/components/responses/InternalServerError' } /superuser/users/{user_id}/subusers/grants/{grant_id}: patch: tags: - Subusers summary: Update a user-scoped subuser grant operationId: updateSuperuserUserSubuserGrant security: - BearerAuth: [] parameters: - name: user_id in: path required: true schema: { type: integer, minimum: 1 } - name: grant_id in: path required: true schema: { type: integer, minimum: 1 } requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/SubuserGrantUpdateRequest' responses: '200': description: Grant updated '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '500': { $ref: '#/components/responses/InternalServerError' } /subusers: get: tags: - Subusers summary: List subusers visible to the authenticated user description: | Returns a paginated list of subusers (drivers) that have enabled grants tied to the authenticated user's customer number. Only subusers with at least one enabled, non-deleted grant for the caller's customer are returned. operationId: listSubusers parameters: - name: page in: query required: false schema: type: integer minimum: 1 - name: limit in: query required: false schema: type: integer minimum: 1 maximum: 1000 - name: search in: query required: false schema: type: string - name: include_non_enabled in: query required: false description: Include subusers that only have non-enabled grants (default false) schema: type: boolean responses: '200': description: List of visible subusers content: application/json: schema: type: array items: type: object properties: id: type: integer username: type: string nullable: true name: type: string nullable: true email: type: string format: email nullable: true phone_country_code: type: integer nullable: true phone: type: integer nullable: true created_at: type: string format: date-time nullable: true updated_at: type: string format: date-time nullable: true suspended_at: type: string format: date-time nullable: true two_factor_enabled: type: boolean description: Indicates if 2FA is enabled for this account permissions: type: array description: Aggregated permission keys granted for the caller's customer items: type: string '401': { $ref: '#/components/responses/Unauthorized' } '500': { $ref: '#/components/responses/InternalServerError' } /subusers/me: get: tags: - Subusers summary: Get current subuser profile description: | Returns the authenticated subuser (driver) profile and their enabled grants grouped by `billing_customer_number`. Notes: - This endpoint is available only to authenticated subuser sessions. - It does not require the `X-Customer-Number` header; all enabled, non-deleted grants for the subuser are included in the response. operationId: getCurrentSubuser responses: '200': description: Current subuser details content: application/json: schema: $ref: '#/components/schemas/SubuserSelf' '401': { $ref: '#/components/responses/Unauthorized' } '500': { $ref: '#/components/responses/InternalServerError' } post: tags: - Subusers summary: Create a subuser registration description: | Starts a driver setup challenge using a company's CVR and a phone number. No company grant is created until the driver proves possession of the phone by completing the SMS setup link. The public response is uniform and never includes setup credentials or relationship state. operationId: createSubuser security: [] requestBody: required: true content: application/json: schema: type: object required: - cvr - phone_country_code - phone - g_recaptcha_response properties: cvr: type: integer description: Danish CVR (8 digits) example: 12345678 phone_country_code: type: integer description: Phone country code (1–3 digits) example: 45 phone: type: integer description: Phone number (4–15 digits, no leading +) example: 12345678 g_recaptcha_response: type: string description: reCAPTCHA response token responses: '200': description: Uniform driver registration acknowledgement content: application/json: schema: type: object properties: message: type: string '400': { $ref: '#/components/responses/BadRequest' } '404': { $ref: '#/components/responses/NotFound' } '500': { $ref: '#/components/responses/InternalServerError' } '503': { $ref: '#/components/responses/ServiceUnavailable' } /subusers/{id}: get: tags: - Subusers summary: Get a subuser by ID (visible by grant) description: | Returns the subuser if the authenticated user has at least one enabled, non-deleted grant for their customer number to this subuser. Otherwise returns 404. operationId: getSubuser parameters: - name: id in: path required: true schema: type: integer responses: '200': description: Subuser details content: application/json: schema: type: object properties: id: type: integer username: type: string nullable: true name: type: string nullable: true email: type: string format: email nullable: true phone_country_code: type: integer nullable: true phone: type: integer nullable: true created_at: type: string format: date-time nullable: true updated_at: type: string format: date-time nullable: true suspended_at: type: string format: date-time nullable: true permissions: type: array description: Aggregated permission keys granted for the caller's customer items: type: string '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } '500': { $ref: '#/components/responses/InternalServerError' } /subusers/setup: get: tags: - Subusers summary: Validate setup token description: Validates a subuser setup token generated during registration. operationId: validateSubuserSetupToken security: [] parameters: - name: token in: query required: true schema: type: string description: One-time setup token received via SMS responses: '200': description: Token is valid content: application/json: schema: type: object properties: message: type: string example: Token is valid subuser_id: type: integer example: 42 '400': { $ref: '#/components/responses/BadRequest' } '500': { $ref: '#/components/responses/InternalServerError' } post: tags: - Subusers summary: Complete subuser setup description: | Completes subuser setup by setting a password and basic profile fields. Accepts optional `username` and `email`. operationId: completeSubuserSetup security: [] requestBody: required: true content: application/json: schema: type: object required: - token - password - name properties: token: type: string description: One-time setup token password: type: string format: password minLength: 8 description: Must include at least one uppercase letter, one lowercase letter, and one number pattern: '^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).+$' name: type: string minLength: 3 maxLength: 255 username: type: string minLength: 3 maxLength: 255 email: type: string format: email minLength: 3 maxLength: 255 responses: '200': description: Setup completed content: application/json: schema: type: object properties: message: type: string example: Password set successfully '400': { $ref: '#/components/responses/BadRequest' } '500': { $ref: '#/components/responses/InternalServerError' } /subusers/auth/password: post: tags: - Subusers summary: Authenticate subuser with password description: | Authenticates a subuser (driver) using a password together with one of the supported identifiers: `phone_country_code` + `phone`, `subuser_id`, or `username`. On success, returns a newly generated session token for the subuser. operationId: subuserPasswordAuth security: [] requestBody: required: true content: application/json: schema: oneOf: - type: object required: [phone_country_code, phone, password] properties: phone_country_code: type: integer description: Phone country code (1–3 digits) minimum: 1 maximum: 999 example: 45 phone: type: integer description: Phone number (4–15 digits, no leading +) minimum: 1000 maximum: 999999999999999 example: 12345678 password: type: string format: password minLength: 8 maxLength: 255 - type: object required: [subuser_id, password] properties: subuser_id: type: integer description: Subuser ID example: 42 password: type: string format: password minLength: 8 maxLength: 255 - type: object required: [username, password] properties: username: type: string minLength: 3 maxLength: 255 example: jdoe password: type: string format: password minLength: 8 maxLength: 255 examples: withPhone: summary: Authenticate with phone value: phone_country_code: 45 phone: 12345678 password: MySecureP@ssw0rd withSubuserId: summary: Authenticate with subuser_id value: subuser_id: 42 password: MySecureP@ssw0rd withUsername: summary: Authenticate with username value: username: jdoe password: MySecureP@ssw0rd responses: '200': description: Authentication successful content: application/json: schema: oneOf: - type: object required: [session] properties: session: type: string description: Newly generated subuser session token example: "2f7a8c0e-9b1d-4c6a-91a9-1a2b3c4d5e6f" - type: object required: [2fa_required, 2fa_token] properties: 2fa_required: type: boolean example: true 2fa_token: type: string description: Temporary 2FA verification token example: "557a3e7b1a2b..." '400': { $ref: '#/components/responses/BadRequest' } '404': { $ref: '#/components/responses/NotFound' } '500': { $ref: '#/components/responses/InternalServerError' } /subusers/grants: get: tags: - Subusers summary: List subuser grants description: Returns subuser grant records filtered by `customer_number` and/or `subuser_id`. operationId: listSubuserGrants security: - BearerAuth: [] parameters: - name: customer_number in: query required: false schema: type: integer description: e-conomic customer number to filter by - name: subuser_id in: query required: false schema: type: integer description: Subuser ID to filter by responses: '200': description: Grants fetched content: application/json: schema: type: object properties: grants: type: array items: $ref: '#/components/schemas/SubuserGrant' '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '500': { $ref: '#/components/responses/InternalServerError' } post: tags: - Subusers summary: Create subuser grant operationId: createSubuserGrant security: - BearerAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/SubuserGrantCreateRequest' examples: default: value: customer_number: 1000 subuser_id: 42 enabled: true note: "Grant for bookings access" permissions: ["BOOKINGS_LIST", "BOOKINGS_ADD", "BOOKINGS_EDIT"] responses: '200': description: Grant created content: application/json: schema: type: object properties: grant: $ref: '#/components/schemas/SubuserGrant' '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '500': { $ref: '#/components/responses/InternalServerError' } /subusers/grants/{id}: patch: tags: - Subusers summary: Update subuser grant operationId: updateSubuserGrant security: - BearerAuth: [] parameters: - in: path name: id required: true schema: type: integer requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/SubuserGrantUpdateRequest' examples: enableOnly: value: enabled: true updatePermissions: value: permissions: ["VEHICLES_LIST", "SELFSERVE_ADD"] responses: '200': description: Grant updated content: application/json: schema: type: object properties: grant: $ref: '#/components/schemas/SubuserGrant' '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } '500': { $ref: '#/components/responses/InternalServerError' } delete: tags: - Subusers summary: Delete subuser grant operationId: deleteSubuserGrant security: - BearerAuth: [] parameters: - in: path name: id required: true schema: type: integer responses: '200': description: Grant deleted content: application/json: schema: type: object properties: message: type: string example: Grant deleted '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/NotFound' } '500': { $ref: '#/components/responses/InternalServerError' } /subusers/permission-nodes: get: tags: - Subusers summary: List available subuser permission nodes description: Returns grouped permission nodes available for subuser grants. operationId: listSubuserPermissionNodes security: - BearerAuth: [] responses: '200': description: Permission nodes fetched content: application/json: schema: type: object properties: permission_nodes: type: array items: $ref: '#/components/schemas/PermissionNodeGroup' '401': { $ref: '#/components/responses/Unauthorized' } '500': { $ref: '#/components/responses/InternalServerError' } /subusers/permission-templates: get: tags: - Subusers summary: List simplified subuser permission templates description: Returns backend-owned driver access profiles and grouped capability metadata for subuser grants. operationId: listSubuserPermissionTemplates security: - BearerAuth: [] responses: '200': description: Permission templates fetched content: application/json: schema: type: object properties: templates: type: array items: $ref: '#/components/schemas/SubuserPermissionTemplate' groups: type: array items: $ref: '#/components/schemas/SubuserPermissionGroup' '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '500': { $ref: '#/components/responses/InternalServerError' } # Authentication Endpoints /auth/limited-backoffice-login-grants/exchange: post: tags: - Authentication summary: Exchange a one-time limited-backoffice employee login grant description: Exchanges an unexpired, unrevoked grant exactly once for a regular employee bearer session. The grant is invalidated atomically before the session is returned. operationId: exchangeLimitedBackofficeEmployeeLoginGrant security: [] requestBody: required: true content: application/json: schema: type: object required: [grant] properties: grant: type: string pattern: '^lbg_[a-f0-9]{64}$' writeOnly: true responses: '200': description: Grant exchanged content: application/json: schema: type: object required: [employee_id, token] properties: employee_id: {type: integer} token: type: string description: Sensitive bearer token returned once by a successful grant exchange. '401': $ref: '#/components/responses/Unauthorized' '500': $ref: '#/components/responses/InternalServerError' /auth/login: post: tags: - Authentication summary: Customer login description: Authenticate a customer using customer number and password operationId: customerLogin security: [] requestBody: required: true content: application/json: schema: type: object required: - customer_number - password - g_recaptcha_response properties: customer_number: type: integer description: Customer's e-conomic customer number example: 12345 password: type: string format: password description: Customer password minLength: 1 g_recaptcha_response: type: string description: reCAPTCHA verification token responses: '200': description: Login successful content: application/json: schema: oneOf: - type: object required: [token] properties: token: type: string description: Bearer authentication token example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." - type: object required: [2fa_required, 2fa_token] properties: 2fa_required: type: boolean example: true 2fa_token: type: string description: Temporary 2FA verification token example: "557a3e7b1a2b..." '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' /auth/employee/login: post: tags: - Authentication summary: Employee login description: Authenticate an employee using user ID and password operationId: employeeLogin security: [] requestBody: required: true content: application/json: schema: type: object required: - user_id - password - g_recaptcha_response properties: user_id: type: integer description: Employee user ID example: 1 password: type: string format: password description: Employee password g_recaptcha_response: type: string description: reCAPTCHA verification token responses: '200': description: Login successful content: application/json: schema: oneOf: - type: object required: [token] properties: token: type: string description: Bearer authentication token - type: object required: [2fa_required, 2fa_token] properties: 2fa_required: type: boolean example: true 2fa_token: type: string description: Temporary 2FA verification token '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' /auth/passkey/challenge: post: tags: - Authentication summary: Initiate passkey authentication challenge description: Generates a WebAuthn PublicKeyCredentialRequestOptions payload. If customer_number is provided, allowCredentials will be populated with existing passkeys for that account. Otherwise, a challenge is issued for discoverable credentials. operationId: passkeyChallenge security: [] requestBody: required: true content: application/json: schema: type: object required: - g_recaptcha_response properties: customer_number: type: integer description: Optional customer's e-conomic customer number example: 12345 g_recaptcha_response: type: string description: reCAPTCHA verification token responses: '200': description: Challenge generated content: application/json: schema: type: object properties: challenge_token: type: string description: Temporary token binding the challenge to the login attempt publicKey: type: object properties: challenge: type: string description: Base64URL-encoded challenge rpId: type: string description: Relying party ID (truckwash.io or localhost) example: truckwash.io timeout: type: integer description: Timeout in milliseconds userVerification: type: string enum: [required, preferred, discouraged] allowCredentials: type: array items: type: object properties: type: type: string example: public-key id: type: string description: Base64URL-encoded credential ID transports: type: array items: type: string '400': $ref: '#/components/responses/BadRequest' /auth/passkey/verify: post: tags: - Authentication summary: Verify passkey authentication and start session description: Verifies the WebAuthn assertion and challenge token. Returns a session token on success. operationId: passkeyVerify security: [] requestBody: required: true content: application/json: schema: type: object required: - challenge_token - credential - g_recaptcha_response properties: challenge_token: type: string description: The token returned by the challenge endpoint g_recaptcha_response: type: string description: reCAPTCHA verification token credential: type: object description: The WebAuthn PublicKeyCredential object (assertion) required: - id - rawId - type - response properties: id: type: string description: The credential ID (base64url) rawId: type: string description: The raw credential ID (base64url) type: type: string example: public-key clientExtensionResults: type: object response: type: object required: - clientDataJSON - authenticatorData - signature properties: clientDataJSON: type: string description: Base64URL-encoded client data authenticatorData: type: string description: Base64URL-encoded authenticator data signature: type: string description: Base64URL-encoded signature userHandle: type: string nullable: true description: Base64URL-encoded user handle responses: '200': description: Verification successful, session started content: application/json: schema: oneOf: - type: object required: [token] properties: token: type: string description: Bearer token for customer - type: object required: [session] properties: session: type: string description: Session token for subuser '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' /auth/logout: get: tags: - Authentication summary: Logout description: Invalidate the current authentication token operationId: logout responses: '200': description: Logout successful content: application/json: schema: type: object properties: message: type: string example: "Logged out" '401': $ref: '#/components/responses/Unauthorized' /auth/session: get: tags: - Authentication summary: Get current session description: Retrieve information about the current authenticated user session operationId: getSession responses: '200': description: Session information retrieved successfully content: application/json: schema: allOf: - $ref: '#/components/schemas/User' - type: object properties: two_factor_enabled: type: boolean description: Indicates if 2FA is enabled for this account runtime_config: type: object properties: economic: type: object properties: transaction_draft_customer_number: type: integer nullable: true default_distribution_department_id: type: integer additionalProperties: false additionalProperties: true '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' /account/deletion: get: tags: - Security summary: Describe account deletion requirements description: Returns the authenticated customer or chauffeur deletion state, required confirmation phrase, and categories retained for legal obligations. operationId: getAccountDeletion responses: '200': description: Account deletion requirements retrieved successfully content: application/json: schema: type: object required: - principal_type - status - confirmation_phrase - password_required - two_factor_required - access_effect - retained_data_categories - privacy_policy_version properties: principal_type: type: string enum: [customer, subuser] status: type: string enum: [available, requested, processing, failed, manual_review, completed] confirmation_phrase: type: string enum: [SLET MIN KONTO] password_required: type: boolean description: False for authenticated passkey-only accounts that have no password. two_factor_required: type: boolean access_effect: type: string retained_data_categories: type: array items: type: string enum: [invoices_payments_accounting, orders_wash_history, security_audit_logs, legal_obligations, customer_reference, driver_reference] privacy_policy_version: type: string request_id: type: string format: uuid nullable: true requested_at: type: string format: date-time nullable: true '401': $ref: '#/components/responses/Unauthorized' '500': $ref: '#/components/responses/InternalServerError' post: tags: - Security summary: Request deletion of the authenticated account description: Reauthenticates the principal, records an auditable deletion request, and revokes access immediately. A background worker subsequently anonymizes personal account fields while preserving legally required history. operationId: requestAccountDeletion requestBody: required: true content: application/json: schema: type: object required: - confirmation - acknowledge_legal_retention properties: password: type: string format: password description: Required when password_required is true; omit for passkey-only accounts. passkey_challenge_token: type: string description: Required for passwordless accounts; issued only by the deletion-specific challenge endpoint. passkey_credential: type: object description: Fresh WebAuthn assertion bound to passkey_challenge_token and the authenticated principal. two_factor_code: type: string description: Required when two-factor authentication is enabled. confirmation: type: string enum: [SLET MIN KONTO] acknowledge_legal_retention: type: boolean enum: [true] responses: '202': description: Deletion request accepted and account access revoked content: application/json: schema: type: object required: - request_id - status - requested_at - access_revoked - retained_data_categories properties: request_id: type: string format: uuid status: type: string enum: [requested, processing, failed, manual_review, completed] requested_at: type: string format: date-time access_revoked: type: boolean enum: [true] retained_data_categories: type: array items: type: string enum: [invoices_payments_accounting, orders_wash_history, security_audit_logs, legal_obligations, customer_reference, driver_reference] '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '429': description: Too many deletion confirmation attempts '500': $ref: '#/components/responses/InternalServerError' /account/deletion/passkey/challenge: post: tags: [Security] summary: Create a deletion-specific WebAuthn challenge description: Creates a short-lived, single-use challenge bound to the authenticated passwordless principal. A normal sign-in assertion cannot authorize deletion. operationId: createAccountDeletionPasskeyChallenge responses: '200': description: Deletion-specific challenge created '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '500': $ref: '#/components/responses/InternalServerError' /auth/2fa/setup: post: tags: - Authentication summary: Generate 2FA secret description: Generate a new TOTP secret for the authenticated user/subuser operationId: setup2fa requestBody: required: false content: application/json: schema: {} responses: '200': description: 2FA secret generated successfully content: application/json: schema: type: object properties: secret: type: string description: The base32 encoded TOTP secret qr_code_url: type: string description: An otpauth URL for generating a QR code '401': $ref: '#/components/responses/Unauthorized' /auth/2fa/enable: post: tags: - Authentication summary: Enable 2FA description: Verify a code and enable 2FA for the authenticated user/subuser operationId: enable2fa requestBody: required: true content: application/json: schema: type: object required: [code] properties: code: type: string description: The 6-digit TOTP code responses: '200': description: 2FA enabled successfully content: application/json: schema: {} '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' /auth/2fa/disable: post: tags: - Authentication summary: Disable 2FA description: Verify a code and disable 2FA for the authenticated user/subuser operationId: disable2fa requestBody: required: true content: application/json: schema: type: object required: [code] properties: code: type: string description: The 6-digit TOTP code responses: '200': description: 2FA disabled successfully content: application/json: schema: {} '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' /auth/2fa/verify: post: tags: - Authentication summary: Verify 2FA code during login description: Complete the login process by verifying the 2FA code operationId: verify2fa requestBody: required: true content: application/json: schema: type: object required: [2fa_token, code] properties: 2fa_token: type: string description: The temporary 2FA verification token code: type: string description: The 6-digit TOTP code responses: '200': description: Login successful content: application/json: schema: oneOf: - type: object required: [token] properties: token: type: string description: Bearer authentication token (for users/employees) - type: object required: [session] properties: session: type: string description: Session token (for subusers) '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' /auth/reCAPTCHA/public: get: tags: - Authentication summary: Get reCAPTCHA configuration description: Retrieve public reCAPTCHA configuration for login forms operationId: getRecaptchaConfig security: [] responses: '200': description: reCAPTCHA configuration retrieved successfully content: application/json: schema: type: object properties: rate_limit: type: object properties: enabled: type: boolean limit: type: integer remaining: type: integer reset: type: integer warning: type: string nullable: true recaptcha: type: object /auth/register/cvr: post: tags: - Authentication summary: Register new customer by CVR description: Register a new customer account using Danish CVR number operationId: registerCustomerByCvr security: [] requestBody: required: true content: application/json: schema: type: object required: - cvr - companyPhone - invoiceEmail - contactEmail - contactPhone - contactName - g_recaptcha_response properties: cvr: type: string description: Danish CVR number minLength: 8 maxLength: 20 example: "44794780" companyPhone: type: integer description: Company phone number minimum: 10000000 maximum: 9999999999 example: 21754690 invoiceEmail: type: string format: email description: Email for invoices minLength: 5 maxLength: 255 example: "invoice@company.dk" contactEmail: type: string format: email description: Contact email minLength: 5 maxLength: 255 example: "contact@company.dk" contactPhone: type: integer description: Contact phone number minimum: 10000000 maximum: 9999999999 example: 21754690 contactName: type: string description: Contact person name example: "Mikkel" g_recaptcha_response: type: string description: reCAPTCHA verification token responses: '200': description: Matching e-conomic customer already existed and local registration was completed content: application/json: schema: {} '201': description: Customer registered successfully content: application/json: schema: {} '400': $ref: '#/components/responses/BadRequest' '409': description: Customer number conflict between the submitted phone number and the existing or created e-conomic customer content: application/json: schema: {} /auth/password-reset/request: post: tags: - Authentication summary: Request a customer password reset email description: Send an email with a password reset token to the customer's email address operationId: requestPasswordReset security: [] requestBody: required: true content: application/json: schema: type: object required: - customer_number - g_recaptcha_response properties: customer_number: type: integer description: The customer number example: 123456 g_recaptcha_response: type: string description: reCAPTCHA verification token responses: '200': description: Request processed content: application/json: schema: type: object properties: message: type: string '400': $ref: '#/components/responses/BadRequest' /auth/password-reset/validate: get: tags: - Authentication summary: Validate a customer password reset key description: Check if a password reset token is valid and hasn't expired operationId: validatePasswordResetToken security: [] parameters: - name: token in: query required: true schema: type: string description: The password reset token responses: '200': description: Token is valid content: application/json: schema: type: object properties: valid: type: boolean customer_id: type: integer '404': description: Invalid or expired token content: application/json: schema: $ref: '#/components/schemas/Error' /auth/password-reset/set: post: tags: - Authentication summary: Set a customer password using a reset key description: Update the customer password using a valid reset token operationId: setPasswordUsingResetToken security: [] requestBody: required: true content: application/json: schema: type: object required: - token - password - g_recaptcha_response properties: token: type: string description: The password reset token password: type: string description: The new password g_recaptcha_response: type: string description: reCAPTCHA verification token responses: '200': description: Password updated successfully content: application/json: schema: type: object properties: message: type: string '400': $ref: '#/components/responses/BadRequest' '404': description: Invalid or expired token content: application/json: schema: $ref: '#/components/schemas/Error' /su/intimidate: post: tags: - Authentication summary: Intimidate a user description: Create an authentication token for another user (Superuser only) operationId: suIntimidate requestBody: required: true content: application/json: schema: type: object required: [user_id] properties: user_id: {type: integer} responses: '200': description: Success content: application/json: schema: type: object properties: token: {type: string} /limited-backoffice/employees/{employeeId}/login-grants: post: tags: - Limited Backoffice summary: Create or preflight a one-time employee login grant description: Requires limited-backoffice employee-management permissions and access to every department assigned to the employee. The bearer is deterministically derived under the server encryption key so an identical idempotent retry can recover the same unconsumed grant after a lost response; only its digest is stored. operationId: createLimitedBackofficeEmployeeLoginGrant parameters: - name: employeeId in: path required: true schema: {type: integer, minimum: 1} requestBody: required: true content: application/json: schema: type: object properties: purpose: type: string enum: [limited_backoffice_employee_login] default: limited_backoffice_employee_login ttl_seconds: type: integer minimum: 60 maximum: 900 default: 300 idempotency_key: type: string minLength: 16 maxLength: 128 writeOnly: true preflight: type: boolean default: false oneOf: - required: [idempotency_key] properties: preflight: type: boolean enum: [false] - required: [preflight] properties: preflight: type: boolean enum: [true] responses: '200': description: Grant created, safely replayed for the same idempotency key, or request validated in preflight mode content: application/json: schema: type: object required: [employee_id, purpose, ttl_seconds, expires_at, one_time, preflight] properties: employee_id: {type: integer} purpose: {type: string} ttl_seconds: {type: integer} expires_at: {type: string, format: date-time} one_time: {type: boolean} preflight: {type: boolean} grant_id: {type: string, pattern: '^[a-f0-9]{32}$'} login_path: type: string description: Sensitive fragment URL returned only for a newly created or safely replayed grant. exchange_path: {type: string} '400': $ref: '#/components/responses/BadRequest' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' delete: tags: - Limited Backoffice summary: Revoke active one-time employee login grants operationId: revokeLimitedBackofficeEmployeeLoginGrants parameters: - name: employeeId in: path required: true schema: {type: integer, minimum: 1} responses: '200': description: Active grants revoked content: application/json: schema: type: object required: [employee_id, revoked_count] properties: employee_id: {type: integer} revoked_count: {type: integer, minimum: 0} '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' /limited-backoffice/employees/{employeeId}/login-link: post: tags: - Limited Backoffice summary: Create a managed employee QR login link description: Create a reusable auth-token login link for an active employee managed through the limited backoffice. operationId: createLimitedBackofficeEmployeeLoginLink parameters: - name: employeeId in: path required: true schema: type: integer minimum: 1 responses: '200': description: Login link created successfully content: application/json: schema: type: object properties: employee_id: type: integer login_path: type: string example: /login/qr?token=abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890 '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' # User Endpoints /users: get: tags: - Users summary: List users description: Retrieve a paginated list of users operationId: listUsers parameters: - $ref: '#/components/parameters/PageParam' - $ref: '#/components/parameters/PerPageParam' - $ref: '#/components/parameters/SearchParam' responses: '200': description: Users retrieved successfully content: application/json: schema: type: array items: $ref: '#/components/schemas/User' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' post: tags: - Users summary: Create new user description: Create a new user account operationId: createUser requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UserCreate' responses: '201': description: User created successfully content: application/json: schema: $ref: '#/components/schemas/User' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' put: tags: - Users summary: Update user description: Update an existing user operationId: updateUser requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UserUpdate' responses: '200': description: User updated successfully content: application/json: schema: {} '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' /users/customer: get: tags: - Users summary: Get customer details description: Get details about a specific customer operationId: getCustomer parameters: - name: customer_number in: query schema: type: integer responses: '200': description: Customer retrieved successfully content: application/json: schema: $ref: '#/components/schemas/User' '404': $ref: '#/components/responses/NotFound' /superuser/user: get: tags: - Users summary: Get user by ID (superuser) description: Get detailed user information by user ID operationId: getSuperuserUser parameters: - name: user_id in: query schema: type: integer responses: '200': description: User retrieved successfully content: application/json: schema: $ref: '#/components/schemas/User' '404': $ref: '#/components/responses/NotFound' /admin/customer/code: get: tags: - Users summary: Get customer code operationId: getCustomerCode parameters: - name: customer_number in: query schema: {type: integer} - name: user_id in: query schema: {type: integer} responses: '200': description: Success content: application/json: schema: {} post: tags: - Users summary: Add customer code operationId: addCustomerCode requestBody: required: true content: application/json: schema: type: object properties: customer_number: {type: integer} user_id: {type: integer} code: {type: string} responses: '200': description: Success content: application/json: schema: {} /customer/department/default: get: tags: - Users summary: Get customer default department operationId: getCustomerDefaultDepartment parameters: - name: customer_number in: query schema: {type: integer} responses: '200': description: Success content: application/json: schema: {} post: tags: - Users summary: Add customer default department operationId: addCustomerDefaultDepartment requestBody: required: true content: application/json: schema: type: object required: [department] properties: customer_number: {type: integer} department: {type: integer} responses: '200': description: Success content: application/json: schema: {} delete: tags: - Users summary: Delete customer default department operationId: deleteCustomerDefaultDepartment parameters: - name: customer_number in: query schema: {type: integer} responses: '200': description: Success content: application/json: schema: {} /customer/pricing/fixed: get: tags: - Users summary: Get customer fixed pricing operationId: getCustomerFixedPricing parameters: - name: customer_number in: query required: true schema: {type: integer} responses: '200': description: Success content: application/json: schema: {} post: tags: - Users summary: Add customer fixed pricing operationId: addCustomerFixedPricing requestBody: required: true content: application/json: schema: type: object required: [customer_number, price, description] properties: customer_number: {type: integer} price: {type: integer} description: {type: string} responses: '200': description: Success content: application/json: schema: {} delete: tags: - Users summary: Delete customer fixed pricing operationId: deleteCustomerFixedPricing parameters: - name: customer_number in: query required: true schema: {type: integer} responses: '200': description: Success content: application/json: schema: {} /account/notifications: put: tags: - Users summary: Update user notification settings operationId: updateUserNotifications requestBody: required: true content: application/json: schema: type: object properties: wash_certificate_email: {type: string} sms_notifications_enabled: {type: boolean} email_notifications_enabled: {type: boolean} superuser_new_customer_email_notifications_enabled: {type: boolean} responses: '200': description: Success content: application/json: schema: {} /user/permissions: get: tags: - Users summary: Get user permissions operationId: getUserPermissions responses: '200': description: Success content: application/json: schema: {} /customers: get: tags: - Users summary: List customers operationId: listCustomers parameters: - $ref: '#/components/parameters/PageParam' - $ref: '#/components/parameters/PerPageParam' - $ref: '#/components/parameters/SearchParam' - name: barred in: query required: false description: Optional e-conomic barred customer filter. schema: type: string enum: ['true', 'false', 'barred', 'active', '1', '0'] responses: '200': description: Success content: application/json: schema: {} /superuser/user/discounts: get: tags: - Users summary: Get user discounts operationId: getUserDiscounts parameters: - name: user_id in: query required: true schema: {type: integer} responses: '200': description: Success content: application/json: schema: {} post: tags: - Users summary: Set user discount operationId: setUserDiscount requestBody: required: true content: application/json: schema: type: object required: [discount, object_id, is_category] properties: user_id: {type: integer} discount: {type: integer} object_id: {type: string} is_category: {type: boolean} responses: '200': description: Success content: application/json: schema: {} /superuser/user/keys: get: tags: - Users summary: Get user keys operationId: getUserKeys parameters: - name: user_id in: query required: true schema: {type: integer} - name: key in: query schema: {type: string} responses: '200': description: Success content: application/json: schema: {} post: tags: - Users summary: Set user key operationId: setUserKey requestBody: required: true content: application/json: schema: type: object required: [key, value] properties: user_id: {type: integer} key: {type: string} value: {type: string} responses: '200': description: Success content: application/json: schema: {} /superuser/user/password: post: tags: - Users summary: Set user password operationId: setUserPassword requestBody: required: true content: application/json: schema: type: object required: [password] properties: user_id: {type: integer} password: {type: string} responses: '200': description: Success content: application/json: schema: {} /admin/customer/getUserId: get: tags: - Users summary: Get user ID from customer number description: Convert e-conomic customer number to internal user ID operationId: getUserIdFromCustomerNumber parameters: - name: customer_number in: query required: true schema: type: integer responses: '200': description: User ID retrieved successfully content: application/json: schema: type: object properties: user_id: type: integer /admin/customer/name: get: tags: - Users summary: Get customer name description: Get the full name of a customer operationId: getCustomerName parameters: - name: user_id in: query schema: type: integer responses: '200': description: Customer name retrieved successfully content: application/json: schema: type: object properties: name: type: string # Orders Endpoints /orders: get: tags: - Orders summary: List orders description: Retrieve a paginated list of orders operationId: listOrders parameters: - $ref: '#/components/parameters/PageParam' - $ref: '#/components/parameters/PerPageParam' - $ref: '#/components/parameters/SearchParam' - name: show_wash_subscription in: query schema: type: string enum: [true, false] responses: '200': description: Orders retrieved successfully content: application/json: schema: type: array items: $ref: '#/components/schemas/Order' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' post: tags: - Orders summary: Create new order description: Create a new wash order operationId: createOrder requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/OrderCreate' responses: '201': description: Order created successfully content: application/json: schema: $ref: '#/components/schemas/Order' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' delete: tags: - Orders summary: Delete order description: Delete an existing order operationId: deleteOrder parameters: - name: id in: query required: true schema: type: integer - name: confirmed in: query required: false description: Must be true to delete an order that is completed or has active items or attachments. schema: type: boolean responses: '200': description: Order deleted successfully content: application/json: schema: {} '409': description: Order deletion requires explicit confirmation content: application/json: schema: type: object properties: success: type: boolean example: false data: type: object properties: message: type: string example: Order deletion requires confirmation requires_confirmation: type: boolean example: true protected_reasons: type: array items: type: string enum: [completed, order_items, attachments] order_item_count: type: integer example: 2 attachment_count: type: integer example: 1 completed_at: type: string nullable: true meta: type: object includes: type: object '401': $ref: '#/components/responses/Unauthorized' '404': $ref: '#/components/responses/NotFound' put: tags: - Orders summary: Update order (alias) description: Update an existing order operationId: updateOrders requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/OrderUpdate' responses: '200': description: Order updated successfully content: application/json: schema: {} '400': $ref: '#/components/responses/BadRequest' /order: get: tags: - Orders summary: Get order details description: Get detailed information about a specific order operationId: getOrder parameters: - name: id in: query required: true schema: type: integer responses: '200': description: Order retrieved successfully content: application/json: schema: $ref: '#/components/schemas/Order' '404': $ref: '#/components/responses/NotFound' put: tags: - Orders summary: Update order description: Update an existing order operationId: updateOrder requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/OrderUpdate' responses: '200': description: Order updated successfully content: application/json: schema: {} '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' /user/orders: get: tags: - Orders summary: Get current user's orders description: Retrieve orders for the authenticated user operationId: getUserOrders parameters: - $ref: '#/components/parameters/PageParam' - $ref: '#/components/parameters/PerPageParam' responses: '200': description: Orders retrieved successfully content: application/json: schema: type: array items: $ref: '#/components/schemas/Order' /user/order: get: tags: - Orders summary: Get user's specific order description: Get details of a specific order for the authenticated user operationId: getUserOrder parameters: - name: id in: query required: true schema: type: integer responses: '200': description: Order retrieved successfully content: application/json: schema: $ref: '#/components/schemas/Order' /orders/mark_as_completed: post: tags: - Orders summary: Mark order as completed description: Mark an order as completed operationId: markOrderCompleted requestBody: required: true content: application/json: schema: type: object required: - id properties: id: type: integer responses: '200': description: Order marked as completed successfully content: application/json: schema: {} '400': $ref: '#/components/responses/BadRequest' /order/wash-certificate: post: tags: - Orders summary: Generate wash certificate description: Generate a wash certificate for an order operationId: generateWashCertificate requestBody: required: true content: application/json: schema: type: object properties: order_id: type: integer responses: '200': description: Wash certificate generated successfully content: application/json: schema: {} '400': $ref: '#/components/responses/BadRequest' # Order Items Endpoints /order/items: get: tags: - Order Items summary: List order items description: Get all items for a specific order operationId: listOrderItems parameters: - name: order_id in: query required: true schema: type: integer responses: '200': description: Order items retrieved successfully content: application/json: schema: type: array items: $ref: '#/components/schemas/OrderItem' post: tags: - Order Items summary: Add item to order description: Add a new item to an existing order operationId: addOrderItem requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/OrderItemCreate' responses: '200': description: Order item added successfully content: application/json: schema: {} '400': description: Invalid order item or product blocked by an active customer rule content: application/json: schema: oneOf: - $ref: '#/components/schemas/CustomerRuleProductRestrictedResponse' - $ref: '#/components/schemas/Error' put: tags: - Order Items summary: Update order item description: Update an existing order item operationId: updateOrderItem requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/OrderItemUpdate' responses: '200': description: Order item updated successfully content: application/json: schema: {} '400': $ref: '#/components/responses/BadRequest' delete: tags: - Order Items summary: Delete order item description: Remove an item from an order operationId: deleteOrderItem parameters: - name: id in: query required: true schema: type: integer responses: '200': description: Order item deleted successfully content: application/json: schema: {} '404': $ref: '#/components/responses/NotFound' # Departments Endpoints /departments: get: tags: - Departments summary: List departments description: Retrieve a list of all visible departments operationId: listDepartments parameters: - name: id in: query schema: type: integer description: Filter by specific department ID - $ref: '#/components/parameters/PageParam' - $ref: '#/components/parameters/PerPageParam' - $ref: '#/components/parameters/SearchParam' responses: '200': description: Departments retrieved successfully content: application/json: schema: type: array items: $ref: '#/components/schemas/Department' '401': $ref: '#/components/responses/Unauthorized' post: tags: - Departments summary: Create department description: Create a new department operationId: createDepartment requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/DepartmentCreate' responses: '201': description: Department created successfully content: application/json: schema: {} '400': $ref: '#/components/responses/BadRequest' put: tags: - Departments summary: Update department description: Update an existing department operationId: updateDepartment requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/DepartmentUpdate' responses: '200': description: Department updated successfully content: application/json: schema: {} '400': $ref: '#/components/responses/BadRequest' /departments/categories: get: tags: - Departments summary: Get department categories description: Get product categories available in a department operationId: getDepartmentCategories parameters: - name: department_id in: query required: true schema: type: integer responses: '200': description: Department categories retrieved successfully content: application/json: schema: {} post: tags: - Departments summary: Add category to department description: Associate a product category with a department operationId: addDepartmentCategory requestBody: required: true content: application/json: schema: type: object properties: department_id: type: integer category_id: type: integer responses: '201': description: Category added to department successfully content: application/json: schema: {} delete: tags: - Departments summary: Remove category from department operationId: removeDepartmentCategory requestBody: required: false content: application/json: schema: {} responses: '200': description: Success content: application/json: schema: {} /departments/self-serve/enabled: get: tags: - Departments summary: Get department self-serve status description: Check if self-serve is enabled for a specific department operationId: getDepartmentSelfServeEnabled parameters: - name: id in: query required: true description: Department ID schema: type: integer responses: '200': description: Successfully retrieved status content: application/json: schema: type: object properties: enabled: type: boolean auto_deactivation: type: object required: [at, timezone, label] properties: at: type: string format: date-time nullable: true timezone: type: string example: Europe/Copenhagen label: type: string '404': $ref: '#/components/responses/NotFound' put: tags: - Departments summary: Update department self-serve status description: Enable or disable self-serve for a specific department operationId: updateDepartmentSelfServeEnabled parameters: - name: id in: query required: true description: Department ID schema: type: integer - name: enabled in: query required: true description: Enabled status (true/false) schema: type: string enum: ['true', 'false'] responses: '200': description: Status updated successfully content: application/json: schema: type: object properties: message: type: string enabled: type: boolean auto_deactivation: type: object required: [at, timezone, label] properties: at: type: string format: date-time nullable: true timezone: type: string example: Europe/Copenhagen label: type: string '404': $ref: '#/components/responses/NotFound' /departments/order/recommended: get: tags: - Departments summary: Get recommended order for department operationId: getDepartmentRecommendedOrder parameters: - name: department_id in: query required: true schema: {type: integer} responses: '200': description: Success content: application/json: schema: {} /department/lanes: get: tags: - Departments summary: List department lanes description: Retrieve a list of all department lanes operationId: listDepartmentLanes parameters: - $ref: '#/components/parameters/PageParam' - $ref: '#/components/parameters/PerPageParam' - $ref: '#/components/parameters/SearchParam' responses: '200': description: Department lanes retrieved successfully content: application/json: schema: type: array items: $ref: '#/components/schemas/DepartmentLane' '401': $ref: '#/components/responses/Unauthorized' post: tags: - Departments summary: Create department lane description: Create a new department lane operationId: createDepartmentLane requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/DepartmentLaneCreate' responses: '201': description: Department lane created successfully content: application/json: schema: {} '400': $ref: '#/components/responses/BadRequest' put: tags: - Departments summary: Update department lane description: Update an existing department lane operationId: updateDepartmentLane requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/DepartmentLaneUpdate' responses: '200': description: Department lane updated successfully content: application/json: schema: {} '400': $ref: '#/components/responses/BadRequest' '404': $ref: '#/components/responses/NotFound' /department/gates: get: tags: - Departments summary: List department gates description: Retrieve department gates, optionally filtered by id operationId: listDepartmentGates parameters: - name: id in: query required: false schema: type: integer minimum: 1 - $ref: '#/components/parameters/PageParam' - $ref: '#/components/parameters/PerPageParam' - $ref: '#/components/parameters/SearchParam' responses: '200': description: Department gates retrieved successfully content: application/json: schema: oneOf: - $ref: '#/components/schemas/DepartmentGate' - type: array items: $ref: '#/components/schemas/DepartmentGate' '401': $ref: '#/components/responses/Unauthorized' post: tags: - Departments summary: Create department gate operationId: createDepartmentGate requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/DepartmentGateCreate' responses: '201': description: Department gate created successfully content: application/json: schema: $ref: '#/components/schemas/DepartmentGate' put: tags: - Departments summary: Update department gate operationId: updateDepartmentGate requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/DepartmentGateUpdate' responses: '200': description: Department gate updated successfully content: application/json: schema: $ref: '#/components/schemas/DepartmentGate' '404': $ref: '#/components/responses/NotFound' delete: tags: - Departments summary: Delete department gate operationId: deleteDepartmentGate parameters: - name: id in: query required: true schema: type: integer minimum: 1 responses: '200': description: Department gate deleted content: application/json: schema: {} /department/relays: get: tags: - Departments summary: List department relays description: Retrieve department relays, optionally filtered by id operationId: listDepartmentRelays parameters: - name: id in: query required: false schema: type: integer minimum: 1 - $ref: '#/components/parameters/PageParam' - $ref: '#/components/parameters/PerPageParam' - $ref: '#/components/parameters/SearchParam' responses: '200': description: Department relays retrieved successfully content: application/json: schema: oneOf: - $ref: '#/components/schemas/DepartmentRelay' - type: array items: $ref: '#/components/schemas/DepartmentRelay' '401': $ref: '#/components/responses/Unauthorized' post: tags: - Departments summary: Create department relay operationId: createDepartmentRelay requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/DepartmentRelayCreate' responses: '201': description: Department relay created successfully content: application/json: schema: $ref: '#/components/schemas/DepartmentRelay' put: tags: - Departments summary: Update department relay operationId: updateDepartmentRelay requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/DepartmentRelayUpdate' responses: '200': description: Department relay updated successfully content: application/json: schema: $ref: '#/components/schemas/DepartmentRelay' '404': $ref: '#/components/responses/NotFound' delete: tags: - Departments summary: Delete department relay operationId: deleteDepartmentRelay parameters: - name: id in: query required: true schema: type: integer minimum: 1 responses: '200': description: Department relay deleted content: application/json: schema: {} /department/lanes/dynamic-image: get: tags: - Departments summary: Generate dynamic image for a department lane description: | Returns a composed machine UI image for the specified department lane. You can optionally highlight button indices, set the current step indicator, and toggle only-current-step mode. operationId: getDepartmentLaneDynamicImage parameters: - name: department in: query required: true description: Department ID schema: type: integer minimum: 1 - name: lane in: query required: true description: Lane ID schema: type: integer minimum: 1 - name: buttons in: query required: false description: Highlighted step tokens in order. Accepts 0-indexed button IDs, "reset", "start", and "program_picker" as CSV, JSON array, or repeated query params. schema: oneOf: - type: string - type: array items: oneOf: - type: integer - type: string - name: current_step in: query required: false description: Current step indicator (non-negative integer) schema: type: integer minimum: 0 - name: only_current_step in: query required: false description: If true, only draw the current step highlight schema: type: boolean - name: vehicle_type in: query required: false description: Vehicle type selection override (nullable non-negative integer) schema: type: integer minimum: 0 responses: '200': description: Dynamic image rendered successfully content: image/png: schema: type: string format: binary '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' /guest/validation/customer-number: post: tags: - Users summary: Validate customer number description: Check if a customer number is valid and exists operationId: validateCustomerNumber security: [] requestBody: required: true content: application/json: schema: type: object required: - customer_number properties: customer_number: type: integer responses: '200': description: Customer number validation successful content: application/json: schema: {} '400': $ref: '#/components/responses/BadRequest' /guest/departments: get: tags: - Departments summary: List public departments description: Get list of departments without authentication operationId: listGuestDepartments security: [] parameters: - name: include_lanes in: query description: Whether to include lane status and self-serve information schema: type: boolean responses: '200': description: Departments retrieved successfully content: application/json: schema: type: array items: $ref: '#/components/schemas/DepartmentGuest' /department/selfserve/machine-types: get: tags: - Self-Serve summary: List reusable self-serve machine types operationId: listSelfserveMachineTypes parameters: - name: id in: query required: true schema: type: integer - $ref: '#/components/parameters/PageParam' - $ref: '#/components/parameters/PerPageParam' - $ref: '#/components/parameters/SearchParam' responses: '200': description: Successfully retrieved machine types content: application/json: schema: oneOf: - $ref: '#/components/schemas/SelfserveMachineType' - type: array items: $ref: '#/components/schemas/SelfserveMachineType' '404': $ref: '#/components/responses/NotFound' post: tags: - Self-Serve summary: Add reusable self-serve machine type operationId: addSelfserveMachineType requestBody: required: true content: application/json: schema: type: object required: - name properties: name: type: string description: type: string nullable: true responses: '200': description: Successfully added machine type content: application/json: schema: $ref: '#/components/schemas/SelfserveMachineType' put: tags: - Self-Serve summary: Update reusable self-serve machine type operationId: updateSelfserveMachineType parameters: - name: id in: query required: true schema: type: integer requestBody: content: application/json: schema: type: object properties: name: type: string description: type: string nullable: true responses: '200': description: Successfully updated machine type content: application/json: schema: $ref: '#/components/schemas/SelfserveMachineType' '404': $ref: '#/components/responses/NotFound' delete: tags: - Self-Serve summary: Delete reusable self-serve machine type operationId: deleteSelfserveMachineType parameters: - name: id in: query required: true schema: type: integer responses: '200': description: Successfully deleted machine type content: application/json: schema: type: string example: Machine type deleted '404': $ref: '#/components/responses/NotFound' /department/selfserve/questions: get: tags: - Self-Serve summary: List self-serve questions description: Retrieve a list of self-serve questions for a department, lane, or product. operationId: listSelfserveQuestions parameters: - name: id in: query description: Filter by question ID schema: type: integer - name: department in: query description: Filter by department ID schema: type: integer - name: lane in: query description: Filter by lane ID schema: type: integer - name: product in: query description: Filter by product ID schema: type: integer - $ref: '#/components/parameters/PageParam' - $ref: '#/components/parameters/PerPageParam' - $ref: '#/components/parameters/SearchParam' - $ref: '#/components/parameters/FiltersParam' responses: '200': description: Successfully retrieved questions content: application/json: schema: type: array items: $ref: '#/components/schemas/DepartmentSelfserveQuestion' '400': $ref: '#/components/responses/BadRequest' '404': $ref: '#/components/responses/NotFound' post: tags: - Self-Serve summary: Add self-serve question description: Add a new self-serve question. Questions are typically shared across departments and lanes by omitting department, lane, and product, which default to 0. operationId: addSelfserveQuestion requestBody: required: true content: application/json: schema: type: object required: - question - description properties: department: type: integer default: 0 lane: type: integer default: 0 product: type: integer default: 0 question: type: string description: type: string condition_id: type: integer nullable: true order_priority: type: integer default: 0 responses: '200': description: Successfully added question content: application/json: schema: $ref: '#/components/schemas/DepartmentSelfserveQuestion' '400': $ref: '#/components/responses/BadRequest' '500': $ref: '#/components/responses/InternalServerError' put: tags: - Self-Serve summary: Update self-serve question description: Update an existing self-serve question. operationId: updateSelfserveQuestion parameters: - name: id in: query required: true description: Question ID schema: type: integer requestBody: content: application/json: schema: type: object properties: department: type: integer lane: type: integer product: type: integer question: type: string description: type: string condition_id: type: integer nullable: true order_priority: type: integer responses: '200': description: Successfully updated question content: application/json: schema: $ref: '#/components/schemas/DepartmentSelfserveQuestion' '400': $ref: '#/components/responses/BadRequest' '404': $ref: '#/components/responses/NotFound' delete: tags: - Self-Serve summary: Delete self-serve question description: Delete a self-serve question by ID. operationId: deleteSelfserveQuestion parameters: - name: id in: query required: true description: Question ID schema: type: integer responses: '200': description: Successfully deleted question content: application/json: schema: type: string example: Question deleted '400': $ref: '#/components/responses/BadRequest' '404': $ref: '#/components/responses/NotFound' /department/selfserve/conditions: get: tags: - Self-Serve summary: List self-serve conditions description: Retrieve a list of self-serve conditions for a department, lane, or product. operationId: listSelfserveConditions parameters: - name: id in: query description: Filter by condition ID schema: type: integer - name: department in: query description: Filter by department ID schema: type: integer - name: lane in: query description: Filter by lane ID schema: type: integer - name: product in: query description: Filter by product ID schema: type: integer - name: condition_id in: query description: Filter by condition ID schema: type: integer - name: machine_type_id in: query description: Filter by reusable machine type ID schema: type: integer - name: machine_type_id in: query description: Filter by reusable machine type ID schema: type: integer - $ref: '#/components/parameters/PageParam' - $ref: '#/components/parameters/PerPageParam' - $ref: '#/components/parameters/SearchParam' - $ref: '#/components/parameters/FiltersParam' responses: '200': description: Successfully retrieved conditions content: application/json: schema: type: array items: $ref: '#/components/schemas/DepartmentSelfserveCondition' '400': $ref: '#/components/responses/BadRequest' '404': $ref: '#/components/responses/NotFound' post: tags: - Self-Serve summary: Add self-serve condition description: Add a new self-serve condition. Either provide a reusable machine_type_id or a legacy department/lane/product scope. operationId: addSelfserveCondition requestBody: required: true content: application/json: schema: type: object required: - name - description properties: department: type: integer default: 0 lane: type: integer default: 0 product: type: integer default: 0 machine_type_id: type: integer nullable: true condition_id: type: integer nullable: true name: type: string description: type: string responses: '200': description: Successfully added condition content: application/json: schema: $ref: '#/components/schemas/DepartmentSelfserveCondition' '400': $ref: '#/components/responses/BadRequest' '500': $ref: '#/components/responses/InternalServerError' put: tags: - Self-Serve summary: Update self-serve condition description: Update an existing self-serve condition. operationId: updateSelfserveCondition parameters: - name: id in: query required: true description: Condition ID schema: type: integer requestBody: content: application/json: schema: type: object properties: department: type: integer lane: type: integer product: type: integer machine_type_id: type: integer nullable: true condition_id: type: integer nullable: true name: type: string description: type: string responses: '200': description: Successfully updated condition content: application/json: schema: $ref: '#/components/schemas/DepartmentSelfserveCondition' '400': $ref: '#/components/responses/BadRequest' '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalServerError' delete: tags: - Self-Serve summary: Delete self-serve condition description: Delete a self-serve condition. operationId: deleteSelfserveCondition parameters: - name: id in: query required: true description: Condition ID schema: type: integer responses: '200': description: Successfully deleted condition content: application/json: schema: type: string example: Condition deleted '400': $ref: '#/components/responses/BadRequest' '404': $ref: '#/components/responses/NotFound' /department/selfserve/condition/rules: get: tags: - Self-Serve summary: List self-serve condition rules description: Retrieve a list of self-serve condition rules. operationId: listSelfserveConditionRules parameters: - name: id in: query description: Filter by rule ID schema: type: integer - name: condition_id in: query description: Filter by condition ID schema: type: integer - name: type in: query description: Filter by rule type schema: type: string - name: object_type in: query description: Filter by object type schema: type: string - name: object_id in: query description: Filter by object ID schema: type: integer - $ref: '#/components/parameters/PageParam' - $ref: '#/components/parameters/PerPageParam' - $ref: '#/components/parameters/SearchParam' - $ref: '#/components/parameters/FiltersParam' responses: '200': description: Successfully retrieved condition rules content: application/json: schema: type: array items: $ref: '#/components/schemas/DepartmentSelfserveConditionRule' '400': $ref: '#/components/responses/BadRequest' '404': $ref: '#/components/responses/NotFound' post: tags: - Self-Serve summary: Add self-serve condition rule description: Add a new self-serve condition rule. operationId: addSelfserveConditionRule requestBody: required: true content: application/json: schema: type: object required: - condition_id - type - object_type - object_id - name - description properties: condition_id: type: integer type: type: string object_type: type: string object_id: type: integer name: type: string description: type: string responses: '200': description: Successfully added condition rule content: application/json: schema: $ref: '#/components/schemas/DepartmentSelfserveConditionRule' '400': $ref: '#/components/responses/BadRequest' '500': $ref: '#/components/responses/InternalServerError' put: tags: - Self-Serve summary: Update self-serve condition rule description: Update an existing self-serve condition rule. operationId: updateSelfserveConditionRule parameters: - name: id in: query required: true description: Rule ID schema: type: integer requestBody: content: application/json: schema: type: object properties: condition_id: type: integer type: type: string object_type: type: string object_id: type: integer name: type: string description: type: string responses: '200': description: Successfully updated condition rule content: application/json: schema: $ref: '#/components/schemas/DepartmentSelfserveConditionRule' '400': $ref: '#/components/responses/BadRequest' '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalServerError' delete: tags: - Self-Serve summary: Delete self-serve condition rule description: Delete a self-serve condition rule. operationId: deleteSelfserveConditionRule parameters: - name: id in: query required: true description: Rule ID schema: type: integer responses: '200': description: Successfully deleted condition rule content: application/json: schema: type: string example: Rule deleted '400': $ref: '#/components/responses/BadRequest' '404': $ref: '#/components/responses/NotFound' /department/selfserve/vehicle/conditions: get: tags: - Self-Serve summary: List vehicle conditions description: Retrieve a list of vehicle conditions for a department, lane, reg, or question. Customers will only see their own vehicle conditions. operationId: listSelfserveVehicleConditions parameters: - name: id in: query description: Filter by condition ID schema: type: integer - name: department in: query description: Filter by department ID schema: type: integer - name: lane in: query description: Filter by lane ID schema: type: integer - name: reg in: query description: Filter by vehicle registration number schema: type: string - name: question in: query description: Filter by question ID schema: type: integer - name: customer_id in: query description: Filter by customer ID schema: type: integer - $ref: '#/components/parameters/PageParam' - $ref: '#/components/parameters/PerPageParam' - $ref: '#/components/parameters/SearchParam' - $ref: '#/components/parameters/FiltersParam' responses: '200': description: Successfully retrieved vehicle conditions content: application/json: schema: type: array items: $ref: '#/components/schemas/DepartmentSelfserveVehicleCondition' '400': $ref: '#/components/responses/BadRequest' '404': $ref: '#/components/responses/NotFound' post: tags: - Self-Serve summary: Add vehicle condition description: Add a new vehicle condition (answer to a question). Customers can only add conditions for their own vehicles. operationId: addSelfserveVehicleCondition requestBody: required: true content: application/json: schema: type: object required: - department - lane - reg - question properties: department: type: integer lane: type: integer reg: type: string question: type: integer value: type: boolean customer_id: type: integer nullable: true vehicle_type: type: integer nullable: true description: Optional product/vehicle type override used when refreshing the self-serve summary. vehicle_type_id: type: integer nullable: true description: Alias for vehicle_type. activate_machine: type: boolean default: true description: Whether the session synchronization may enable the machine relay. User wash-start saves answers with false. sync_relay_state: type: boolean default: true description: Whether the answer mutation should synchronize live relay state. responses: '200': description: Successfully added vehicle condition content: application/json: schema: $ref: '#/components/schemas/DepartmentSelfserveVehicleConditionMutationResponse' '400': $ref: '#/components/responses/BadRequest' '500': $ref: '#/components/responses/InternalServerError' put: tags: - Self-Serve summary: Update vehicle condition description: Update an existing vehicle condition. Customers can only update conditions for their own vehicles. operationId: updateSelfserveVehicleCondition parameters: - name: id in: query required: true description: Condition ID schema: type: integer requestBody: content: application/json: schema: type: object properties: department: type: integer lane: type: integer reg: type: string question: type: integer value: type: boolean customer_id: type: integer nullable: true vehicle_type: type: integer nullable: true description: Optional product/vehicle type override used when refreshing the self-serve summary. vehicle_type_id: type: integer nullable: true description: Alias for vehicle_type. activate_machine: type: boolean default: true description: Whether the session synchronization may enable the machine relay. sync_relay_state: type: boolean default: true description: Whether the mutation should synchronize live relay state. responses: '200': description: Successfully updated vehicle condition content: application/json: schema: $ref: '#/components/schemas/DepartmentSelfserveVehicleConditionMutationResponse' '400': $ref: '#/components/responses/BadRequest' '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalServerError' delete: tags: - Self-Serve summary: Delete vehicle condition description: Delete a vehicle condition. Customers can only delete conditions for their own vehicles. operationId: deleteSelfserveVehicleCondition parameters: - name: id in: query required: true description: Condition ID schema: type: integer responses: '200': description: Successfully deleted vehicle condition content: application/json: schema: type: object properties: message: type: string example: Condition deleted selfserve: allOf: - $ref: '#/components/schemas/SelfserveWashSummary' nullable: true '400': $ref: '#/components/responses/BadRequest' '404': $ref: '#/components/responses/NotFound' /department/selfserve/vehicle/allowed: get: tags: - Self-Serve summary: Check whether self-serve is allowed for a vehicle on a lane description: Customers with own self-serve permissions may evaluate any registration plate for their wash. Persisted self-serve answers are only applied when they are scoped to the authenticated customer. operationId: getSelfserveVehicleAllowed parameters: - name: lane_id in: query required: true schema: type: integer - name: reg in: query required: true schema: type: string - name: vehicle_type_id in: query required: false description: Optional vehicle type override used when no vehicle is found by registration plate. schema: type: integer minimum: 0 - name: vehicle_type in: query required: false description: Backward-compatible alias of `vehicle_type_id`. schema: type: integer minimum: 0 responses: '200': description: Successfully evaluated self-serve eligibility content: application/json: schema: $ref: '#/components/schemas/SelfserveVehicleAllowedResponse' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' /department/selfserve/washes/summary: get: tags: - Self-Serve summary: Get self-serve wash summary operationId: getSelfserveWashSummary parameters: - name: session_id in: query required: false schema: type: integer - name: lane_id in: query required: false schema: type: integer - name: reg in: query required: false schema: type: string - name: vehicle_type_id in: query required: false description: Optional vehicle type override used to refresh summary data for unknown or reassigned plates. schema: type: integer minimum: 0 - name: vehicle_type in: query required: false description: Backward-compatible alias of `vehicle_type_id`. schema: type: integer minimum: 0 responses: '200': description: Successfully retrieved self-serve wash summary content: application/json: schema: $ref: '#/components/schemas/SelfserveWashSummary' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' /department/selfserve/tasks: get: tags: - Self-Serve summary: List self-serve tasks description: Retrieve a list of self-serve tasks for a department, lane, product, or condition_id. operationId: listSelfserveTasks parameters: - name: id in: query description: Filter by task ID schema: type: integer - name: department in: query description: Filter by department ID schema: type: integer - name: lane in: query description: Filter by lane ID schema: type: integer - name: product in: query description: Filter by product ID schema: type: integer - name: condition_id in: query description: Filter by condition ID schema: type: integer - $ref: '#/components/parameters/PageParam' - $ref: '#/components/parameters/PerPageParam' - $ref: '#/components/parameters/SearchParam' - $ref: '#/components/parameters/FiltersParam' responses: '200': description: Successfully retrieved tasks content: application/json: schema: type: array items: $ref: '#/components/schemas/DepartmentSelfserveTask' '400': $ref: '#/components/responses/BadRequest' '404': $ref: '#/components/responses/NotFound' post: tags: - Self-Serve summary: Add self-serve task description: Add a new self-serve task. Either provide a reusable machine_type_id or a legacy department/lane/product scope. operationId: addSelfserveTask requestBody: required: true content: application/json: schema: type: object required: - task - description properties: department: type: integer default: 0 lane: type: integer default: 0 product: type: integer default: 0 machine_type_id: type: integer nullable: true condition_id: type: integer nullable: true task: type: string description: type: string order_priority: type: integer default: 0 services: type: array description: Optional services enabled by this task. Items must be valid service enum names. items: $ref: '#/components/schemas/SelfserveLaneService' buttons: type: array description: Optional dynamic image button IDs enabled by this task. items: type: integer default: [] dynamic_images_vehicle_type: type: integer nullable: true description: Optional vehicle type selection override for the machine UI. Integer >= 0 or null. responses: '200': description: Successfully added task content: application/json: schema: $ref: '#/components/schemas/DepartmentSelfserveTask' '400': $ref: '#/components/responses/BadRequest' '500': $ref: '#/components/responses/InternalServerError' put: tags: - Self-Serve summary: Update self-serve task description: Update an existing self-serve task. operationId: updateSelfserveTask parameters: - name: id in: query required: true description: Task ID schema: type: integer requestBody: content: application/json: schema: type: object properties: department: type: integer lane: type: integer product: type: integer machine_type_id: type: integer nullable: true condition_id: type: integer nullable: true task: type: string description: type: string order_priority: type: integer services: type: array nullable: true description: Services enabled by this task. Set to null to clear all services. items: $ref: '#/components/schemas/SelfserveLaneService' buttons: type: array nullable: true description: Button IDs enabled by this task. Set to null to clear all buttons. items: type: integer dynamic_images_vehicle_type: type: integer nullable: true description: Vehicle type selection override. Set to null to clear. responses: '200': description: Successfully updated task content: application/json: schema: $ref: '#/components/schemas/DepartmentSelfserveTask' '400': $ref: '#/components/responses/BadRequest' '404': $ref: '#/components/responses/NotFound' delete: tags: - Self-Serve summary: Delete self-serve task description: Delete a self-serve task by ID. operationId: deleteSelfserveTask parameters: - name: id in: query required: true description: Task ID schema: type: integer responses: '200': description: Successfully deleted task content: application/json: schema: type: string example: Task deleted '400': $ref: '#/components/responses/BadRequest' '404': $ref: '#/components/responses/NotFound' /department/selfserve/tasks/attachments: get: tags: - Self-Serve summary: List task attachments description: Retrieve a list of attachments for a specific self-serve task. operationId: listSelfserveTaskAttachments parameters: - name: id in: query required: true description: Task ID schema: type: integer responses: '200': description: Successfully retrieved task attachments content: application/json: schema: {} '400': $ref: '#/components/responses/BadRequest' '404': $ref: '#/components/responses/NotFound' delete: tags: - Self-Serve summary: Delete task attachment description: Remove an attachment from a specific self-serve task. operationId: deleteSelfserveTaskAttachment parameters: - name: task_id in: query required: true description: Task ID schema: type: integer - name: attachment_id in: query required: true description: Attachment ID schema: type: integer responses: '200': description: Attachment deleted successfully content: application/json: schema: type: object properties: message: type: string example: Attachment deleted successfully '400': $ref: '#/components/responses/BadRequest' '404': $ref: '#/components/responses/NotFound' /department/selfserve/tasks/attachments/upload: post: tags: - Self-Serve summary: Upload task attachment description: Upload a new attachment to a specific self-serve task using base64 encoding. operationId: uploadSelfserveTaskAttachment requestBody: required: true content: application/json: schema: type: object required: - task_id - base64_file - file_name properties: task_id: type: integer base64_file: type: string description: Base64 encoded file content file_name: type: string description: Name of the file including extension responses: '200': description: Attachment uploaded successfully content: application/json: schema: {} '400': $ref: '#/components/responses/BadRequest' '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalServerError' /department/selfserve/tasks/attachments/download: get: tags: - Self-Serve summary: Download task attachment description: Generate a download link for a specific self-serve task attachment. operationId: downloadSelfserveTaskAttachment parameters: - name: task_id in: query required: true description: Task ID schema: type: integer - name: attachment_id in: query required: true description: Attachment ID schema: type: integer responses: '200': description: Successfully generated download link content: application/json: schema: type: object properties: download_link: type: string format: uri '400': $ref: '#/components/responses/BadRequest' '404': $ref: '#/components/responses/NotFound' /department/selfserve/studio/graph: get: tags: - Self-Serve summary: Get all-in-one self-serve studio graph description: Returns the replacement studio workspace graph backed by the schema_version 2 draft config. Conditions own grouped expression trees directly; standalone rule nodes are omitted from v2 graphs. operationId: getSelfserveStudioGraph parameters: - name: department in: query required: true schema: type: integer responses: '200': description: Studio graph returned content: application/json: schema: $ref: '#/components/schemas/SelfserveStudioGraph' put: tags: - Self-Serve summary: Bulk save all-in-one self-serve studio graph changes description: Creates, updates, deletes, connects, disconnects, reorders, and upserts self-serve answer paths by editing the schema_version 2 draft config JSON. Condition connections create expression predicates; Path Editor upserts create normal generated condition and task nodes; layout remains separate from runtime behavior. operationId: saveSelfserveStudioGraph requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/SelfserveStudioGraphSaveRequest' responses: '200': description: Studio graph saved content: application/json: schema: $ref: '#/components/schemas/SelfserveStudioGraph' '422': $ref: '#/components/responses/BadRequest' /department/selfserve/studio/layout: put: tags: - Self-Serve summary: Save self-serve studio canvas layout description: Persists canvas-only node positions and viewport state. Layout does not affect runtime wash behavior. operationId: saveSelfserveStudioLayout requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/SelfserveStudioLayoutSaveRequest' responses: '200': description: Layout saved content: application/json: schema: $ref: '#/components/schemas/SelfserveStudioLayout' /department/selfserve/studio/validate: post: tags: - Self-Serve summary: Validate self-serve studio graph operationId: validateSelfserveStudioGraph requestBody: required: true content: application/json: schema: type: object required: [department] properties: department: type: integer responses: '200': description: Validation result content: application/json: schema: $ref: '#/components/schemas/SelfserveStudioValidation' /department/selfserve/studio/simulate: post: tags: - Self-Serve summary: Simulate self-serve studio runtime operationId: simulateSelfserveStudioGraph requestBody: required: true content: application/json: schema: type: object required: [department, lane_id, reg] properties: department: { type: integer } lane_id: { type: integer } reg: { type: string } customer_number: { type: integer, nullable: true } vehicle_type_id: { type: integer, nullable: true } config_source: type: string enum: [draft, published] default: draft answer_overrides: type: array items: type: object required: [question_id] properties: question_id: { type: integer } value: type: boolean nullable: true include_hardware: type: boolean default: true mode: type: string enum: [full_dry_run] default: full_dry_run responses: '200': description: Simulator result content: application/json: schema: $ref: '#/components/schemas/SelfserveStudioSimulationResponse' /department/selfserve/studio/path-outcomes: post: tags: - Self-Serve summary: Project grouped self-serve studio question path outcomes description: Enumerates feasible yes/no answer paths for the selected studio scope and groups terminal paths by resulting tasks, services, and dry-run signal timeline. No live hardware commands are sent. operationId: projectSelfserveStudioPathOutcomes requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/SelfserveStudioPathOutcomesRequest' responses: '200': description: Grouped path outcomes returned content: application/json: schema: $ref: '#/components/schemas/SelfserveStudioPathOutcomesResponse' '422': $ref: '#/components/responses/BadRequest' /department/selfserve/studio/path-outcomes/stream: post: tags: - Self-Serve summary: Stream self-serve studio question path outcome progress description: Streams newline-delimited JSON progress events while enumerating the complete feasible yes/no answer path space. Progress events contain the same response shape as the final result with partial outcomes and paths. operationId: streamSelfserveStudioPathOutcomes requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/SelfserveStudioPathOutcomesRequest' responses: '200': description: Newline-delimited path outcome progress events content: application/x-ndjson: schema: type: string '422': $ref: '#/components/responses/BadRequest' /department/selfserve/studio/path-confirmations: post: tags: - Self-Serve summary: Confirm or reset a projected self-serve studio path description: Stores confirmation for a projected terminal path using its stable path and result signatures. Projections report confirmed, unconfirmed, or stale when the resulting tasks, buttons, services, or signals change. operationId: confirmSelfserveStudioPath requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/SelfserveStudioPathConfirmationRequest' responses: '200': description: Path confirmation updated content: application/json: schema: $ref: '#/components/schemas/SelfserveStudioPathConfirmation' '422': $ref: '#/components/responses/BadRequest' /department/selfserve/studio/publish: post: tags: - Self-Serve summary: Publish self-serve studio draft operationId: publishSelfserveStudioDraft requestBody: required: true content: application/json: schema: type: object required: [department] properties: department: { type: integer } responses: '200': description: Published version content: application/json: schema: $ref: '#/components/schemas/SelfserveConfigVersion' /department/selfserve/studio/rollback: post: tags: - Self-Serve summary: Roll back self-serve studio to an earlier version operationId: rollbackSelfserveStudioDraft requestBody: required: true content: application/json: schema: type: object required: [department, target_version_id] properties: department: { type: integer } target_version_id: { type: integer } responses: '200': description: Rollback version content: application/json: schema: $ref: '#/components/schemas/SelfserveConfigVersion' /department/selfserve/studio/gateway-action: post: tags: - Self-Serve summary: Run permission-gated edge gateway action from studio operationId: runSelfserveStudioGatewayAction requestBody: required: true content: application/json: schema: type: object required: [department, gateway_id, action] properties: department: { type: integer } gateway_id: { type: integer } action: type: string enum: [discovery, discover, update, uninstall, cancel, rotate_credentials, bindings] confirm: type: boolean description: Required for dangerous gateway actions such as uninstall and credential rotation. operation_id: { type: integer, nullable: true } request: type: object additionalProperties: true bindings: type: array items: type: object additionalProperties: true responses: '200': description: Gateway action result content: application/json: schema: type: object additionalProperties: true # Products Endpoints /products: get: tags: - Products summary: List products description: Retrieve a list of products with optional filters for customer pricing and department operationId: listProducts parameters: - name: customer_id in: query schema: type: integer description: Customer ID for custom pricing - name: department_id in: query schema: type: integer description: Department ID for department-specific pricing - name: category in: query schema: type: integer description: Filter by category ID - name: id in: query schema: type: integer description: Get specific product by ID - name: final_price in: query schema: type: boolean description: Whether to return final prices including discounts - $ref: '#/components/parameters/PageParam' - $ref: '#/components/parameters/PerPageParam' responses: '200': description: Products retrieved successfully content: application/json: schema: type: array items: $ref: '#/components/schemas/Product' post: tags: - Products summary: Create product description: Create a new product operationId: createProduct requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ProductCreate' responses: '201': description: Product created successfully content: application/json: schema: {} '400': $ref: '#/components/responses/BadRequest' put: tags: - Products summary: Update product description: Update an existing product operationId: updateProduct requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ProductUpdate' responses: '200': description: Product updated successfully content: application/json: schema: {} '400': $ref: '#/components/responses/BadRequest' # Categories Endpoints /categories: get: tags: - Categories summary: List categories description: Retrieve a list of product categories operationId: listCategories parameters: - $ref: '#/components/parameters/PageParam' - $ref: '#/components/parameters/PerPageParam' responses: '200': description: Categories retrieved successfully content: application/json: schema: type: array items: $ref: '#/components/schemas/Category' post: tags: - Categories summary: Create category description: Create a new product category operationId: createCategory requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CategoryCreate' responses: '201': description: Category created successfully content: application/json: schema: {} put: tags: - Categories summary: Update category description: Update an existing category operationId: updateCategory requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CategoryUpdate' responses: '200': description: Category updated successfully content: application/json: schema: {} # Bookings Endpoints /bookings: get: tags: - Bookings summary: List bookings description: Retrieve a list of bookings operationId: listBookings parameters: - $ref: '#/components/parameters/PageParam' - $ref: '#/components/parameters/PerPageParam' responses: '200': description: Bookings retrieved successfully content: application/json: schema: type: array items: $ref: '#/components/schemas/Booking' put: tags: - Bookings summary: Update booking description: Update an existing booking operationId: updateBooking requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/BookingUpdate' responses: '200': description: Booking updated successfully content: application/json: schema: {} /user/bookings: get: tags: - Bookings summary: Get user bookings description: Retrieve bookings for the authenticated user operationId: getUserBookings responses: '200': description: User bookings retrieved successfully content: application/json: schema: type: array items: $ref: '#/components/schemas/Booking' /order-bookings: get: tags: - Bookings summary: List order bookings operationId: listOrderBookings parameters: - name: id in: query required: false schema: {type: integer} responses: '200': description: Success content: application/json: schema: {} # Goals Endpoints /goals/department: get: tags: - Goals summary: List or get department goals description: | Retrieve a list of department goals or a single goal when `id` is provided. Access control: - A user may only access goals where the goal's `departments` set is a subset of the user's departments. - Users with the `superuser` permission may access all goals. operationId: listDepartmentGoals parameters: - name: id in: query required: false schema: { type: integer } description: When provided, returns the single goal with this id (if accessible) - $ref: '#/components/parameters/PageParam' - $ref: '#/components/parameters/PerPageParam' - $ref: '#/components/parameters/SearchParam' - $ref: '#/components/parameters/FiltersParam' responses: '200': description: Goals retrieved successfully content: application/json: schema: type: array items: $ref: '#/components/schemas/DepartmentGoal' '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } post: tags: - Goals summary: Create department goal description: | Create a new department goal. Access control: - The provided `departments` must be a subset of the user's departments unless the user has `superuser`. operationId: createDepartmentGoal requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/DepartmentGoalCreate' responses: '201': description: Department goal created successfully content: application/json: schema: $ref: '#/components/schemas/DepartmentGoal' '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } put: tags: - Goals summary: Update department goal description: | Update an existing department goal by `id`. Access control: - The creator (`created_by`) may update regardless of department membership. - Otherwise the user must satisfy the same subset rule as for read access, and any new `departments` provided must also be a subset unless the user has `superuser`. operationId: updateDepartmentGoal requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/DepartmentGoalUpdate' responses: '200': description: Department goal updated successfully content: application/json: schema: $ref: '#/components/schemas/DepartmentGoal' '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } /goals/department/progress-alert/test: post: tags: - Goals summary: Send a test progress alert for a department goal description: | Sends a progress alert for a department goal to the destination defined in the goal's criteria. Permission required: `goals_department_progress_alert_test`. Access control: - The caller must be a superuser or belong to all departments targeted by the goal. Behavior: - Looks up the goal by `id`. - Rebuilds the criteria from stored JSON and attaches the goal's departments. - Renders the alert using the server-side renderer (respecting progress type/style/format and destination limits). - Sends the alert to Slack, Email, or SMS depending on `progress_alert_destination`, unless overridden. operationId: sendDepartmentGoalProgressAlertTest requestBody: required: true content: application/json: schema: type: object required: - id properties: id: type: integer description: The department goal id example: 42 overrideDestination: type: string description: Override the destination for this test enum: [SLACK, EMAIL, SMS, NONE] example: SLACK email_to: type: string format: email description: Email recipient when destination is EMAIL example: tester@example.com subject: type: string description: Optional email subject when destination is EMAIL example: Dept Goal Progress Test sms_to: description: One or more MSISDN recipients when destination is SMS oneOf: - type: string description: Comma or semicolon separated list example: "+4512345678, +4598765432" - type: array items: type: string example: ["+4512345678", "+4598765432"] slack_webhook: type: string description: Slack webhook URL when destination is SLACK example: https://hooks.slack.com/services/T000/B000/XXX department_id: type: integer description: Department id to use that department's Slack webhook when destination is SLACK example: 3 responses: '200': description: Alert sent successfully content: application/json: schema: type: object properties: id: type: integer description: Goal id destination: type: string description: Final destination used enum: [SLACK, EMAIL, SMS, NONE] target: description: The target used for delivery (email address, phone numbers, department id, or webhook) message_preview: type: string description: Rendered message preview provider_response: description: Provider-specific response or status message '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '500': { $ref: '#/components/responses/InternalServerError' } delete: tags: - Goals summary: Delete department goal description: | Delete a department goal by `id`. Access control: - The creator (`created_by`) may delete regardless of department membership. - Otherwise the user must satisfy the subset rule or have `superuser`. operationId: deleteDepartmentGoal parameters: - name: id in: query required: true schema: { type: integer } description: ID of the goal to delete responses: '200': description: Department goal deleted successfully content: application/json: schema: {} '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '/order-bookings': post: tags: - Bookings summary: Create order booking operationId: createOrderBooking requestBody: required: true content: application/json: schema: type: object required: [department, reg_1, datetime, items] properties: customer_number: {type: integer} department: {type: integer} reg_1: {type: string} reg_2: {type: string} reg_3: {type: string} datetime: {type: string, format: date-time} note: {type: string} reference: {type: string} po: {type: string} pickup: {type: boolean} items: type: array items: type: object required: [id, quantity] properties: id: {type: integer} quantity: {type: integer} responses: '200': description: Success '400': description: Invalid booking input or a product blocked by active customer rules content: application/json: schema: oneOf: - $ref: '#/components/schemas/CustomerRuleProductRestrictedResponse' - type: object put: tags: - Bookings summary: Update order booking operationId: updateOrderBooking requestBody: required: true content: application/json: schema: type: object required: [id] properties: id: {type: integer} customer_number: {type: integer} department: {type: integer} reg_1: {type: string} reg_2: {type: string} reg_3: {type: string} datetime: {type: string, format: date-time} note: {type: string} reference: {type: string} po: {type: string} pickup: {type: boolean} order_id: {type: integer, nullable: true} items: type: array items: type: object required: [id, quantity] properties: id: {type: integer} quantity: {type: integer} responses: '200': description: Success delete: tags: - Bookings summary: Delete order booking operationId: deleteOrderBooking parameters: - name: id in: query required: true schema: {type: integer} responses: '200': description: Success /order-bookings/booking-confirmation/resend: post: tags: - Bookings summary: Resend order booking confirmation description: Resends the customer booking confirmation email for an order booking. Requires `resend_booking_confirmations` and access to the booking's department. operationId: resendOrderBookingConfirmation requestBody: required: true content: application/json: schema: type: object required: [id] properties: id: {type: integer} responses: '200': description: Booking confirmation resent successfully content: application/json: schema: {} '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } /order-bookings/completion-confirmation/resend: post: tags: - Bookings summary: Resend order booking completion confirmation description: Resends the customer completion confirmation email with the wash certificate for a completed order booking. Requires `complete_bookings` and access to the booking's department. operationId: resendOrderBookingCompletionConfirmation requestBody: required: true content: application/json: schema: type: object required: [id] properties: id: {type: integer} responses: '200': description: Completion confirmation resent successfully content: application/json: schema: {} '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '409': { $ref: '#/components/responses/Conflict' } /order-bookings/complete: post: tags: - Bookings summary: Complete order booking operationId: completeOrderBooking requestBody: required: true content: application/json: schema: type: object required: [id] properties: id: {type: integer} safety_seal: {type: integer} responses: '200': description: Success content: application/json: schema: {} /admin/bookings/sync: post: tags: - Bookings summary: Sync booking from external system operationId: syncBooking requestBody: required: false content: application/json: schema: {} responses: '200': description: Success content: application/json: schema: {} /admin/bookings/department/count: get: tags: - Bookings summary: Get department unfulfilled bookings count operationId: getDepartmentBookingCount parameters: - name: department_id in: query required: true schema: {type: integer} responses: '200': description: Success content: application/json: schema: {} /user/bookings/washcertificate/download: post: tags: - Bookings summary: Get download link for own wash certificate operationId: downloadOwnWashCertificate requestBody: required: true content: application/json: schema: type: object required: [id] properties: id: {type: integer} responses: '200': description: Success content: application/json: schema: {} /bookings/download_pdf: get: tags: - Bookings summary: Download booking PDF operationId: downloadBookingPdf parameters: - name: id in: query required: true schema: {type: integer} responses: '200': description: Success content: application/json: schema: {} /admin/bookings/delete: post: tags: - Bookings summary: Delete booking (admin) operationId: adminDeleteBooking requestBody: required: true content: application/json: schema: type: object required: [id] properties: id: {type: integer} responses: '200': description: Success content: application/json: schema: {} /superuser/bookings/sync/all: post: tags: - Bookings summary: Sync all bookings from external system operationId: syncAllBookings requestBody: required: false content: application/json: schema: {} responses: '200': description: Success content: application/json: schema: {} /admin/bookings/completeWashWithoutWashCertificate: post: tags: - Bookings summary: Complete wash without wash certificate operationId: completeWashWithoutWashCertificate requestBody: required: true content: application/json: schema: type: object required: [id] properties: id: {type: integer} responses: '200': description: Success content: application/json: schema: {} /user/bookings/delete: post: tags: - Bookings summary: Delete own booking operationId: deleteOwnBooking requestBody: required: true content: application/json: schema: type: object required: [id] properties: id: {type: integer} responses: '200': description: Success content: application/json: schema: {} # Invoices Endpoints /invoices/draft: get: tags: - Invoices summary: List draft invoices description: Retrieve a list of draft invoices operationId: listDraftInvoices parameters: - $ref: '#/components/parameters/PageParam' - $ref: '#/components/parameters/PerPageParam' responses: '200': description: Draft invoices retrieved successfully content: application/json: schema: {} /invoices/draft/close: post: tags: - Invoices summary: Close draft invoice description: Close a draft invoice operationId: closeDraftInvoice requestBody: required: true content: application/json: schema: type: object properties: id: type: integer responses: '200': description: Draft invoice closed successfully content: application/json: schema: {} /invoices/pdf: get: tags: - Invoices summary: Get invoice PDF description: Download an invoice as PDF operationId: getInvoicePdf parameters: - name: id in: query required: true schema: type: integer responses: '200': description: PDF retrieved successfully content: application/pdf: schema: type: string format: binary /user/invoices: get: tags: - Invoices summary: Get user invoices description: Retrieve invoices for the authenticated user operationId: getUserInvoices responses: '200': description: User invoices retrieved successfully content: application/json: schema: {} /collected-invoices: get: tags: - Invoices summary: List collected invoices description: Get list of collected invoices operationId: listCollectedInvoices parameters: - $ref: '#/components/parameters/PageParam' - $ref: '#/components/parameters/PerPageParam' responses: '200': description: Collected invoices retrieved successfully content: application/json: schema: {} post: tags: - Invoices summary: Create collected invoice description: Create a new collected invoice operationId: createCollectedInvoice requestBody: required: false content: application/json: schema: {} responses: '201': description: Collected invoice created successfully content: application/json: schema: {} put: tags: - Invoices summary: Update collected invoice description: Update a collected invoice operationId: updateCollectedInvoice requestBody: required: false content: application/json: schema: {} responses: '200': description: Collected invoice updated successfully content: application/json: schema: {} /collected-invoices/ready-to-invoice: get: tags: - Invoices summary: Get invoices ready to process description: Get collected invoices that are ready to be processed operationId: getReadyToInvoice responses: '200': description: Ready invoices retrieved successfully content: application/json: schema: {} /collected-invoices/economic: post: tags: - Invoices summary: Export collected invoice to e-conomic description: | Exports collected invoice to e-conomic. Uses async queue when available, otherwise falls back to synchronous processing. operationId: queueCollectedInvoiceEconomicTransfer requestBody: required: true content: application/json: schema: type: object required: [id] properties: id: type: integer minimum: 1 send_as_is: type: boolean default: false responses: '200': description: Collected invoice export processed synchronously (fallback) content: application/json: schema: $ref: '#/components/schemas/EconomicTransferSynchronousFallbackResponse' '202': description: Collected invoice transfer queued content: application/json: schema: $ref: '#/components/schemas/EconomicTransferQueueEnqueueResponse' '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '500': { $ref: '#/components/responses/InternalServerError' } /collected-invoices/stripe/book: post: tags: - Invoices summary: Export Stripe collected invoice to e-conomic description: | Exports a Stripe-backed collected invoice to e-conomic. Uses async queue when available, otherwise falls back to synchronous processing. operationId: queueStripeCollectedInvoiceEconomicTransfer requestBody: required: true content: application/json: schema: type: object required: [id] properties: id: type: integer minimum: 1 responses: '200': description: Stripe collected invoice export processed synchronously (fallback) content: application/json: schema: $ref: '#/components/schemas/EconomicTransferSynchronousFallbackResponse' '202': description: Stripe collected invoice transfer queued content: application/json: schema: $ref: '#/components/schemas/EconomicTransferQueueEnqueueResponse' '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '500': { $ref: '#/components/responses/InternalServerError' } /collected-invoices/economic/queue: get: tags: - Invoices summary: List collected-invoice e-conomic transfer queue jobs operationId: listCollectedInvoiceEconomicQueueJobs parameters: - name: status in: query required: false description: Comma-separated queue statuses to filter by. style: form explode: false schema: type: array items: $ref: '#/components/schemas/EconomicTransferQueueStatus' uniqueItems: true example: [QUEUED, FAILED] - name: limit in: query required: false schema: type: integer minimum: 1 maximum: 500 default: 50 - name: offset in: query required: false schema: type: integer minimum: 0 default: 0 responses: '200': description: Queue jobs retrieved content: application/json: schema: $ref: '#/components/schemas/EconomicTransferQueueListResponse' '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '503': { $ref: '#/components/responses/ServiceUnavailable' } '500': { $ref: '#/components/responses/InternalServerError' } /collected-invoices/economic/queue/monitor: get: tags: - Invoices summary: Monitor current-user visible collected-invoice e-conomic transfer queue jobs operationId: monitorCollectedInvoiceEconomicQueueJobs parameters: - name: limit in: query required: false schema: type: integer minimum: 1 maximum: 100 default: 50 responses: '200': description: Queue monitor state retrieved content: application/json: schema: $ref: '#/components/schemas/EconomicTransferQueueMonitorResponse' '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '503': { $ref: '#/components/responses/ServiceUnavailable' } '500': { $ref: '#/components/responses/InternalServerError' } /collected-invoices/economic/queue/status: get: tags: - Invoices summary: Get collected-invoice e-conomic transfer queue job status operationId: getCollectedInvoiceEconomicQueueJobStatus parameters: - name: job_id in: query required: true schema: type: integer minimum: 1 responses: '200': description: Queue job status content: application/json: schema: $ref: '#/components/schemas/EconomicTransferQueueStatusResponse' '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '503': { $ref: '#/components/responses/ServiceUnavailable' } '500': { $ref: '#/components/responses/InternalServerError' } /collected-invoices/economic/queue/retry: post: tags: - Invoices summary: Retry failed collected-invoice queue job operationId: retryCollectedInvoiceEconomicQueueJob requestBody: required: true content: application/json: schema: type: object required: [job_id] properties: job_id: type: integer minimum: 1 responses: '200': description: Queue job retried content: application/json: schema: $ref: '#/components/schemas/EconomicTransferQueueRetryResponse' '400': { $ref: '#/components/responses/BadRequest' } '409': { $ref: '#/components/responses/Conflict' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '503': { $ref: '#/components/responses/ServiceUnavailable' } '500': { $ref: '#/components/responses/InternalServerError' } /collected-invoices/economic/queue/dismiss: post: tags: - Invoices summary: Clear one completed or failed collected-invoice queue job for the current user operationId: dismissCollectedInvoiceEconomicQueueJob requestBody: required: true content: application/json: schema: type: object required: [job_id] properties: job_id: type: integer minimum: 1 responses: '200': description: Queue job cleared content: application/json: schema: $ref: '#/components/schemas/EconomicTransferQueueDismissResponse' '400': { $ref: '#/components/responses/BadRequest' } '409': { $ref: '#/components/responses/Conflict' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '503': { $ref: '#/components/responses/ServiceUnavailable' } '500': { $ref: '#/components/responses/InternalServerError' } /collected-invoices/economic/queue/dismiss-terminal: post: tags: - Invoices summary: Clear all visible completed or failed collected-invoice queue jobs for the current user operationId: dismissCollectedInvoiceEconomicTerminalQueueJobs responses: '200': description: Terminal queue jobs cleared content: application/json: schema: $ref: '#/components/schemas/EconomicTransferQueueDismissTerminalResponse' '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '503': { $ref: '#/components/responses/ServiceUnavailable' } '500': { $ref: '#/components/responses/InternalServerError' } /collected-invoices/economic/queue/run: post: tags: - Invoices summary: Run one collected-invoice queue batch immediately operationId: runCollectedInvoiceEconomicQueueBatch requestBody: required: false content: application/json: schema: type: object properties: limit: type: integer minimum: 1 maximum: 10 default: 10 responses: '200': description: Queue batch processed content: application/json: schema: $ref: '#/components/schemas/EconomicTransferQueueRunResponse' '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '503': { $ref: '#/components/responses/ServiceUnavailable' } '500': { $ref: '#/components/responses/InternalServerError' } /collected-invoices/economic/compare: get: tags: - Invoices summary: Compare collected invoice totals with E-conomic description: | Compares a collected invoice in the system with its corresponding invoice in E-conomic. Returns totals from both sources, their difference, and any warnings detected during comparison. operationId: compareCollectedInvoiceEconomic parameters: - name: collected_invoice_id in: query required: true description: The internal collected invoice ID to compare schema: type: integer minimum: 1 responses: '200': description: Comparison completed successfully content: application/json: schema: $ref: '#/components/schemas/CollectedInvoiceEconomicCompareResponse' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalServerError' /collected-invoices/economic/pdf: get: tags: - Invoices summary: Download a collected invoice e-conomic PDF description: | Resolves the requested collected invoice context and returns a presigned URL for the draft or booked e-conomic invoice PDF. operationId: downloadCollectedInvoiceEconomicPdf parameters: - name: collected_invoice_id in: query required: true description: The internal collected invoice ID schema: type: integer minimum: 1 - name: type in: query required: true description: Which e-conomic invoice PDF to download schema: type: string enum: - draft - booked responses: '200': description: PDF URL resolved successfully content: application/json: schema: $ref: '#/components/schemas/CollectedInvoiceEconomicPdfResponse' '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '500': { $ref: '#/components/responses/InternalServerError' } /collected-invoices/economic/v2/details: get: tags: - Invoices summary: Get deep V2 e-conomic invoice details description: | Returns normalized internal lines and best-effort fetched draft/booked e-conomic lines for a collected invoice, including department distributions and warnings. operationId: getCollectedInvoiceEconomicV2Details parameters: - name: collected_invoice_id in: query required: true schema: type: integer minimum: 1 responses: '200': description: Details resolved successfully content: application/json: schema: $ref: '#/components/schemas/CollectedInvoiceEconomicV2DetailsResponse' '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '500': { $ref: '#/components/responses/InternalServerError' } /collected-invoices/economic/v2/compare: get: tags: - Invoices summary: Compare internal invoice with draft/booked (V2) operationId: compareCollectedInvoiceEconomicV2 parameters: - name: collected_invoice_id in: query required: true schema: type: integer minimum: 1 responses: '200': description: Comparison completed content: application/json: schema: $ref: '#/components/schemas/CollectedInvoiceEconomicV2CompareResponse' examples: exactMatch: summary: Exact match between internal and draft/booked value: collected_invoice_id: 123 warnings: [] comparison: totals: internal_net_total: 694 targets: draft: target: draft status: exact_match overall_match: true booked: target: booked status: exact_match overall_match: true partialMismatch: summary: Partial mismatch with line and department differences value: collected_invoice_id: 123 warnings: - Non-billable line count differs comparison: totals: internal_net_total: 694 targets: draft: target: draft status: partial_mismatch overall_match: false mismatch_reasons: - quantity_mismatch - department_total_mismatch missingBooked: summary: Missing booked target value: collected_invoice_id: 123 comparison: totals: internal_net_total: 694 targets: booked: target: booked status: missing_target overall_match: false '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '500': { $ref: '#/components/responses/InternalServerError' } /collected-invoices/economic/v2/compare/bulk: post: tags: - Invoices summary: Bulk compare collected invoices against draft/booked (V2) operationId: compareCollectedInvoiceEconomicV2Bulk requestBody: required: true content: application/json: schema: type: object required: [collected_invoice_ids] properties: collected_invoice_ids: type: array minItems: 1 maxItems: 200 items: type: integer minimum: 1 responses: '200': description: Bulk comparison completed content: application/json: schema: $ref: '#/components/schemas/CollectedInvoiceEconomicV2CompareBulkResponse' '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '500': { $ref: '#/components/responses/InternalServerError' } /collected-invoices/economic/v2/revenue-statistics: get: tags: - Invoices summary: Get overall booked revenue statistics from e-conomic (V2) description: | Aggregates booked e-conomic revenue across invoices and lines, with optional filters for date range, customer(s), department(s), currency, and barred-customer status. operationId: getCollectedInvoiceEconomicV2RevenueStatistics parameters: - name: dateFrom in: query required: false description: Start date (inclusive), defaults to first day of current month. schema: type: string format: date - name: dateTo in: query required: false description: End date (inclusive), defaults to today. schema: type: string format: date - name: customer_numbers in: query required: false description: Comma-separated customer numbers to include. schema: type: string example: "42493959,42493960" - name: department_numbers in: query required: false description: Comma-separated department numbers to include. schema: type: string example: "75,10" - name: currency in: query required: false description: Restrict to a specific invoice currency. schema: type: string example: "DKK" - name: barred in: query required: false description: Filter by e-conomic customer barred status. schema: type: string enum: [all, barred, active] default: all - name: max_pages in: query required: false description: Safety cap for paginated e-conomic reads. schema: type: integer minimum: 1 maximum: 200 default: 10 responses: '200': description: Revenue statistics resolved successfully content: application/json: schema: $ref: '#/components/schemas/CollectedInvoiceEconomicV2RevenueStatisticsResponse' '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '500': { $ref: '#/components/responses/InternalServerError' } /superuser/invoicing/period: get: tags: - Invoices summary: Get invoicing periods description: Retrieve invoicing periods for superusers operationId: getInvoicingPeriods parameters: - name: dateFrom in: query required: true schema: {type: string, format: date} - name: dateTo in: query required: true schema: {type: string, format: date} responses: '200': description: Invoicing periods retrieved successfully content: application/json: schema: $ref: '#/components/schemas/InvoicingPeriodResponseEnvelope' /superuser/invoicing/period/distribution/fixed-pricing: get: tags: - Invoices summary: Get fixed pricing distribution description: Get invoicing distribution for fixed pricing items operationId: getInvoicingFixedPricingDistribution parameters: - name: dateFrom in: query required: true schema: {type: string, format: date} - name: dateTo in: query required: true schema: {type: string, format: date} responses: '200': description: Fixed pricing distribution retrieved successfully content: application/json: schema: $ref: '#/components/schemas/InvoicingFixedPricingDistributionResponse' /superuser/invoicing/period/distribution/wash-subscriptions: get: tags: - Invoices summary: Get wash subscriptions distribution description: Get invoicing distribution for wash subscriptions operationId: getInvoicingWashSubscriptionsDistribution parameters: - name: dateFrom in: query required: true schema: {type: string, format: date} - name: dateTo in: query required: true schema: {type: string, format: date} responses: '200': description: Wash subscriptions distribution retrieved successfully content: application/json: schema: $ref: '#/components/schemas/InvoicingWashSubscriptionsDistributionResponse' /superuser/invoicing/period/distribution/v2/all: get: tags: - Invoices summary: Get version-aware historical distribution (all) operationId: getInvoicingPeriodDistributionV2All parameters: - name: dateFrom in: query required: true schema: { type: string, format: date } - name: dateTo in: query required: true schema: { type: string, format: date } responses: '200': description: Version-aware historical distribution (all categories) content: application/json: schema: $ref: '#/components/schemas/InvoicingDistributionV2AllResponse' '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '500': { $ref: '#/components/responses/InternalServerError' } /superuser/invoicing/period/distribution/v2/fixed-pricing: get: tags: - Invoices summary: Get version-aware historical fixed pricing distribution operationId: getInvoicingPeriodDistributionV2FixedPricing parameters: - name: dateFrom in: query required: true schema: { type: string, format: date } - name: dateTo in: query required: true schema: { type: string, format: date } responses: '200': description: Version-aware fixed pricing distribution content: application/json: schema: $ref: '#/components/schemas/InvoicingDistributionV2FixedPricingResponse' '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '500': { $ref: '#/components/responses/InternalServerError' } /superuser/invoicing/period/distribution/v2/wash-subscriptions: get: tags: - Invoices summary: Get version-aware historical wash subscription distribution operationId: getInvoicingPeriodDistributionV2WashSubscriptions parameters: - name: dateFrom in: query required: true schema: { type: string, format: date } - name: dateTo in: query required: true schema: { type: string, format: date } responses: '200': description: Version-aware wash subscription distribution content: application/json: schema: $ref: '#/components/schemas/InvoicingDistributionV2WashSubscriptionsResponse' '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '500': { $ref: '#/components/responses/InternalServerError' } /superuser/invoicing/period/distribution/v2/customer-prices: get: tags: - Invoices summary: Get version-aware historical customer-price discount distribution operationId: getInvoicingPeriodDistributionV2CustomerPrices parameters: - name: dateFrom in: query required: true schema: { type: string, format: date } - name: dateTo in: query required: true schema: { type: string, format: date } responses: '200': description: Version-aware customer-price discount distribution content: application/json: schema: $ref: '#/components/schemas/InvoicingDistributionV2CustomerPricesResponse' '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '500': { $ref: '#/components/responses/InternalServerError' } /superuser/invoicing/period/distribution/v2/booked-department-75: get: tags: - Invoices summary: Get booked e-conomic department 75 redistribution operationId: getInvoicingPeriodDistributionV2BookedDepartment75 parameters: - name: dateFrom in: query required: true schema: { type: string, format: date } - name: dateTo in: query required: true schema: { type: string, format: date } responses: '200': description: Actual booked e-conomic department 75 net amounts redistributed to internal departments content: application/json: schema: $ref: '#/components/schemas/InvoicingDistributionV2BookedDepartment75Response' '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '500': { $ref: '#/components/responses/InternalServerError' } /superuser/customers/pricing-history: get: tags: - Invoices summary: Get customer versioned pricing/subscription/discount timeline operationId: getCustomerPricingHistoryV2 parameters: - name: customer_number in: query required: true schema: type: integer minimum: 1 - name: dateFrom in: query required: true schema: { type: string, format: date } - name: dateTo in: query required: true schema: { type: string, format: date } responses: '200': description: Customer timeline resolved content: application/json: schema: $ref: '#/components/schemas/CustomerPricingHistoryResponse' '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '500': { $ref: '#/components/responses/InternalServerError' } /superuser/users/{user_id}/vehicles: get: tags: [Vehicles] summary: List vehicles for a selected superuser user operationId: listSuperuserUserVehicles parameters: - name: user_id in: path required: true schema: { type: integer, minimum: 1 } - $ref: '#/components/parameters/PageParam' - $ref: '#/components/parameters/PerPageParam' responses: '200': description: User-scoped vehicles retrieved content: application/json: schema: type: array items: $ref: '#/components/schemas/Vehicle' '400': { $ref: '#/components/responses/BadRequest' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } post: tags: [Vehicles] summary: Add a vehicle for a selected superuser user operationId: addSuperuserUserVehicle parameters: - name: user_id in: path required: true schema: { type: integer, minimum: 1 } requestBody: required: true content: application/json: schema: type: object required: [reg, type] properties: reg: { type: string, minLength: 2, maxLength: 12 } type: { type: integer } wash_subscription: { type: boolean, default: false } reference: { type: string, nullable: true, maxLength: 255 } customer_id: type: integer description: Optional guard value; must match the selected user's customer number. responses: '200': description: Vehicle created content: application/json: schema: $ref: '#/components/schemas/Vehicle' '400': { $ref: '#/components/responses/BadRequest' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } put: tags: [Vehicles] summary: Edit a vehicle for a selected superuser user operationId: editSuperuserUserVehicle parameters: - name: user_id in: path required: true schema: { type: integer, minimum: 1 } requestBody: required: true content: application/json: schema: type: object required: [id] properties: id: { type: integer, minimum: 1 } reg: { type: string, minLength: 2, maxLength: 12 } type: { type: integer } wash_subscription: { type: boolean } reference: { type: string, nullable: true, maxLength: 255 } customer_id: type: integer description: Optional guard value; moving vehicles between customers is not allowed here. responses: '200': description: Vehicle updated content: application/json: schema: $ref: '#/components/schemas/Vehicle' '400': { $ref: '#/components/responses/BadRequest' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } delete: tags: [Vehicles] summary: Delete a vehicle for a selected superuser user operationId: deleteSuperuserUserVehicle parameters: - name: user_id in: path required: true schema: { type: integer, minimum: 1 } - name: id in: query required: true schema: { type: integer, minimum: 1 } responses: '200': { description: Vehicle deleted } '400': { $ref: '#/components/responses/BadRequest' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } /superuser/users/{user_id}/vehicles/summary: get: tags: [Vehicles] summary: Summarize vehicles for a selected superuser user operationId: summarizeSuperuserUserVehicles parameters: - name: user_id in: path required: true schema: { type: integer, minimum: 1 } responses: '200': description: User-scoped vehicle summary content: application/json: schema: $ref: '#/components/schemas/VehicleManagementSummary' '400': { $ref: '#/components/responses/BadRequest' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } # Vehicles Endpoints /vehicles: get: tags: - Vehicles summary: List vehicles description: | List vehicles or fetch a specific vehicle when `id` is provided. - When `id` is present, returns a single vehicle object (404 if not found). - Otherwise returns a paginated list of vehicles. Permissions: - Own scope: `list_own_vehicles` (linked to subuser node `VEHICLES_LIST`). - Broader scope: `list_vehicles_other`. Subusers may specify header `X-Customer-Number` to target a specific customer. If the broader permission is missing, the list will automatically be restricted to the effective customer context. operationId: listVehicles parameters: - $ref: '#/components/parameters/PageParam' - $ref: '#/components/parameters/PerPageParam' - $ref: '#/components/parameters/XCustomerNumber' - name: id in: query schema: {type: integer} - name: reg in: query schema: {type: string} - name: customer_id in: query schema: {type: integer} responses: '200': description: Vehicle(s) retrieved successfully content: application/json: schema: oneOf: - $ref: '#/components/schemas/Vehicle' - type: array items: $ref: '#/components/schemas/Vehicle' '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } post: tags: - Vehicles summary: Add vehicle operationId: addVehicle description: | Create a new vehicle for a customer. Permissions: - Own scope: `add_vehicle` (linked to subuser node `VEHICLES_ADD`). - Broader scope: `add_vehicle_other`. parameters: - $ref: '#/components/parameters/XCustomerNumber' requestBody: required: true content: application/json: schema: type: object required: [reg, type] properties: reg: type: string minLength: 2 maxLength: 12 description: Vehicle registration number type: type: integer description: Product ID representing the vehicle wash type wash_subscription: type: boolean default: false description: Defaults to false when omitted. reference: type: string maxLength: 255 nullable: true customer_id: type: integer description: Optional explicit target customer. Defaults to the effective customer context. responses: '200': description: Vehicle created content: application/json: schema: {} '400': { $ref: '#/components/responses/BadRequest' } '403': { $ref: '#/components/responses/Forbidden' } put: tags: - Vehicles summary: Edit vehicle operationId: editVehicle description: | Update fields on an existing vehicle. Permissions: - Own scope: `edit_vehicle` (linked to subuser node `VEHICLES_EDIT`). - Broader scope: `edit_vehicle_other`. parameters: - $ref: '#/components/parameters/XCustomerNumber' requestBody: required: true content: application/json: schema: type: object required: [id] properties: id: {type: integer} reg: type: string minLength: 2 maxLength: 12 type: {type: integer} wash_subscription: {type: boolean} reference: type: string maxLength: 255 nullable: true responses: '200': description: Vehicle updated content: application/json: schema: {} '400': { $ref: '#/components/responses/BadRequest' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } delete: tags: - Vehicles summary: Delete vehicle operationId: deleteVehicle description: | Delete an existing vehicle. Permissions: - Own scope: `delete_vehicle` (linked to subuser node `VEHICLES_DELETE`). - Broader scope: `delete_vehicle_other`. parameters: - $ref: '#/components/parameters/XCustomerNumber' - name: id in: query required: true schema: {type: integer} responses: '200': description: Vehicle deleted content: application/json: schema: {} '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } /vehicles/addons/available: get: tags: - Vehicles summary: Get available vehicle addons description: | Get list of available addons for a vehicle. Permissions: - Own scope: `list_vehicle_addon_own` (linked to subuser node `VEHICLES_LIST`). - Broader scope: `list_vehicles_addon_other`. operationId: getAvailableVehicleAddons parameters: - $ref: '#/components/parameters/XCustomerNumber' - name: id in: query required: true schema: type: integer responses: '200': description: Available addons retrieved successfully content: application/json: schema: {} '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } /vehicles/addons/toggle: post: tags: - Vehicles summary: Toggle vehicle addon description: | Enable or disable a vehicle addon for a vehicle. Permissions: - Own scope: `toggle_vehicle_addon_own` (linked to subuser node `VEHICLES_EDIT`). - Broader scope: `toggle_vehicle_addon_other`. operationId: toggleVehicleAddon parameters: - $ref: '#/components/parameters/XCustomerNumber' requestBody: required: true content: application/json: schema: type: object required: [vehicle_id, addon_id] properties: vehicle_id: type: integer addon_id: type: integer responses: '200': description: Vehicle addon toggled successfully content: application/json: schema: {} '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } /department/vehicles/unknown-customer: get: tags: - Vehicles summary: Get unknown customer vehicles in department operationId: getUnknownCustomerVehicles responses: '200': description: Success content: application/json: schema: {} /department/vehicle/customer-suggestions: get: tags: - Vehicles summary: Get vehicle customer suggestions operationId: getVehicleCustomerSuggestions parameters: - name: reg in: query required: true schema: {type: string} responses: '200': description: Success content: application/json: schema: {} /vehicles/set-auto-start-on-lpr: post: tags: - Vehicles summary: Set auto start on LPR operationId: setVehicleAutoStartOnLpr description: | Enable or disable automatic start on LPR for a vehicle in XL Vask. Permissions: - Own scope: `set_auto_start_on_lpr` (linked to subuser node `VEHICLES_EDIT`). - Broader scope: `set_auto_start_on_lpr_other`. parameters: - $ref: '#/components/parameters/XCustomerNumber' requestBody: required: true content: application/json: schema: type: object required: [id, active] properties: id: {type: integer} active: {type: boolean} responses: '200': description: Success content: application/json: schema: {} '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } /vehicles/set-vehicle-type-id: post: tags: - Vehicles summary: Set vehicle type ID operationId: setVehicleTypeId description: | Set or change the XL Vask `vehicleTypeId` for a vehicle. Permissions: - Own scope: `set_vehicle_type_id` (linked to subuser node `VEHICLES_EDIT`). - Broader scope: `set_vehicle_type_id_other`. parameters: - $ref: '#/components/parameters/XCustomerNumber' requestBody: required: true content: application/json: schema: type: object required: [id, vehicleTypeId] properties: id: {type: integer} vehicleTypeId: type: string minLength: 1 maxLength: 50 responses: '200': description: Success content: application/json: schema: {} '400': { $ref: '#/components/responses/BadRequest' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } /superuser/users-with-vehicle-subscriptions: get: tags: - Vehicles summary: Get users with vehicle subscriptions operationId: getUsersWithVehicleSubscriptions responses: '200': description: Success content: application/json: schema: {} /vehicles/status: get: tags: - Vehicles summary: Get vehicle status operationId: getVehicleStatus parameters: - name: reg in: query required: true schema: {type: string} responses: '200': description: Success content: application/json: schema: {} /vehicles/search: get: tags: - Vehicles summary: Search vehicles operationId: searchVehicles parameters: - name: search in: query required: true schema: {type: string} responses: '200': description: Success content: application/json: schema: {} # Notifications Endpoints /notifications: get: tags: - Notifications summary: List notifications description: Get list of notifications operationId: listNotifications parameters: - $ref: '#/components/parameters/PageParam' - $ref: '#/components/parameters/PerPageParam' responses: '200': description: Notifications retrieved successfully content: application/json: schema: type: array items: $ref: '#/components/schemas/Notification' post: tags: - Notifications summary: Create notification description: Create a new notification operationId: createNotification requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/NotificationCreate' responses: '201': description: Notification created successfully content: application/json: schema: {} delete: tags: - Notifications summary: Delete notification description: Delete a notification operationId: deleteNotification parameters: - name: id in: query required: true schema: type: integer responses: '200': description: Notification deleted successfully content: application/json: schema: {} # Statistics Endpoints /statistics/bookings/new: get: tags: - Statistics summary: Get new bookings statistics description: Get statistics for new bookings operationId: getNewBookingsStats responses: '200': description: New bookings statistics retrieved successfully content: application/json: schema: {} /orders/module/stripe/payment_intent: get: tags: - Orders summary: Get Stripe payment intent operationId: getStripePaymentIntent parameters: - name: id in: query required: true schema: {type: integer} responses: '200': description: Success content: application/json: schema: {} post: tags: - Orders summary: Create Stripe payment intent description: Creates a Stripe Terminal card payment intent with fixed 25% moms. operationId: createStripePaymentIntent requestBody: required: true content: application/json: schema: type: object required: [id, reader] properties: id: {type: integer} reader: {type: string} responses: '200': description: Success content: application/json: schema: {} delete: tags: - Orders summary: Delete Stripe payment intent operationId: deleteStripePaymentIntent parameters: - name: id in: query required: true schema: {type: integer} responses: '200': description: Success content: application/json: schema: {} /orders/module/stripe/payment_intent/capture: post: tags: - Orders summary: Capture Stripe payment intent operationId: captureStripePaymentIntent requestBody: required: true content: application/json: schema: type: object required: [id] properties: id: {type: integer} responses: '200': description: Success content: application/json: schema: {} /orders/module/stripe/debug/simulate_payment: post: tags: - Orders summary: Simulate Stripe payment operationId: simulateStripePayment requestBody: required: true content: application/json: schema: type: object required: [id] properties: id: {type: integer} responses: '200': description: Success content: application/json: schema: {} /statistics/orders/new: get: tags: - Statistics summary: Get new orders statistics description: Get statistics for new orders operationId: getNewOrdersStats responses: '200': description: New orders statistics retrieved successfully content: application/json: schema: {} /statistics/income/today: get: tags: - Statistics summary: Get today's income description: Get income statistics for today operationId: getTodayIncome responses: '200': description: Today's income statistics retrieved successfully content: application/json: schema: {} /statistics/income/yesterday: get: tags: - Statistics summary: Get yesterday's income description: Get income statistics for yesterday operationId: getYesterdayIncome responses: '200': description: Yesterday's income statistics retrieved successfully content: application/json: schema: {} /statistics/income/this-month: get: tags: - Statistics summary: Get this month's income description: Get income statistics for the current month operationId: getThisMonthIncome responses: '200': description: This month's income statistics retrieved successfully content: application/json: schema: {} /statistics/income/last-month: get: tags: - Statistics summary: Get last month's income description: Get income statistics for the previous month operationId: getLastMonthIncome responses: '200': description: Last month's income statistics retrieved successfully content: application/json: schema: {} /statistics/income/this-year: get: tags: - Statistics summary: Get this year's income description: Get income statistics for the current year operationId: getThisYearIncome responses: '200': description: This year's income statistics retrieved successfully content: application/json: schema: {} /statistics/income/departments: get: tags: - Statistics summary: Get total income today by departments operationId: getTotalIncomeTodayByDepartments responses: '200': description: Success content: application/json: schema: {} /statistics/economic/totals: get: tags: - Statistics summary: Get total economic statistics operationId: getEconomicTotals responses: '200': description: Success content: application/json: schema: {} /statistics/economic/totals/department_sent_invoice_totals: get: tags: - Statistics summary: Get department sent invoice totals operationId: getDepartmentSentInvoiceTotals responses: '200': description: Success content: application/json: schema: {} /statistics/economic/totals/department_draft_invoice_totals: get: tags: - Statistics summary: Get department draft invoice totals operationId: getDepartmentDraftInvoiceTotals responses: '200': description: Success content: application/json: schema: {} # Worker Endpoints /worker/version: get: tags: - Worker summary: Get worker version description: Get the current version of the system worker operationId: getWorkerVersion responses: '200': description: Worker version retrieved successfully content: application/json: schema: {} /worker/update-version: get: tags: - Worker summary: Update worker version description: Set the target version for the worker update operationId: updateWorkerVersion parameters: - name: version in: query required: true schema: type: string responses: '200': description: Version update target set successfully content: application/json: schema: {} /worker/status: get: tags: - Worker summary: Get worker status description: Get detailed status of the system worker operationId: getWorkerStatus responses: '200': description: Worker status retrieved successfully content: application/json: schema: type: object properties: data: type: object properties: api_commit_sha: type: string description: Running API commit SHA, or unknown when unavailable. /worker/debug: get: tags: - Worker summary: Debug worker description: Execute debug commands on the worker (often restricted) operationId: debugWorker responses: '200': description: Debug information retrieved successfully content: application/json: schema: {} '403': $ref: '#/components/responses/Forbidden' /worker/debug/on: get: tags: - Worker summary: Enable worker debug operationId: enableWorkerDebug responses: '200': description: Worker debug enabled content: application/json: schema: {} '403': $ref: '#/components/responses/Forbidden' /worker/debug/off: get: tags: - Worker summary: Disable worker debug operationId: disableWorkerDebug responses: '200': description: Worker debug disabled content: application/json: schema: {} '403': $ref: '#/components/responses/Forbidden' /worker/licenseplates: get: tags: - Worker summary: Get unique license plates description: Fetch all unique license plates from various database tables operationId: getWorkerLicensePlates responses: '200': description: License plates retrieved successfully content: application/json: schema: {} '403': $ref: '#/components/responses/Forbidden' /economic/doesCustomerExist: get: tags: - Modules summary: Check if customer exists in e-conomic operationId: checkEconomicCustomerExists parameters: - name: cvr in: query required: true schema: type: string responses: '200': description: Customer check completed content: application/json: schema: {} '404': $ref: '#/components/responses/NotFound' /cvr/lookup: get: tags: - Modules summary: Lookup CVR information description: Get detailed information for a CVR number operationId: lookupCvr parameters: - name: cvr in: query required: true schema: type: string responses: '200': description: CVR information retrieved successfully content: application/json: schema: {} /cvr/search: get: tags: - Modules summary: Search CVR description: Search for companies by name or CVR operationId: searchCvr parameters: - name: query in: query required: true schema: type: string minLength: 2 responses: '200': description: Search results retrieved successfully content: application/json: schema: {} # Plate Scans Endpoints /numberplatescans: get: tags: - Plate Scans summary: List plate scans description: Get a list of license plate scans operationId: listPlateScans parameters: - $ref: '#/components/parameters/PageParam' - $ref: '#/components/parameters/PerPageParam' - $ref: '#/components/parameters/SearchParam' responses: '200': description: Plate scans retrieved successfully content: application/json: schema: {} post: tags: - Plate Scans summary: Record plate scan description: Record a new license plate scan operationId: recordPlateScan requestBody: required: true content: application/json: schema: type: object required: - plate - lane_id properties: plate: type: string lane_id: type: integer responses: '201': description: Plate scan recorded successfully content: application/json: schema: {} /numberplatescans/department: post: tags: - Plate Scans summary: Record plate scan for department operationId: recordDepartmentPlateScan requestBody: required: true content: application/json: schema: type: object required: - plate - department_id properties: plate: type: string department_id: type: integer responses: '201': description: Plate scan recorded successfully content: application/json: schema: {} /numberplatescans/post: get: tags: - Plate Scans summary: Get post-scan results operationId: getPlateScanPostResults responses: '200': description: Post-scan results retrieved successfully content: application/json: schema: {} /numberplatescanners: get: tags: - Plate Scans summary: List plate scanners description: Get a list of all number plate scanners operationId: listPlateScanners parameters: - $ref: '#/components/parameters/PageParam' - $ref: '#/components/parameters/PerPageParam' - $ref: '#/components/parameters/SearchParam' responses: '200': description: Plate scanners retrieved successfully content: application/json: schema: {} post: tags: - Plate Scans summary: Add plate scanner operationId: addPlateScanner requestBody: required: true content: application/json: schema: type: object required: [department_id, name, notes] properties: department_id: {type: integer} name: {type: string} notes: {type: string} responses: '201': description: Plate scanner added successfully content: application/json: schema: {} put: tags: - Plate Scans summary: Update plate scanner operationId: updatePlateScanner requestBody: required: true content: application/json: schema: type: object required: [id, department_id, name, notes] properties: id: {type: integer} department_id: {type: integer} name: {type: string} notes: {type: string} responses: '200': description: Plate scanner updated successfully content: application/json: schema: {} /department/numberplatescanners: get: tags: - Plate Scans summary: List department plate scanners operationId: listDepartmentPlateScanners parameters: - name: id in: query required: true schema: type: integer responses: '200': description: Department plate scanners retrieved successfully content: application/json: schema: {} /relay/button/press/post: get: tags: - Plate Scans summary: Record machine start button press webhook operationId: addButtonPress parameters: - name: token in: query required: false schema: {type: string} - name: lane_id in: query required: false schema: type: integer - name: reg in: query required: false schema: type: string responses: '201': description: Button press recorded and linked to a self-serve wash session content: application/json: schema: $ref: '#/components/schemas/MachineButtonPressWebhookResponse' '404': $ref: '#/components/responses/NotFound' post: tags: - Plate Scans summary: Record machine start button press webhook operationId: addButtonPressPost requestBody: required: false content: application/json: schema: type: object properties: token: type: string lane_id: type: integer reg: type: string responses: '201': description: Button press recorded and linked to a self-serve wash session content: application/json: schema: $ref: '#/components/schemas/MachineButtonPressWebhookResponse' '404': $ref: '#/components/responses/NotFound' /relay/machine/on/post: get: tags: - Plate Scans summary: Record Shelly machine ON signal webhook description: Accepts Shelly Cloud webhook/query parameters for input.toggle_on or switch.on and records the physical machine start signal for self-serve billing. operationId: recordShellyMachineOnSignal parameters: - name: token in: query required: false schema: {type: string} - name: lane_id in: query required: false schema: type: integer - name: relay_id in: query required: false schema: type: string - name: event in: query required: false schema: type: string enum: [input.toggle_on, switch.on] - name: reg in: query required: false schema: type: string responses: '201': description: Machine ON signal recorded and linked to a self-serve wash session content: application/json: schema: $ref: '#/components/schemas/MachineButtonPressWebhookResponse' '202': description: Shelly signal was recognized but ignored '400': $ref: '#/components/responses/BadRequest' post: tags: - Plate Scans summary: Record Shelly machine ON signal webhook description: Accepts Shelly Cloud JSON webhook payloads for input.toggle_on or switch.on and records the physical machine start signal for self-serve billing. operationId: recordShellyMachineOnSignalPost requestBody: required: false content: application/json: schema: type: object properties: token: type: string lane_id: type: integer relay_id: type: string event: type: string enum: [input.toggle_on, switch.on] component: type: string example: input:0 state: type: boolean output: type: boolean reg: type: string responses: '201': description: Machine ON signal recorded and linked to a self-serve wash session content: application/json: schema: $ref: '#/components/schemas/MachineButtonPressWebhookResponse' '202': description: Shelly signal was recognized but ignored '400': $ref: '#/components/responses/BadRequest' # Module - e-conomic Endpoints /economic/customers/import: post: tags: - Modules summary: Import e-conomic customers description: Import customers from e-conomic operationId: importEconomicCustomers requestBody: required: false content: application/json: schema: {} responses: '200': description: Customers imported successfully content: application/json: schema: {} /economic/departments: get: tags: - Modules summary: Get e-conomic departments operationId: getEconomicDepartments responses: '200': description: Success content: application/json: schema: {} /economic/products: get: tags: - Modules summary: Get e-conomic products operationId: getEconomicProducts responses: '200': description: Success content: application/json: schema: {} /modules/economic/customer: get: tags: - Modules summary: Get e-conomic customer details operationId: getEconomicCustomer parameters: - name: customer_number in: query required: true schema: {type: integer} responses: '200': description: Success content: application/json: schema: {} post: tags: - Modules summary: Create e-conomic customer operationId: createEconomicCustomer requestBody: required: true content: application/json: schema: type: object required: [customer_number, cvr, email, phone, name] properties: customer_number: {type: integer} cvr: {type: integer} email: {type: string} phone: {type: integer} name: {type: string} responses: '200': description: Success content: application/json: schema: {} /economic/layouts: get: tags: - Modules summary: Get e-conomic layouts description: Get available invoice layouts from e-conomic operationId: getEconomicLayouts responses: '200': description: Layouts retrieved successfully content: application/json: schema: {} /economic/payment-terms: get: tags: - Modules summary: Get e-conomic payment terms description: Get available payment terms from e-conomic operationId: getEconomicPaymentTerms responses: '200': description: Payment terms retrieved successfully content: application/json: schema: {} /economic/invoice/draft/export: post: tags: - Modules summary: Export draft invoice to e-conomic description: Exports draft invoice using queue processing when available, with synchronous fallback when queue dependencies are unavailable. operationId: queueDraftInvoiceExportToEconomic requestBody: required: true content: application/json: schema: type: object required: [order_id] properties: order_id: type: integer minimum: 1 responses: '200': description: Draft invoice export processed synchronously (fallback) content: application/json: schema: $ref: '#/components/schemas/EconomicTransferSynchronousFallbackResponse' '202': description: Draft invoice export queued content: application/json: schema: $ref: '#/components/schemas/EconomicTransferQueueEnqueueResponse' '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '500': { $ref: '#/components/responses/InternalServerError' } /economic/invoice/draft/export/status: get: tags: - Modules summary: Get queued draft export job status operationId: getDraftInvoiceExportQueueStatus parameters: - name: job_id in: query required: true schema: type: integer minimum: 1 responses: '200': description: Queue job status content: application/json: schema: $ref: '#/components/schemas/EconomicTransferQueueStatusResponse' '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '503': { $ref: '#/components/responses/ServiceUnavailable' } '500': { $ref: '#/components/responses/InternalServerError' } /economic/invoice/draft/export/retry: post: tags: - Modules summary: Retry failed draft export queue job operationId: retryDraftInvoiceExportQueueJob requestBody: required: true content: application/json: schema: type: object required: [job_id] properties: job_id: type: integer minimum: 1 responses: '200': description: Queue job retried content: application/json: schema: $ref: '#/components/schemas/EconomicTransferQueueRetryResponse' '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '503': { $ref: '#/components/responses/ServiceUnavailable' } '500': { $ref: '#/components/responses/InternalServerError' } /economic/invoice/export: post: tags: - Modules summary: Export invoice to e-conomic description: Exports booked invoice using queue processing when available, with synchronous fallback when queue dependencies are unavailable. operationId: queueInvoiceExportToEconomic requestBody: required: true content: application/json: schema: type: object required: [order_id] properties: order_id: type: integer minimum: 1 responses: '200': description: Invoice export processed synchronously (fallback) content: application/json: schema: $ref: '#/components/schemas/EconomicTransferSynchronousFallbackResponse' '202': description: Invoice export queued content: application/json: schema: $ref: '#/components/schemas/EconomicTransferQueueEnqueueResponse' '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '500': { $ref: '#/components/responses/InternalServerError' } /economic/invoice/export/status: get: tags: - Modules summary: Get queued invoice export job status operationId: getInvoiceExportQueueStatus parameters: - name: job_id in: query required: true schema: type: integer minimum: 1 responses: '200': description: Queue job status content: application/json: schema: $ref: '#/components/schemas/EconomicTransferQueueStatusResponse' '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '503': { $ref: '#/components/responses/ServiceUnavailable' } '500': { $ref: '#/components/responses/InternalServerError' } /economic/invoice/export/retry: post: tags: - Modules summary: Retry failed invoice export queue job operationId: retryInvoiceExportQueueJob requestBody: required: true content: application/json: schema: type: object required: [job_id] properties: job_id: type: integer minimum: 1 responses: '200': description: Queue job retried content: application/json: schema: $ref: '#/components/schemas/EconomicTransferQueueRetryResponse' '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '503': { $ref: '#/components/responses/ServiceUnavailable' } '500': { $ref: '#/components/responses/InternalServerError' } # Module - Stripe Endpoints /modules/stripe/customers: get: tags: - Modules summary: List Stripe customers description: Get list of Stripe customers operationId: listStripeCustomers responses: '200': description: Stripe customers retrieved successfully content: application/json: schema: {} /modules/stripe/products: get: tags: - Modules summary: List Stripe products description: Get list of Stripe products operationId: listStripeProducts responses: '200': description: Stripe products retrieved successfully content: application/json: schema: {} /modules/stripe/prices: get: tags: - Modules summary: List Stripe prices description: Get list of Stripe prices operationId: listStripePrices responses: '200': description: Stripe prices retrieved successfully content: application/json: schema: {} /modules/stripe/invoice: post: tags: - Modules summary: Create Stripe invoice description: Create an invoice in Stripe operationId: createStripeInvoice requestBody: required: false content: application/json: schema: {} responses: '201': description: Stripe invoice created successfully content: application/json: schema: {} /modules/stripe/terminal/readers: get: tags: - Modules summary: List Stripe terminal readers operationId: listStripeTerminalReaders responses: '200': description: Success content: application/json: schema: {} /modules/stripe/terminal/locations: get: tags: - Modules summary: List Stripe terminal locations operationId: listStripeTerminalLocations responses: '200': description: Success content: application/json: schema: {} /modules/stripe/department/terminal/location: get: tags: - Modules summary: Get department terminal location operationId: getDepartmentTerminalLocation parameters: - name: id in: query required: true schema: {type: integer} responses: '200': description: Success content: application/json: schema: {} post: tags: - Modules summary: Set department terminal location operationId: setDepartmentTerminalLocation requestBody: required: true content: application/json: schema: type: object required: [id, location] properties: id: {type: integer} location: {type: string} responses: '200': description: Success content: application/json: schema: {} /modules/stripe/department/terminal/readers: get: tags: - Modules summary: Get department terminal readers operationId: getDepartmentTerminalReaders parameters: - name: id in: query required: true schema: {type: integer} responses: '200': description: Success content: application/json: schema: {} # Module - Backup Endpoints /modules/backup/backups: get: tags: - Modules summary: List backup modules operationId: listBackupModules responses: '200': description: Success content: application/json: schema: {} post: tags: - Modules summary: Create backup module operationId: createBackupModule requestBody: required: true content: application/json: schema: type: object required: [name, description] properties: name: {type: string} description: {type: string} responses: '200': description: Success content: application/json: schema: {} # Module - XLVask Endpoints /modules/xlvask/usageLog: get: tags: - Modules summary: Get XLVask usage logs description: Retrieve usage logs from XLVask system operationId: getXlvaskUsageLogs parameters: - $ref: '#/components/parameters/PageParam' - $ref: '#/components/parameters/PerPageParam' responses: '200': description: Usage logs retrieved successfully content: application/json: schema: {} /modules/xlvask/vehicles: get: tags: - Modules summary: List XLVask vehicles description: Get list of vehicles from XLVask operationId: listXlvaskVehicles responses: '200': description: XLVask vehicles retrieved successfully content: application/json: schema: {} /modules/xlvask/customers: get: tags: - Modules summary: List XLVask customers description: Get list of customers from XLVask operationId: listXlvaskCustomers responses: '200': description: XLVask customers retrieved successfully content: application/json: schema: {} /modules/action-logs: get: tags: - Modules summary: List module action logs description: Retrieve a paginated list of module action logs with searching and filtering operationId: listModuleActionLogs parameters: - $ref: '#/components/parameters/PageParam' - $ref: '#/components/parameters/LimitParam' - $ref: '#/components/parameters/SearchParam' - $ref: '#/components/parameters/FiltersParam' responses: '200': description: Module action logs retrieved successfully content: application/json: schema: type: array items: $ref: '#/components/schemas/ModuleActionLog' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' # Module - Self-Serve Endpoints /modules/self-serve/lane/status: get: tags: - Modules summary: Get self-serve lane status description: Retrieve the current status of a self-serve lane operationId: getSelfServeLaneStatus parameters: - name: lane_id in: query required: true schema: type: integer responses: '200': description: Lane status retrieved successfully content: application/json: schema: $ref: '#/components/schemas/SelfServeLaneStatus' /modules/self-serve/lane/wash/in-progress: get: tags: - Modules summary: Get in-progress self-serve wash customer and vehicle details description: | Returns the current open self-serve wash session details for a lane (if any), including resolved customer and vehicle details. operationId: getSelfServeLaneWashInProgress parameters: - name: lane_id in: query required: true schema: type: integer responses: '200': description: In-progress wash details resolved content: application/json: schema: type: object properties: lane_id: type: integer in_progress: type: boolean session: type: object nullable: true properties: id: type: integer status: type: string reg: type: string customer_number: type: integer nullable: true vehicle_id: type: integer nullable: true vehicle_type_id: type: integer nullable: true included_minutes: type: integer nullable: true machine_type_id: type: integer nullable: true machine_relay_enabled: type: boolean machine_relay_enabled_at: type: string nullable: true machine_start_triggered: type: boolean machine_start_triggered_at: type: string nullable: true wash_started_at: type: string nullable: true created_at: type: string updated_at: type: string nullable: true customer: type: object nullable: true properties: id: type: integer nullable: true customer_number: type: integer nullable: true display_name: type: string nullable: true email: type: string nullable: true phone_country_code: type: integer nullable: true phone: type: string nullable: true vehicle: type: object nullable: true properties: id: type: integer customer_id: type: integer type: type: integer reg: type: string reference: type: string nullable: true '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' /modules/self-serve/lane/wash/my-active-wash: get: tags: - Modules summary: Get the authenticated customer's active self-serve wash description: | Returns the latest active self-serve wash for the authenticated customer, without requiring the frontend to know or poll a lane id. operationId: getMyActiveSelfServeWash responses: '200': description: Active self-serve wash details resolved content: application/json: schema: type: object properties: lane_id: type: integer status: type: string in_progress: type: boolean elapsed_minutes: type: integer session: type: object nullable: true customer: type: object nullable: true vehicle: type: object nullable: true '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' /modules/self-serve/sessions: get: tags: - Modules summary: List self-serve wash sessions description: Retrieve paginated self-serve wash sessions with search, filters, ordering, and active/open-only support. operationId: listSelfServeSessions parameters: - $ref: '#/components/parameters/PageParam' - $ref: '#/components/parameters/LimitParam' - $ref: '#/components/parameters/SearchParam' - $ref: '#/components/parameters/FiltersParam' - name: order in: query required: false schema: type: string example: id:DESC - name: open_only in: query required: false schema: type: boolean responses: '200': description: Self-serve wash sessions retrieved successfully content: application/json: schema: type: array items: allOf: - $ref: '#/components/schemas/SelfserveWashSession' - type: object properties: elapsed_minutes: type: integer open: type: boolean '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' /modules/self-serve/sessions/{id}: get: tags: - Modules summary: Get self-serve wash session detail operationId: getSelfServeSessionDetail parameters: - name: id in: path required: true schema: type: integer responses: '200': description: Self-serve wash session detail retrieved successfully content: application/json: schema: $ref: '#/components/schemas/SelfserveWashSummary' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' /modules/self-serve/lane/force/stop: post: tags: - Modules summary: Force stop a self-serve wash session description: | Clears the current self-serve wash session and lane runtime with RESET behavior only. This administrative action does not signal relays or gates. When billing is requested, only elapsed-minute billing is attempted before runtime is cleared. operationId: forceStopSelfServeLane requestBody: required: true content: application/json: schema: type: object required: - lane_id - bill properties: lane_id: type: integer session_id: type: integer nullable: true bill: type: boolean reason: type: string nullable: true responses: '200': description: Self-serve wash force stopped successfully content: application/json: schema: $ref: '#/components/schemas/SelfserveForceStopResponse' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '409': $ref: '#/components/responses/Conflict' /modules/self-serve/lane/command: post: tags: - Modules summary: Send self-serve lane command description: | Send a command (e.g., start, stop, reset) to a self-serve lane. Property gate commands (`OPEN_PROPERTY_ACCESS_GATE`, `OPEN_PROPERTY_EXIT_GATE`) are also supported here. Property gate commands require the matching explicit command permissions. Operator callers require the base command permission plus the command-specific permission. Authenticated customers with `list_own_department_selfserve_vehicle_conditions` may send `START` on enabled self-serve lanes. Customer `STOP` requires the customer's active self-serve wash in the target department. operationId: sendSelfServeLaneCommand requestBody: required: true content: application/json: schema: type: object required: - lane_id - command properties: lane_id: type: integer command: type: string enum: [START, STOP, RESET, RESERVE, RELEASE, OPEN_PROPERTY_ACCESS_GATE, OPEN_PROPERTY_EXIT_GATE] license_plate: type: string description: Required for START command wash_type: type: string enum: [Manual, Machine] description: Optional customer-selected wash type for START. When provided, Manual and Machine start actions use this explicit choice instead of inferring mode from allowed services. wash_mode: type: string enum: [manual, machine] description: Lowercase alias for wash_type accepted by backend clients. customer_number: type: integer description: Required for START and RESERVE commands. The authenticated customer's number is applied server-side when omitted by user clients. defer_relay_side_effects: type: boolean default: false description: For START, open the entrance gate as part of the command but defer cleaner and machine relay activation to explicit relay endpoints. responses: '200': description: Command sent successfully content: application/json: schema: $ref: '#/components/schemas/SelfServeLaneStatus' '400': description: Command execution failed content: application/json: schema: type: object properties: message: type: string example: 'Failed to execute command: Failed to open property access gate.' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' /modules/self-serve/lane/services/allowed: post: tags: - Modules summary: Set allowed services for a lane based on shown tasks description: | Updates the set of services that are allowed to be manually activated for a given self-serve lane, derived from the tasks currently shown to the user after answering the self-serve questions. This endpoint does not activate anything by itself; it only sets what is allowed to be activated. Operator callers require `modules_selfserve_lane_services_set_allowed`; authenticated customers with `list_own_department_selfserve_vehicle_conditions` may update their enabled self-serve lane before confirming a wash start. operationId: setSelfServeLaneAllowedServices requestBody: required: true content: application/json: schema: type: object required: - lane_id properties: lane_id: type: integer task_ids: type: array description: List of task IDs that are currently shown to the user items: type: integer responses: '200': description: Allowed services updated content: application/json: schema: type: object properties: lane_id: type: integer allowed_services: type: array items: type: string '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' /modules/self-serve/lane/gate/open: post: tags: - Modules summary: Open a self-serve lane gate description: | Opens either the ENTRANCE or EXIT gate relay for a self-serve lane. Failures return a sanitized gate-specific message. operationId: openSelfServeLaneGate requestBody: required: true content: application/json: schema: type: object required: - lane_id - gate properties: lane_id: type: integer gate: type: string enum: [ENTRANCE, EXIT] responses: '200': description: Lane gate opened content: application/json: schema: type: object properties: lane_id: type: integer gate: type: string enum: [ENTRANCE, EXIT] opened: type: boolean state: type: string '400': description: Gate open failed content: application/json: schema: type: object properties: message: type: string example: Failed to open entrance gate. '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' /modules/self-serve/lane/relay/machine/status: get: tags: - Modules summary: Get MACHINE relay status for a lane description: | Reads the current Shelly MACHINE relay status (`on`/`off`) for the given lane. operationId: getSelfServeLaneMachineRelayStatus parameters: - name: lane_id in: query required: true schema: type: integer responses: '200': description: MACHINE relay status retrieved content: application/json: schema: $ref: '#/components/schemas/SelfServeLaneMachineRelayStatus' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' /modules/self-serve/lane/relay/machine_program_picker/status: get: tags: - Modules summary: Get MACHINE_PROGRAM_PICKER relay status for a lane description: | Reads the current Shelly MACHINE_PROGRAM_PICKER relay status (`on`/`off`) for the given lane. operationId: getSelfServeLaneMachineProgramPickerRelayStatus parameters: - name: lane_id in: query required: true schema: type: integer responses: '200': description: MACHINE_PROGRAM_PICKER relay status retrieved content: application/json: schema: $ref: '#/components/schemas/SelfServeLaneMachineRelayStatus' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' /modules/self-serve/lane/relay/machine_program_picker/set: post: tags: - Modules summary: Set MACHINE_PROGRAM_PICKER relay status for a lane description: | Sets the Shelly MACHINE_PROGRAM_PICKER relay state for the lane to on or off and returns the latest status. operationId: setSelfServeLaneMachineProgramPickerRelayStatus requestBody: required: true content: application/json: schema: type: object required: - lane_id - on properties: lane_id: type: integer on: type: boolean responses: '200': description: MACHINE_PROGRAM_PICKER relay status updated content: application/json: schema: type: object properties: lane_id: type: integer relay: type: string enum: [MACHINE_PROGRAM_PICKER] requested_on: type: boolean relay_id: type: string online: type: boolean on: type: boolean '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' /modules/self-serve/lane/relay/machine_cleaner/status: get: tags: - Modules summary: Get MACHINE_CLEANER relay status for a lane description: | Reads the current Shelly MACHINE_CLEANER relay status (`on`/`off`) for the given lane. operationId: getSelfServeLaneMachineCleanerRelayStatus parameters: - name: lane_id in: query required: true schema: type: integer responses: '200': description: MACHINE_CLEANER relay status retrieved content: application/json: schema: $ref: '#/components/schemas/SelfServeLaneMachineRelayStatus' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' /modules/self-serve/lane/relay/machine_cleaner/set: post: tags: - Modules summary: Set MACHINE_CLEANER relay status for a lane description: | Sets the Shelly MACHINE_CLEANER relay state for the lane to on or off and returns the latest status. operationId: setSelfServeLaneMachineCleanerRelayStatus requestBody: required: true content: application/json: schema: type: object required: - lane_id - on properties: lane_id: type: integer on: type: boolean responses: '200': description: MACHINE_CLEANER relay status updated content: application/json: schema: type: object properties: lane_id: type: integer relay: type: string enum: [MACHINE_CLEANER] requested_on: type: boolean relay_id: type: string online: type: boolean on: type: boolean '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' /modules/self-serve/lane/relay/machine/set: post: tags: - Modules summary: Set MACHINE relay status for a lane description: | Sets the Shelly MACHINE relay state for the lane to on or off and returns the latest status. operationId: setSelfServeLaneMachineRelayStatus requestBody: required: true content: application/json: schema: type: object required: - lane_id - on properties: lane_id: type: integer on: type: boolean responses: '200': description: MACHINE relay status updated content: application/json: schema: type: object properties: lane_id: type: integer relay: type: string enum: [MACHINE] requested_on: type: boolean relay_id: type: string online: type: boolean on: type: boolean '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' /modules/self-serve/lane/relay/machine/enable: post: tags: - Modules summary: Manually enable MACHINE relay for a lane description: | Manually turns on the MACHINE relay for a self-serve lane if and only if the current allowed services include `MACHINE` (set via `/modules/self-serve/lane/services/allowed`). The relay is never automatically enabled; an explicit call to this endpoint is required. Operator callers require `modules_selfserve_lane_relay_enable_machine`; authenticated customers with `list_own_department_selfserve_vehicle_conditions` may enable it only for their active self-serve wash. operationId: enableSelfServeLaneMachineRelay requestBody: required: true content: application/json: schema: type: object required: - lane_id properties: lane_id: type: integer duration: type: integer description: Optional number of seconds after which the relay should automatically turn off responses: '200': description: MACHINE relay enabled content: application/json: schema: type: object properties: lane_id: type: integer relay: type: string enabled: type: boolean duration: type: integer nullable: true '401': $ref: '#/components/responses/Unauthorized' '403': description: Not allowed to enable MACHINE relay (no matching task currently shown) content: application/json: schema: $ref: '#/components/schemas/Error' /modules/self-serve/lane/force/machine/enable: post: tags: - Modules summary: Force enable MACHINE relay and mark lane as in-wash (superusers only) description: | Superuser/emergency endpoint. Bypasses the allowed services gating and directly turns on the MACHINE relay. Also ensures the lane is marked as OCCUPIED and IN_WASH with a wash start timestamp if not already set. operationId: forceEnableSelfServeLaneMachine requestBody: required: true content: application/json: schema: type: object required: - lane_id properties: lane_id: type: integer duration: type: integer nullable: true description: Optional number of seconds after which the relay should automatically turn off license_plate: type: string nullable: true description: Optional license plate to associate with the lane responses: '200': description: MACHINE relay force-enabled and lane marked in-wash content: application/json: schema: type: object properties: lane_id: type: integer forced: type: boolean machine: type: string enum: [ENABLED] duration: type: integer nullable: true status: type: string state: type: string wash_start_time: type: integer '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' /modules/self-serve/lane/force/machine/disable: post: tags: - Modules summary: Force disable MACHINE relay but keep lane as in-wash (superusers only) description: | Superuser/emergency endpoint. Turns off the MACHINE relay while ensuring the lane remains in an IN_WASH state (simulating a started wash without machine assistance). operationId: forceDisableSelfServeLaneMachine requestBody: required: true content: application/json: schema: type: object required: - lane_id properties: lane_id: type: integer license_plate: type: string nullable: true responses: '200': description: MACHINE relay force-disabled and lane ensured in-wash content: application/json: schema: type: object properties: lane_id: type: integer forced: type: boolean machine: type: string enum: [DISABLED] status: type: string state: type: string wash_start_time: type: integer '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' # Module - Other Integration Endpoints /modules/motorapi/lookup: get: tags: - Modules summary: Lookup vehicle via MotorAPI description: Look up vehicle information using license plate operationId: motorApiLookup parameters: - name: plate in: query required: true schema: type: string responses: '200': description: Vehicle information retrieved successfully content: application/json: schema: {} /modules/virkdata/search: get: tags: - Modules summary: Search VirkData description: Search for company information in VirkData operationId: virkdataSearch parameters: - name: search in: query required: true schema: type: string responses: '200': description: Company information retrieved successfully content: application/json: schema: {} /modules/fxratesapi/rate: get: tags: - Modules summary: Get exchange rate description: Get current exchange rate operationId: getExchangeRate parameters: - name: from in: query required: true schema: type: string - name: to in: query required: true schema: type: string responses: '200': description: Exchange rate retrieved successfully content: application/json: schema: {} /modules/fxratesapi/rates: get: tags: - Modules summary: Get all exchange rates description: Get all available exchange rates operationId: getAllExchangeRates responses: '200': description: Exchange rates retrieved successfully content: application/json: schema: {} /modules/weatherapi/current: get: tags: - Modules summary: Get current weather description: Get current weather data from WeatherAPI for a location query operationId: weatherApiCurrent parameters: - name: q in: query required: true schema: type: string description: Location query (e.g. city, postal code, or latitude,longitude) responses: '200': description: Current weather retrieved successfully content: application/json: schema: $ref: '#/components/schemas/WeatherApiObjectResponse' /modules/weatherapi/forecast: get: tags: - Modules summary: Get weather forecast description: Get forecast weather data from WeatherAPI operationId: weatherApiForecast parameters: - name: q in: query required: true schema: type: string description: Location query (e.g. city, postal code, or latitude,longitude) - name: days in: query required: false schema: type: integer minimum: 1 maximum: 14 description: Number of forecast days responses: '200': description: Forecast weather retrieved successfully content: application/json: schema: $ref: '#/components/schemas/WeatherApiObjectResponse' /modules/weatherapi/search: get: tags: - Modules summary: Search weather locations description: Search location suggestions from WeatherAPI operationId: weatherApiSearch parameters: - name: q in: query required: true schema: type: string description: Search text responses: '200': description: Location search results retrieved successfully content: application/json: schema: $ref: '#/components/schemas/WeatherApiObjectResponse' /modules/workfeed/employees: get: tags: - Modules summary: List Workfeed employees description: List employees from Workfeed (`GET /companies/{CompanyID}/employees`) operationId: workfeedListEmployees responses: '200': description: Workfeed employees retrieved successfully content: application/json: schema: $ref: '#/components/schemas/WorkfeedEmployeeListResponse' /modules/workfeed/employees/{id}: get: tags: - Modules summary: Get Workfeed employee description: Retrieve a single Workfeed employee by identifier operationId: workfeedGetEmployee parameters: - name: id in: path required: true schema: type: string description: Workfeed employee identifier responses: '200': description: Workfeed employee retrieved successfully content: application/json: schema: $ref: '#/components/schemas/WorkfeedEmployeeSingleResponse' /modules/workfeed/shifts: get: tags: - Modules summary: List Workfeed shifts description: List Workfeed shifts (`GET /companies/{CompanyID}/shifts`) operationId: workfeedListShifts parameters: - name: startFrom in: query required: true schema: type: string format: date-time description: Only return shifts starting on or after this timestamp (ISO 8601) - name: startTo in: query required: true schema: type: string format: date-time description: Only return shifts starting before this timestamp (ISO 8601) - name: employeeID in: query required: false schema: type: string description: Filter shifts by employee ID - name: released in: query required: false schema: type: boolean description: Filter by released/published status responses: '200': description: Workfeed shifts retrieved successfully content: application/json: schema: $ref: '#/components/schemas/WorkfeedShiftListResponse' /modules/workfeed/shifts/{id}: get: tags: - Modules summary: Get Workfeed shift description: Retrieve a single Workfeed shift by identifier operationId: workfeedGetShift parameters: - name: id in: path required: true schema: type: string description: Workfeed shift identifier responses: '200': description: Workfeed shift retrieved successfully content: application/json: schema: $ref: '#/components/schemas/WorkfeedShiftSingleResponse' /modules/workfeed/departments: get: tags: - Modules summary: List Workfeed departments description: List departments from Workfeed (`GET /companies/{CompanyID}/departments`) operationId: workfeedListDepartments responses: '200': description: Workfeed departments retrieved successfully content: application/json: schema: $ref: '#/components/schemas/WorkfeedDepartmentListResponse' /departments/weather: get: tags: - Departments summary: Get department weather timeline description: Returns hourly weather, washes, Workfeed employee-hours, and productivity status aggregated across selected departments (server local time). Default range is start of yesterday (`00:00`) to end of today (`23:00`). Use `date_from` and `date_to` (`YYYY-MM-DD`) together to override the range. If department coordinates are missing/invalid or WeatherAPI cannot resolve the location, weather data falls back silently and timeline slots default to `mostly_clear`. Slots return `unknown` status when they have no evaluable employee-hours or when one or more selected departments are missing department weather targets. operationId: getDepartmentWeatherTimeline parameters: - name: id in: query required: false schema: type: array items: type: integer minimum: 1 minItems: 1 uniqueItems: true style: form explode: true description: Department ID list. Repeat `id` to select multiple departments (`?id=1&id=2`). - name: ids in: query required: false schema: type: string example: '1,2,3' description: Optional CSV alternative for department IDs. Merged with `id` if both are provided. At least one of `id` or `ids` must be provided. - name: date_from in: query required: false schema: type: string format: date example: '2026-03-23' description: Optional range start date (`YYYY-MM-DD`). Must be used together with `date_to`. - name: date_to in: query required: false schema: type: string format: date example: '2026-03-24' description: Optional range end date (`YYYY-MM-DD`, inclusive). Must be used together with `date_from`. responses: '200': description: Department weather timeline retrieved successfully content: application/json: schema: $ref: '#/components/schemas/DepartmentWeatherTimelineResponse' example: success: true meta: [] includes: [] data: - date: '2026-03-23' time: '00:00' current: false weather: mostly_cloudy washes: 1 hours: 2.0 status: degraded - date: '2026-03-24' time: '13:00' current: true weather: rain washes: 0 hours: 2.5 status: unhealthy - date: '2026-03-24' time: '14:00' current: false weather: mostly_clear washes: 0 hours: 1.0 status: unknown /departments/weather/targets: get: tags: - Departments summary: Get department weather status targets description: Returns department-specific weather productivity thresholds used by `/departments/weather` to classify `healthy`, `degraded`, and `unhealthy` statuses. operationId: getDepartmentWeatherTargets parameters: - name: id in: query required: false schema: type: array items: type: integer minimum: 1 minItems: 1 uniqueItems: true style: form explode: true description: Department ID list. Repeat `id` to select multiple departments (`?id=1&id=2`). - name: ids in: query required: false schema: type: string example: '1,2,3' description: Optional CSV alternative for department IDs. Merged with `id` if both are provided. At least one of `id` or `ids` must be provided. responses: '200': description: Department weather targets retrieved successfully content: application/json: schema: $ref: '#/components/schemas/DepartmentWeatherTargetsResponse' example: success: true meta: [] includes: [] data: - department_id: 1 degraded_threshold: 1.0 healthy_threshold: 1.3 configured: true - department_id: 2 degraded_threshold: null healthy_threshold: null configured: false put: tags: - Departments summary: Upsert department weather status targets description: Creates or updates the weather productivity thresholds for one department. `healthy_threshold` must be greater than or equal to `degraded_threshold`. operationId: upsertDepartmentWeatherTarget requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/DepartmentWeatherTargetUpsertRequest' example: department_id: 1 degraded_threshold: 1.0 healthy_threshold: 1.3 responses: '200': description: Department weather targets updated successfully content: application/json: schema: allOf: - $ref: '#/components/schemas/ModuleConfigEnvelopeBase' - type: object properties: data: $ref: '#/components/schemas/DepartmentWeatherTarget' required: [data] example: success: true meta: [] includes: [] data: department_id: 1 degraded_threshold: 1.0 healthy_threshold: 1.3 configured: true /modules/entra/users: get: tags: - Modules summary: List Microsoft Entra users description: Get list of users from Microsoft Entra (Azure AD) operationId: listEntraUsers responses: '200': description: Entra users retrieved successfully content: application/json: schema: {} # Attachments Endpoints /attachments/upload: post: tags: - Attachments summary: Upload attachment description: Upload a file attachment operationId: uploadAttachment requestBody: required: true content: multipart/form-data: schema: type: object properties: file: type: string format: binary responses: '201': description: Attachment uploaded successfully content: application/json: schema: {} '400': $ref: '#/components/responses/BadRequest' /orders/attachments: get: tags: - Attachments summary: List order attachments description: Get attachments for an order operationId: listOrderAttachments parameters: - name: order_id in: query required: true schema: type: integer responses: '200': description: Order attachments retrieved successfully content: application/json: schema: {} /orders/attachments/upload: post: tags: - Attachments summary: Upload order attachment description: Upload an attachment to an order operationId: uploadOrderAttachment requestBody: required: true content: multipart/form-data: schema: type: object properties: order_id: type: integer file: type: string format: binary responses: '201': description: Order attachment uploaded successfully content: application/json: schema: {} /orders/attachments/download: get: tags: - Attachments summary: Get order attachment download link description: Return a legacy HTTPS download link for a specific order attachment operationId: downloadOrderAttachment parameters: - name: order_id in: query required: true schema: type: integer minimum: 1 - name: attachment_id in: query required: true schema: type: integer minimum: 1 responses: '200': description: Attachment download link resolved successfully content: application/json: schema: $ref: '#/components/schemas/OrderAttachmentDownloadLinkResponse' '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '502': description: Attachment storage is unavailable /orders/attachments/content: get: tags: - Attachments summary: Stream authenticated order attachment content description: Streams an attachment after validating order scope and attachment ownership operationId: streamOrderAttachmentContent parameters: - name: order_id in: query required: true schema: type: integer minimum: 1 - name: attachment_id in: query required: true schema: type: integer minimum: 1 - name: disposition in: query required: false schema: type: string enum: [inline, attachment] default: inline responses: '200': description: Attachment content streamed successfully headers: Content-Disposition: schema: type: string content: application/octet-stream: schema: type: string format: binary image/*: schema: type: string format: binary application/pdf: schema: type: string format: binary '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } '502': description: Attachment storage is unavailable # Forms Endpoints /form: get: tags: - Forms summary: Get form description: Retrieve a form definition operationId: getForm parameters: - name: id in: query schema: type: integer responses: '200': description: Form retrieved successfully content: application/json: schema: {} post: tags: - Forms summary: Submit form description: Submit a form operationId: submitForm requestBody: required: true content: application/json: schema: type: object required: [id, data] properties: id: type: string description: Form identifier data: type: object description: Form submission data g_recaptcha_response: type: string description: reCAPTCHA verification token (required if not authenticated) responses: '201': description: Form submitted successfully content: application/json: schema: {} # Permissions Endpoints /permissions: get: tags: - Users summary: List permissions description: Get list of all available permissions operationId: listPermissions responses: '200': description: Permissions retrieved successfully content: application/json: schema: type: array items: $ref: '#/components/schemas/Permission' # Customer Management Endpoints /superuser/customer-rules/product-restrictions: get: tags: [Superuser, Users] summary: List global customer-rule product restrictions operationId: listCustomerRuleProductRestrictions responses: '200': description: Rule collections and product catalog retrieved content: application/json: schema: $ref: '#/components/schemas/CustomerRuleProductRestrictionListResponse' '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } /superuser/customer-rules/product-restrictions/{attribute}: put: tags: [Superuser, Users] summary: Atomically replace one customer rule's product collections operationId: replaceCustomerRuleProductRestriction parameters: - name: attribute in: path required: true schema: $ref: '#/components/schemas/CustomerProductImpactAttribute' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CustomerRuleProductRestrictionUpdateRequest' responses: '200': description: Rule configuration replaced content: application/json: schema: $ref: '#/components/schemas/CustomerRuleProductRestrictionResponse' '409': { $ref: '#/components/responses/Conflict' } '422': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } /customer/attributes: get: tags: - Users summary: Get customer attributes description: Get custom attributes for a customer. Authenticated customer accounts may read their own attributes without list_customer_attributes. operationId: getCustomerAttributes parameters: - name: customer_number in: query schema: type: integer - name: user_id in: query schema: type: integer responses: '200': description: Customer attributes retrieved successfully content: application/json: schema: $ref: '#/components/schemas/CustomerAttributesResponse' post: tags: - Users summary: Add customer attribute description: Add a custom attribute to a customer operationId: addCustomerAttribute requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CustomerAttributeMutationRequest' responses: '200': description: Customer attribute added successfully content: application/json: schema: {} delete: tags: - Users summary: Delete customer attribute description: Remove a custom attribute from a customer operationId: deleteCustomerAttribute parameters: - name: user_id in: query schema: { type: integer } - name: customer_number in: query schema: { type: integer } - name: attribute in: query required: true schema: { type: string } responses: '200': description: Customer attribute deleted successfully content: application/json: schema: {} /customer/notes: get: tags: - Users summary: Get customer notes description: Get notes for a customer operationId: getCustomerNotes parameters: - name: customer_id in: query schema: type: integer responses: '200': description: Customer notes retrieved successfully content: application/json: schema: {} post: tags: - Users summary: Add customer note description: Add a note to a customer operationId: addCustomerNote requestBody: required: false content: application/json: schema: {} responses: '201': description: Customer note added successfully content: application/json: schema: {} delete: tags: - Users summary: Delete customer note description: Remove a note from a customer operationId: deleteCustomerNote requestBody: required: false content: application/json: schema: {} responses: '200': description: Customer note deleted successfully content: application/json: schema: {} /customers/search: post: tags: - Users summary: Search customers description: Search for customers using various criteria operationId: searchCustomers requestBody: required: true content: application/json: schema: type: object properties: query: type: string responses: '200': description: Customers found successfully content: application/json: schema: {} /search/system: get: tags: - Search summary: System-wide search description: Search across supported entities with permission-aware filtering, strict relevance filtering, and a single capped result set. Customer records and customer-related entities are matched against a local e-conomic customer index (name/address/email/CVR) that is refreshed by cron. Results are ordered by relevance, with recent records preferred when relevance is comparable. operationId: systemWideSearchGet parameters: - in: query name: query required: true schema: type: string description: Free-text query to search for. Supports deterministic domain synonyms such as `rabat` -> `discount`. - in: query name: include_types required: false schema: type: array items: $ref: '#/components/schemas/SystemSearchEntityType' style: form explode: false description: Comma-separated list of entity types to include. Defaults to all allowed types. - in: query name: exclude_types required: false schema: type: array items: $ref: '#/components/schemas/SystemSearchEntityType' style: form explode: false description: Comma-separated list of entity types to exclude. - in: query name: include_associations required: false schema: type: boolean default: true description: Include associated objects when matching a primary entity such as a customer. - in: query name: max_results required: false schema: type: integer minimum: 1 maximum: 50 default: 50 description: Maximum number of relevance-ranked results to return. Values above 50 are capped to 50. responses: '200': description: Search results returned successfully content: application/json: schema: $ref: '#/components/schemas/SystemSearchResponse' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' post: tags: - Search summary: System-wide search description: Search across supported entities using JSON request payload. Customer-related matches include local e-conomic customer index fields. Results use strict relevance filtering and are ordered by relevance, with recent records preferred when relevance is comparable. operationId: systemWideSearchPost requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/SystemSearchRequest' responses: '200': description: Search results returned successfully content: application/json: schema: $ref: '#/components/schemas/SystemSearchResponse' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' /superuser/search/system/cache: delete: tags: - Search summary: Clear system search caches description: Clears query-result caches for system-wide search. operationId: clearSystemSearchCache responses: '200': description: Cache cleared successfully content: application/json: schema: $ref: '#/components/schemas/SystemSearchCacheClearResponse' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' /superuser/search/system/cache/rebuild: post: tags: - Search summary: Queue system search cache rebuild description: Queues a document-index rebuild request and clears active query cache entries immediately. operationId: rebuildSystemSearchCache requestBody: required: false content: application/json: schema: $ref: '#/components/schemas/SystemSearchCacheRebuildRequest' responses: '200': description: Cache rebuild queued successfully content: application/json: schema: $ref: '#/components/schemas/SystemSearchCacheRebuildResponse' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' /superuser/system/status: get: tags: - Superuser summary: Aggregated system status snapshot description: Returns a read-only snapshot of runtime health, dependency connectivity, module configuration/probe status, and active user session activity for the superuser dashboard. operationId: getSuperuserSystemStatus parameters: - in: query name: force required: false schema: type: boolean default: false description: Bypass cached external module probes for this request. responses: '200': description: System status snapshot returned successfully content: application/json: schema: $ref: '#/components/schemas/SuperuserSystemStatusResponse' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' /superuser/replication: get: tags: - Superuser summary: Database, Redis, and MinIO replication topology operationId: getSuperuserReplication parameters: - in: query name: refresh required: false schema: type: boolean default: false description: Refresh host connectivity and replication status before returning the topology. responses: '200': description: Replication topology returned successfully content: application/json: schema: $ref: '#/components/schemas/SuperuserReplicationResponse' '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } /superuser/replication/databases: post: tags: - Superuser summary: Add database replication host credentials operationId: addSuperuserDatabaseReplicationHost requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/SuperuserReplicationHostCreateRequest' responses: '201': description: Database replication host added content: application/json: schema: $ref: '#/components/schemas/SuperuserReplicationHostResponse' '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } /superuser/replication/redis: post: tags: - Superuser summary: Add Redis replication host credentials operationId: addSuperuserRedisReplicationHost requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/SuperuserReplicationHostCreateRequest' responses: '201': description: Redis replication host added content: application/json: schema: $ref: '#/components/schemas/SuperuserReplicationHostResponse' '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } /superuser/replication/minio: post: tags: - Superuser summary: Add MinIO replication host credentials operationId: addSuperuserMinioReplicationHost requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/SuperuserReplicationHostCreateRequest' responses: '201': description: MinIO replication host added content: application/json: schema: $ref: '#/components/schemas/SuperuserReplicationHostResponse' '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } /superuser/replication/compose-template: post: tags: - Superuser summary: Generate a replication-ready Docker Compose template operationId: generateSuperuserReplicationComposeTemplate requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/SuperuserReplicationComposeTemplateRequest' responses: '200': description: Docker Compose template generated successfully content: application/json: schema: $ref: '#/components/schemas/SuperuserReplicationComposeTemplateResponse' '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } /superuser/replication/test-credentials: post: tags: - Superuser summary: Test replication host credentials before saving operationId: testSuperuserReplicationCredentials requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/SuperuserReplicationUnsavedCredentialTestRequest' responses: '200': description: Credential test returned successfully content: application/json: schema: $ref: '#/components/schemas/SuperuserReplicationOperationResponse' '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } /superuser/replication/{kind}/{id}/test: post: tags: - Superuser summary: Test replication host connectivity and privileges operationId: testSuperuserReplicationHost parameters: - $ref: '#/components/parameters/SuperuserReplicationKindParam' - $ref: '#/components/parameters/SuperuserReplicationHostIdParam' responses: '200': description: Host test result returned content: application/json: schema: $ref: '#/components/schemas/SuperuserReplicationOperationResponse' '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } /superuser/replication/{kind}/{id}/provision: post: tags: - Superuser summary: Provision a host as a replica of the current primary operationId: provisionSuperuserReplicationHost parameters: - $ref: '#/components/parameters/SuperuserReplicationKindParam' - $ref: '#/components/parameters/SuperuserReplicationHostIdParam' responses: '200': description: Replica provisioning started or completed content: application/json: schema: $ref: '#/components/schemas/SuperuserReplicationOperationResponse' '409': { $ref: '#/components/responses/Conflict' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } /superuser/replication/{kind}/{id}/promote: post: tags: - Superuser summary: Promote a caught-up replica to primary operationId: promoteSuperuserReplicationHost parameters: - $ref: '#/components/parameters/SuperuserReplicationKindParam' - $ref: '#/components/parameters/SuperuserReplicationHostIdParam' responses: '200': description: Replica promoted to primary content: application/json: schema: $ref: '#/components/schemas/SuperuserReplicationOperationResponse' '409': { $ref: '#/components/responses/Conflict' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } /superuser/replication/{kind}/{id}: patch: tags: - Superuser summary: Rename a replication host operationId: renameSuperuserReplicationHost parameters: - $ref: '#/components/parameters/SuperuserReplicationKindParam' - $ref: '#/components/parameters/SuperuserReplicationHostIdParam' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/SuperuserReplicationHostRenameRequest' responses: '200': description: Replication host renamed content: application/json: schema: $ref: '#/components/schemas/SuperuserReplicationHostResponse' '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } delete: tags: - Superuser summary: Remove an inactive or unhealthy replication host operationId: removeSuperuserReplicationHost parameters: - $ref: '#/components/parameters/SuperuserReplicationKindParam' - $ref: '#/components/parameters/SuperuserReplicationHostIdParam' responses: '200': description: Replication host removed content: application/json: schema: $ref: '#/components/schemas/SuperuserReplicationOperationResponse' '409': { $ref: '#/components/responses/Conflict' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } /superuser/coolify: get: tags: - Superuser summary: Coolify-managed replicated infrastructure state operationId: getSuperuserCoolify responses: '200': description: Coolify summary returned successfully content: application/json: schema: $ref: '#/components/schemas/SuperuserCoolifyResponse' '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } /superuser/coolify/load-balancer: get: tags: - Superuser summary: Coolify public gateway Load Balancer state operationId: getSuperuserCoolifyLoadBalancer responses: '200': description: Load Balancer summary returned successfully content: application/json: schema: $ref: '#/components/schemas/SuperuserCoolifyLoadBalancerResponse' '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } /superuser/coolify/load-balancer/reconcile: post: tags: - Superuser summary: Reconcile Coolify public gateway Load Balancer state operationId: reconcileSuperuserCoolifyLoadBalancer requestBody: required: false content: application/json: schema: type: object properties: dry_run: type: boolean default: true enforce: type: boolean default: false responses: '200': description: Load Balancer reconcile result returned content: application/json: schema: $ref: '#/components/schemas/SuperuserCoolifyLoadBalancerReconcileResponse' '409': { $ref: '#/components/responses/Conflict' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } /superuser/coolify/load-balancer/routes/deploy: post: tags: - Superuser summary: Deploy the Coolify API route for the public gateway host operationId: deploySuperuserCoolifyGatewayRoutes requestBody: required: false content: application/json: schema: type: object properties: dry_run: type: boolean default: true enforce: type: boolean default: false responses: '200': description: Gateway application route deploy result returned content: application/json: schema: $ref: '#/components/schemas/SuperuserCoolifyGatewayRouteDeployResponse' '409': { $ref: '#/components/responses/Conflict' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } /superuser/coolify/load-balancer/api/deploy: post: tags: - Superuser summary: Deploy the latest Coolify API code for the public gateway host operationId: deploySuperuserCoolifyGatewayApiCode requestBody: required: false content: application/json: schema: type: object properties: dry_run: type: boolean default: true enforce: type: boolean default: false deploy_routes: type: boolean default: true responses: '200': description: Gateway API code deployment result returned content: application/json: schema: $ref: '#/components/schemas/SuperuserCoolifyGatewayRouteDeployResponse' '409': { $ref: '#/components/responses/Conflict' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } /superuser/coolify/gateways: get: tags: - Superuser summary: List Coolify public gateway Load Balancer targets operationId: listSuperuserCoolifyGateways responses: '200': description: Gateway targets returned content: application/json: schema: $ref: '#/components/schemas/SuperuserCoolifyGatewaysResponse' '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } post: tags: - Superuser summary: Create or update a Coolify public gateway Load Balancer target operationId: saveSuperuserCoolifyGateway requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/SuperuserCoolifyGatewaySaveRequest' responses: '201': description: Gateway target saved content: application/json: schema: $ref: '#/components/schemas/SuperuserCoolifyGatewayResponse' '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } /superuser/coolify/gateways/{id}/test: post: tags: - Superuser summary: Probe a Coolify public gateway target operationId: testSuperuserCoolifyGateway parameters: - in: path name: id required: true schema: type: integer responses: '200': description: Gateway target probe result returned content: application/json: schema: $ref: '#/components/schemas/SuperuserCoolifyOperationResponse' '409': { $ref: '#/components/responses/Conflict' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } /superuser/coolify/instances: post: tags: - Superuser summary: Create Coolify API connection operationId: createSuperuserCoolifyInstance requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/SuperuserCoolifyInstanceCreateRequest' responses: '201': description: Coolify instance created content: application/json: schema: $ref: '#/components/schemas/SuperuserCoolifyInstanceResponse' '400': { $ref: '#/components/responses/BadRequest' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } /superuser/coolify/instances/{id}/test: post: tags: - Superuser summary: Test Coolify API connection operationId: testSuperuserCoolifyInstance parameters: - in: path name: id required: true schema: type: integer responses: '200': description: Coolify connection test returned content: application/json: schema: $ref: '#/components/schemas/SuperuserCoolifyOperationResponse' '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } /superuser/coolify/instances/{id}/placement: get: tags: - Superuser summary: Discover Coolify placement options operationId: discoverSuperuserCoolifyInstancePlacement parameters: - in: path name: id required: true schema: type: integer responses: '200': description: Coolify project, environment, and server options returned content: application/json: schema: $ref: '#/components/schemas/SuperuserCoolifyPlacementResponse' '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } '404': { $ref: '#/components/responses/NotFound' } /superuser/coolify/targets: get: tags: - Superuser summary: List Coolify-managed replication targets operationId: listSuperuserCoolifyTargets parameters: - in: query name: kind required: false schema: type: string enum: [database, redis, minio] responses: '200': description: Coolify targets returned content: application/json: schema: $ref: '#/components/schemas/SuperuserCoolifyTargetsResponse' '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } post: tags: - Superuser summary: Create Coolify-managed passive replication target operationId: createSuperuserCoolifyTarget requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/SuperuserCoolifyTargetCreateRequest' responses: '201': description: Coolify target created content: application/json: schema: $ref: '#/components/schemas/SuperuserCoolifyOperationResponse' '409': { $ref: '#/components/responses/Conflict' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } /superuser/coolify/targets/{id}/reconcile: post: tags: - Superuser summary: Reconcile a passive Coolify target operationId: reconcileSuperuserCoolifyTarget parameters: - in: path name: id required: true schema: type: integer responses: '200': { description: Reconcile completed or queued } '409': { $ref: '#/components/responses/Conflict' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } /superuser/coolify/targets/{id}/deploy: post: tags: - Superuser summary: Deploy and provision a passive Coolify target operationId: deploySuperuserCoolifyTarget parameters: - in: path name: id required: true schema: type: integer responses: '200': { description: Deploy and provision flow completed or queued } '409': { $ref: '#/components/responses/Conflict' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } /superuser/coolify/targets/{id}/restart: post: tags: - Superuser summary: Restart a passive Coolify target operationId: restartSuperuserCoolifyTarget parameters: - in: path name: id required: true schema: type: integer responses: '200': { description: Restart requested } '409': { $ref: '#/components/responses/Conflict' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } /superuser/coolify/targets/{id}/failover: post: tags: - Superuser summary: Promote a Coolify-managed replica through replication failover operationId: failoverSuperuserCoolifyTarget parameters: - in: path name: id required: true schema: type: integer responses: '200': { description: Failover action returned } '409': { $ref: '#/components/responses/Conflict' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } /superuser/coolify/targets/{id}: delete: tags: - Superuser summary: Delete a Coolify target mapping with destructive confirmation operationId: deleteSuperuserCoolifyTarget parameters: - in: path name: id required: true schema: type: integer requestBody: required: true content: application/json: schema: type: object required: [confirm] properties: confirm: type: string description: Must equal delete-coolify-target-{id}. delete_resource: type: boolean default: false responses: '200': { description: Target deleted } '409': { $ref: '#/components/responses/Conflict' } '401': { $ref: '#/components/responses/Unauthorized' } '403': { $ref: '#/components/responses/Forbidden' } # Configuration Endpoints /economic/config: get: tags: [Config] summary: Get e-conomic config operationId: getEconomicConfig responses: '200': description: e-conomic configuration retrieved successfully content: application/json: schema: $ref: '#/components/schemas/EconomicConfigListResponse' post: tags: [Config] summary: Update e-conomic config operationId: updateEconomicConfig requestBody: required: false content: application/json: schema: {} responses: '200': description: e-conomic configuration updated successfully content: application/json: schema: $ref: '#/components/schemas/ModuleConfigUpdateResponse' /reCAPTCHA/config: get: tags: [Config] summary: Get reCAPTCHA config operationId: getRecaptchaModuleConfig responses: '200': description: reCAPTCHA configuration retrieved successfully content: application/json: schema: $ref: '#/components/schemas/RecaptchaConfigListResponse' post: tags: [Config] summary: Update reCAPTCHA config operationId: updateRecaptchaConfig requestBody: required: false content: application/json: schema: {} responses: '200': description: reCAPTCHA configuration updated successfully content: application/json: schema: $ref: '#/components/schemas/ModuleConfigUpdateResponse' /email/config: get: tags: [Config] summary: Get email config operationId: getEmailConfig responses: '200': description: Email configuration retrieved successfully content: application/json: schema: $ref: '#/components/schemas/EmailConfigListResponse' post: tags: [Config] summary: Update email config operationId: updateEmailConfig requestBody: required: false content: application/json: schema: {} responses: '200': description: Email configuration updated successfully content: application/json: schema: $ref: '#/components/schemas/ModuleConfigUpdateResponse' /email/config/test: post: tags: [Config] summary: Test email config operationId: testEmailConfig requestBody: required: false content: application/json: schema: {} responses: '200': description: Email configuration test completed content: application/json: schema: $ref: '#/components/schemas/ModuleConfigTestResponse' /slack/config: get: tags: [Config] summary: Get Slack config operationId: getSlackConfig responses: '200': description: Slack configuration retrieved successfully content: application/json: schema: $ref: '#/components/schemas/SlackConfigListResponse' post: tags: [Config] summary: Update Slack config operationId: updateSlackConfig requestBody: required: false content: application/json: schema: {} responses: '200': description: Slack configuration updated successfully content: application/json: schema: $ref: '#/components/schemas/ModuleConfigUpdateResponse' /slack/config/test: post: tags: [Config] summary: Test Slack customer registration webhook operationId: testSlackCustomerRegistrationWebhook responses: '200': description: Slack customer registration webhook test completed successfully content: application/json: schema: $ref: '#/components/schemas/SlackConfigTestResponse' '400': description: Slack customer registration webhook URL is not configured '502': description: Slack customer registration webhook test failed /slack/config/internal-department-goal-progress: get: tags: [Config] summary: Get Slack internal department goal progress config operationId: getSlackInternalDepartmentGoalProgressConfig responses: '200': description: Slack internal department goal progress configuration retrieved successfully content: application/json: schema: $ref: '#/components/schemas/SlackInternalDepartmentGoalProgressConfigResponse' post: tags: [Config] summary: Update Slack internal department goal progress config operationId: updateSlackInternalDepartmentGoalProgressConfig requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/SlackInternalDepartmentGoalProgressConfigUpdate' responses: '200': description: Slack internal department goal progress configuration updated successfully content: application/json: schema: $ref: '#/components/schemas/SlackInternalDepartmentGoalProgressConfigResponse' /slack/config/internal-department-goal-progress/test: post: tags: [Config] summary: Test Slack internal department goal progress webhook operationId: testSlackInternalDepartmentGoalProgressWebhook responses: '200': description: Slack internal department goal progress webhook test completed successfully content: application/json: schema: $ref: '#/components/schemas/SlackConfigTestResponse' '400': description: Slack internal department goal progress webhook URL is not configured '502': description: Slack internal department goal progress webhook test failed /backups/config: get: tags: [Config] summary: Get backups config operationId: getBackupsConfig responses: '200': description: Backups configuration retrieved successfully content: application/json: schema: $ref: '#/components/schemas/BackupsConfigListResponse' post: tags: [Config] summary: Update backups config operationId: updateBackupsConfig requestBody: required: false content: application/json: schema: {} responses: '200': description: Backups configuration updated successfully content: application/json: schema: $ref: '#/components/schemas/ModuleConfigUpdateResponse' /bird/config: get: tags: [Config] summary: Get Bird config operationId: getBirdConfig responses: '200': description: Bird configuration retrieved successfully content: application/json: schema: $ref: '#/components/schemas/BirdConfigListResponse' post: tags: [Config] summary: Update Bird config operationId: updateBirdConfig requestBody: required: false content: application/json: schema: {} responses: '200': description: Bird configuration updated successfully content: application/json: schema: $ref: '#/components/schemas/ModuleConfigUpdateResponse' /motorapi/config: get: tags: [Config] summary: Get MotorAPI config operationId: getMotorApiConfig responses: '200': description: MotorAPI configuration retrieved successfully content: application/json: schema: $ref: '#/components/schemas/MotorApiConfigListResponse' post: tags: [Config] summary: Update MotorAPI config operationId: updateMotorApiConfig requestBody: required: false content: application/json: schema: {} responses: '200': description: MotorAPI configuration updated successfully content: application/json: schema: $ref: '#/components/schemas/ModuleConfigUpdateResponse' /stripe/config: get: tags: [Config] summary: Get Stripe config operationId: getStripeConfig responses: '200': description: Stripe configuration retrieved successfully content: application/json: schema: $ref: '#/components/schemas/StripeConfigListResponse' post: tags: [Config] summary: Update Stripe config operationId: updateStripeConfig requestBody: required: false content: application/json: schema: {} responses: '200': description: Stripe configuration updated successfully content: application/json: schema: $ref: '#/components/schemas/ModuleConfigUpdateResponse' /fxratesapi/config: get: tags: [Config] summary: Get FXRatesAPI config operationId: getFxRatesApiConfig responses: '200': description: FXRatesAPI configuration retrieved successfully content: application/json: schema: $ref: '#/components/schemas/FxRatesApiConfigListResponse' post: tags: [Config] summary: Update FXRatesAPI config operationId: updateFxRatesApiConfig requestBody: required: false content: application/json: schema: {} responses: '200': description: FXRatesAPI configuration updated successfully content: application/json: schema: $ref: '#/components/schemas/ModuleConfigUpdateResponse' /weatherapi/config: get: tags: [Config] summary: Get WeatherAPI config operationId: getWeatherApiConfig responses: '200': description: WeatherAPI configuration retrieved successfully content: application/json: schema: $ref: '#/components/schemas/WeatherApiConfigListResponse' post: tags: [Config] summary: Update WeatherAPI config operationId: updateWeatherApiConfig requestBody: required: false content: application/json: schema: {} responses: '200': description: WeatherAPI configuration updated successfully content: application/json: schema: $ref: '#/components/schemas/ModuleConfigUpdateResponse' /workfeed/config: get: tags: [Config] summary: Get Workfeed config operationId: getWorkfeedConfig responses: '200': description: Workfeed configuration retrieved successfully content: application/json: schema: $ref: '#/components/schemas/WorkfeedConfigListResponse' examples: default: summary: Workfeed module configuration value: success: true data: - module: workfeed variable: enabled type: bool value: true - module: workfeed variable: api_url type: string value: https://europe-west1-production-eu-327a3.cloudfunctions.net/api - module: workfeed variable: api_key type: string value: wf_live_xxxxxxxxxxxxxxxxx - module: workfeed variable: CompanyID type: string value: "123456" meta: [] includes: [] post: tags: [Config] summary: Update Workfeed config operationId: updateWorkfeedConfig requestBody: required: false content: application/json: schema: {} responses: '200': description: Workfeed configuration updated successfully content: application/json: schema: $ref: '#/components/schemas/ModuleConfigUpdateResponse' /gatewayapi/config: get: tags: [Config] summary: Get GatewayAPI config operationId: getGatewayApiConfig responses: '200': description: GatewayAPI configuration retrieved successfully content: application/json: schema: $ref: '#/components/schemas/GatewayApiConfigListResponse' post: tags: [Config] summary: Update GatewayAPI config operationId: updateGatewayApiConfig requestBody: required: false content: application/json: schema: {} responses: '200': description: GatewayAPI configuration updated successfully content: application/json: schema: $ref: '#/components/schemas/ModuleConfigUpdateResponse' /xlvask/config: get: tags: [Config] summary: Get XLVask config operationId: getXlvaskConfig responses: '200': description: XLVask configuration retrieved successfully content: application/json: schema: $ref: '#/components/schemas/XlvaskConfigListResponse' post: tags: [Config] summary: Update XLVask config operationId: updateXlvaskConfig requestBody: required: false content: application/json: schema: {} responses: '200': description: XLVask configuration updated successfully content: application/json: schema: $ref: '#/components/schemas/ModuleConfigUpdateResponse' /entra/config: get: tags: [Config] summary: Get Entra config operationId: getEntraConfig responses: '200': description: Entra configuration retrieved successfully content: application/json: schema: $ref: '#/components/schemas/EntraConfigListResponse' post: tags: [Config] summary: Update Entra config operationId: updateEntraConfig requestBody: required: false content: application/json: schema: {} responses: '200': description: Entra configuration updated successfully content: application/json: schema: $ref: '#/components/schemas/ModuleConfigUpdateResponse' /limble/config: get: tags: [Config] summary: Get Limble config operationId: getLimbleConfig responses: '200': description: Limble configuration retrieved successfully content: application/json: schema: $ref: '#/components/schemas/LimbleConfigListResponse' post: tags: [Config] summary: Update Limble config operationId: updateLimbleConfig requestBody: required: false content: application/json: schema: {} responses: '200': description: Limble configuration updated successfully content: application/json: schema: $ref: '#/components/schemas/ModuleConfigUpdateResponse' /ocrspace/config: get: tags: [Config] summary: Get OcrSpace config operationId: getOcrSpaceConfig responses: '200': description: OcrSpace configuration retrieved successfully content: application/json: schema: $ref: '#/components/schemas/OcrSpaceConfigListResponse' post: tags: [Config] summary: Update OcrSpace config operationId: updateOcrSpaceConfig requestBody: required: false content: application/json: schema: {} responses: '200': description: OcrSpace configuration updated successfully content: application/json: schema: $ref: '#/components/schemas/ModuleConfigUpdateResponse' /openai/config: get: tags: [Config] summary: Get OpenAI config operationId: getOpenAiConfig responses: '200': description: OpenAI configuration retrieved successfully content: application/json: schema: $ref: '#/components/schemas/OpenAiConfigListResponse' post: tags: [Config] summary: Update OpenAI config operationId: updateOpenAiConfig requestBody: required: false content: application/json: schema: {} responses: '200': description: OpenAI configuration updated successfully content: application/json: schema: $ref: '#/components/schemas/ModuleConfigUpdateResponse' /licenseplaterecognizer/config: get: tags: [Config] summary: Get LicensePlateRecognizer config operationId: getLicensePlateRecognizerConfig responses: '200': description: LicensePlateRecognizer configuration retrieved successfully content: application/json: schema: $ref: '#/components/schemas/LicensePlateRecognizerConfigListResponse' post: tags: [Config] summary: Update LicensePlateRecognizer config operationId: updateLicensePlateRecognizerConfig requestBody: required: false content: application/json: schema: {} responses: '200': description: LicensePlateRecognizer configuration updated successfully content: application/json: schema: $ref: '#/components/schemas/ModuleConfigUpdateResponse' /virkdata/config: get: tags: [Config] summary: Get Virkdata config operationId: getVirkdataConfig responses: '200': description: Virkdata configuration retrieved successfully content: application/json: schema: $ref: '#/components/schemas/VirkdataConfigListResponse' post: tags: [Config] summary: Update Virkdata config operationId: updateVirkdataConfig requestBody: required: false content: application/json: schema: {} responses: '200': description: Virkdata configuration updated successfully content: application/json: schema: $ref: '#/components/schemas/ModuleConfigUpdateResponse' /shelly/config: get: tags: [Config] summary: Get Shelly config operationId: getShellyConfig responses: '200': description: Shelly configuration retrieved successfully content: application/json: schema: $ref: '#/components/schemas/ShellyConfigListResponse' post: tags: [Config] summary: Update Shelly config operationId: updateShellyConfig requestBody: required: false content: application/json: schema: {} responses: '200': description: Shelly configuration updated successfully content: application/json: schema: $ref: '#/components/schemas/ModuleConfigUpdateResponse' /selfserve/config: get: tags: [Config] summary: Get Self-Serve config operationId: getSelfServeConfig responses: '200': description: Self-serve configuration retrieved successfully content: application/json: schema: $ref: '#/components/schemas/SelfServeConfigListResponse' post: tags: [Config] summary: Update Self-Serve config operationId: updateSelfServeConfig requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/SelfServeConfig' responses: '200': description: Self-serve configuration updated successfully content: application/json: schema: $ref: '#/components/schemas/ModuleConfigUpdateResponse' /branding: get: tags: - Branding summary: List branding options description: Retrieve a list of branding options or a specific branding option if ID is provided operationId: listBrandingOptions parameters: - name: id in: query required: false schema: type: integer - $ref: '#/components/parameters/PageParam' - $ref: '#/components/parameters/PerPageParam' responses: '200': description: Branding options retrieved successfully content: application/json: schema: {} '400': $ref: '#/components/responses/BadRequest' '403': $ref: '#/components/responses/Forbidden' post: tags: - Branding summary: Add branding option description: Create a new branding option operationId: addBrandingOption requestBody: required: true content: application/json: schema: type: object required: [name, description, cvr] properties: name: {type: string} description: {type: string} cvr: {type: integer} address: {type: string, nullable: true} phone_country_code: {type: integer, nullable: true} phone: {type: integer, nullable: true} email: {type: string, nullable: true} website: {type: string, nullable: true} banner: {type: string, nullable: true} logo: {type: string, nullable: true} favicon: {type: string, nullable: true} signature: {type: string, nullable: true} responses: '200': description: Branding option added successfully content: application/json: schema: {} '400': $ref: '#/components/responses/BadRequest' '403': $ref: '#/components/responses/Forbidden' put: tags: - Branding summary: Edit branding option description: Update an existing branding option operationId: editBrandingOption requestBody: required: true content: application/json: schema: type: object required: [id] properties: id: {type: integer} name: {type: string, nullable: true} description: {type: string, nullable: true} cvr: {type: integer, nullable: true} address: {type: string, nullable: true} phone_country_code: {type: integer, nullable: true} phone: {type: integer, nullable: true} email: {type: string, nullable: true} website: {type: string, nullable: true} banner: {type: string, nullable: true} logo: {type: string, nullable: true} favicon: {type: string, nullable: true} signature: {type: string, nullable: true} responses: '200': description: Branding option updated successfully content: application/json: schema: {} '400': $ref: '#/components/responses/BadRequest' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' /roles: get: tags: - Roles summary: List roles operationId: listRoles responses: '200': description: Success content: application/json: schema: {} post: tags: - Roles summary: Add role operationId: addRole requestBody: required: true content: application/json: schema: type: object required: [name] properties: name: {type: string} responses: '200': description: Success content: application/json: schema: {} put: tags: - Roles summary: Edit role operationId: editRole requestBody: required: true content: application/json: schema: type: object required: [id, name] properties: id: {type: integer} name: {type: string} responses: '200': description: Success content: application/json: schema: {} /roles/limited-backoffice-permission-templates: get: tags: - Roles summary: List limited backoffice permission templates operationId: listLimitedBackofficeRolePermissionTemplates responses: '200': description: Success content: application/json: schema: type: array items: type: object properties: key: {type: string} label: {type: string} description: {type: string} permissions: type: array items: {type: string} /roles/permissions: post: tags: - Roles summary: Add permission to role operationId: addRolePermission requestBody: required: true content: application/json: schema: type: object required: [role_id, permission] properties: role_id: {type: integer} permission: {type: string} responses: '200': description: Success content: application/json: schema: {} delete: tags: - Roles summary: Remove permission from role operationId: removeRolePermission parameters: - name: role_id in: query required: true schema: {type: integer} - name: permission in: query required: true schema: {type: string} responses: '200': description: Success content: application/json: schema: {} /roles/clone: post: tags: - Roles summary: Clone role operationId: cloneRole requestBody: required: true content: application/json: schema: type: object required: [role_id, name] properties: role_id: {type: integer} name: {type: string} responses: '200': description: Success content: application/json: schema: {} /modules/washcertificates: get: tags: - Modules summary: List wash certificates operationId: listWashCertificates responses: '200': description: Success content: application/json: schema: {} /modules/xlvask/services/usage/orders: get: tags: - Modules summary: Get XLVask usage orders operationId: getXlvaskUsageOrders responses: '200': description: Success content: application/json: schema: {} /modules/xlvask/services/usage/orders/fast-link: get: tags: - Modules summary: Get XLVask usage orders fast link operationId: getXlvaskUsageOrdersFastLink responses: '200': description: Success content: application/json: schema: {} /superuser/department: get: tags: - Departments summary: List departments (superuser) operationId: listSuperuserDepartments responses: '200': description: Success content: application/json: schema: {} /superuser/department/branding: put: tags: - Departments summary: Set department branding description: Assign an existing branding option to a department, or clear the department branding by sending a null branding_id. operationId: setDepartmentBranding requestBody: required: true content: application/json: schema: type: object required: [department_id, branding_id] properties: department_id: {type: integer} branding_id: {type: integer, nullable: true} responses: '200': description: Department branding updated successfully content: application/json: schema: {} '400': $ref: '#/components/responses/BadRequest' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' /superuser/department/prices: get: tags: - Departments summary: Get department prices operationId: getDepartmentPrices parameters: - name: department_id in: query required: true schema: {type: integer} responses: '200': description: Success content: application/json: schema: {} post: tags: - Departments summary: Set department price operationId: setDepartmentPrice requestBody: required: true content: application/json: schema: type: object required: [department_id, product_id, price] properties: department_id: {type: integer} product_id: {type: integer} price: {type: integer} responses: '200': description: Success content: application/json: schema: {} /superuser/department/customer-pricing: get: tags: - Departments summary: Get department-specific customer pricing description: Returns customer price overrides and the department product catalog when custom-only pricing is enabled for the department. operationId: getSuperuserDepartmentCustomerPricing parameters: - name: department_id in: query required: true schema: type: integer minimum: 1 - name: user_id in: query required: false description: Internal user id. Provide either user_id or customer_number. schema: type: integer minimum: 1 - name: customer_number in: query required: false description: Customer number. Provide either user_id or customer_number. schema: type: integer minimum: 1 responses: '200': description: Department customer pricing content: application/json: schema: $ref: '#/components/schemas/DepartmentCustomerPricingResponse' '400': $ref: '#/components/responses/BadRequest' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' put: tags: - Departments summary: Replace department-specific customer pricing description: Replaces the complete override set for one customer in one custom-only department. operationId: setSuperuserDepartmentCustomerPricing requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/DepartmentCustomerPricingUpdateRequest' responses: '200': description: Department customer pricing updated content: application/json: schema: $ref: '#/components/schemas/DepartmentCustomerPricingResponse' '400': $ref: '#/components/responses/BadRequest' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/PricingConflict' /limited-backoffice/departments/{departmentId}/prices: get: tags: - Limited Backoffice summary: Get explicit limited-backoffice department prices description: Returns only explicit department prices and an opaque revision for optimistic concurrency. operationId: getLimitedBackofficeDepartmentPrices parameters: - name: departmentId in: path required: true schema: type: integer minimum: 1 responses: '200': description: Explicit department prices and current revision content: application/json: schema: $ref: '#/components/schemas/LimitedBackofficeDepartmentPricesResponse' '400': $ref: '#/components/responses/BadRequest' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' put: tags: - Limited Backoffice summary: Replace explicit limited-backoffice department prices description: Replaces the submitted explicit prices atomically. Send the revision returned by GET as `expected_revision` to prevent stale writes. operationId: setLimitedBackofficeDepartmentPrices parameters: - name: departmentId in: path required: true schema: type: integer minimum: 1 requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/LimitedBackofficeDepartmentPricesUpdateRequest' responses: '200': description: Explicit department prices updated atomically content: application/json: schema: $ref: '#/components/schemas/LimitedBackofficeDepartmentPricesResponse' '400': $ref: '#/components/responses/BadRequest' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/PricingConflict' /limited-backoffice/departments/{departmentId}/customer-pricing: get: tags: - Limited Backoffice summary: Get limited-backoffice department customer pricing description: Returns department-specific customer pricing for an assigned custom-only department. operationId: getLimitedBackofficeDepartmentCustomerPricing parameters: - name: departmentId in: path required: true schema: type: integer minimum: 1 - name: user_id in: query required: false description: Internal user id. Provide either user_id or customer_number. schema: type: integer minimum: 1 - name: customer_number in: query required: false description: Customer number. Provide either user_id or customer_number. schema: type: integer minimum: 1 responses: '200': description: Department customer pricing content: application/json: schema: $ref: '#/components/schemas/DepartmentCustomerPricingResponse' '400': $ref: '#/components/responses/BadRequest' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' put: tags: - Limited Backoffice summary: Replace limited-backoffice department customer pricing description: Replaces the complete override set atomically. Send the revision returned by GET as `expected_revision` to prevent stale writes. operationId: setLimitedBackofficeDepartmentCustomerPricing parameters: - name: departmentId in: path required: true schema: type: integer minimum: 1 requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/DepartmentCustomerPricingLimitedUpdateRequest' responses: '200': description: Department customer pricing updated content: application/json: schema: $ref: '#/components/schemas/DepartmentCustomerPricingResponse' '400': $ref: '#/components/responses/BadRequest' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/PricingConflict' /superuser/department/variables: get: tags: - Departments summary: Get department variables operationId: getDepartmentVariables parameters: - name: department_id in: query required: true schema: {type: integer} responses: '200': description: Success content: application/json: schema: {} post: tags: - Departments summary: Set department variable operationId: setDepartmentVariable requestBody: required: true content: application/json: schema: type: object required: [department_id, variable, value] properties: department_id: {type: integer} variable: {type: string} value: {type: string} responses: '200': description: Success content: application/json: schema: {} /departments/daily-reports: get: tags: - Departments summary: List daily reports operationId: listDailyReports responses: '200': description: Success content: application/json: schema: {} post: tags: - Departments summary: Add daily report operationId: addDailyReport requestBody: required: true content: application/json: schema: type: object required: [department_id, date, report] properties: department_id: {type: integer} date: {type: string} report: {type: string} responses: '200': description: Success content: application/json: schema: {} put: tags: - Departments summary: Edit daily report operationId: editDailyReport requestBody: required: true content: application/json: schema: type: object required: [id, report] properties: id: {type: integer} report: {type: string} responses: '200': description: Success content: application/json: schema: {} /departments/daily-reports/overview: get: tags: - Departments summary: Get daily report overview operationId: getDailyReportOverview parameters: - name: date in: query required: true schema: {type: string} - name: date_to in: query required: false schema: {type: string} - name: department_ids in: query required: true schema: type: string responses: '200': description: Success content: application/json: schema: $ref: '#/components/schemas/DepartmentDailyReportOverviewResponse' /departments/daily-reports/product-targets: put: tags: - Departments summary: Set daily report product target description: Requires set_department_daily_report_product_targets and department_access_:department_id. Send a null target_percentage to clear the target. operationId: setDailyReportProductTarget requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/DepartmentDailyReportProductTargetRequest' responses: '200': description: Daily report product target updated successfully content: application/json: schema: $ref: '#/components/schemas/DepartmentDailyReportProductTargetResponse' '400': $ref: '#/components/responses/BadRequest' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' /departments/daily-reports/get: get: tags: - Departments summary: Get daily report operationId: getDailyReport parameters: - name: department_id in: query required: true schema: {type: integer} - name: date in: query required: true schema: {type: string} responses: '200': description: Success content: application/json: schema: {} /departments/daily-reports/complaints: get: tags: - Departments summary: List or fetch daily report customer complaints operationId: listDailyReportComplaints parameters: - name: id in: query required: false schema: {type: integer} - name: page in: query required: false schema: {type: integer} - name: limit in: query required: false schema: {type: integer} - name: search in: query required: false schema: {type: string} - name: filters in: query required: false schema: {type: string} - name: order in: query required: false schema: {type: string} responses: '200': description: Success content: application/json: schema: $ref: '#/components/schemas/DepartmentDailyReportComplaintCollectionResponse' post: tags: - Departments summary: Create daily report customer complaint operationId: createDailyReportComplaint requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/DepartmentDailyReportComplaintCreateRequest' responses: '200': description: Success content: application/json: schema: $ref: '#/components/schemas/DepartmentDailyReportComplaintResponse' put: tags: - Departments summary: Update daily report customer complaint operationId: updateDailyReportComplaint requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/DepartmentDailyReportComplaintUpdateRequest' responses: '200': description: Success content: application/json: schema: $ref: '#/components/schemas/DepartmentDailyReportComplaintResponse' delete: tags: - Departments summary: Delete daily report customer complaint operationId: deleteDailyReportComplaint parameters: - name: id in: query required: true schema: {type: integer} responses: '200': description: Success content: application/json: schema: $ref: '#/components/schemas/DepartmentDailyReportComplaintDeleteResponse' /departments/daily-reports/complaints/customers: get: tags: - Departments summary: Search selectable customers for daily report complaints operationId: searchDailyReportComplaintCustomers parameters: - name: search in: query required: true schema: type: string minLength: 2 - name: limit in: query required: false schema: type: integer minimum: 1 maximum: 20 default: 10 responses: '200': description: Success content: application/json: schema: $ref: '#/components/schemas/DepartmentDailyReportComplaintCustomerSearchResponse' /departments/daily-reports/product-count: get: tags: - Departments summary: Get product count for daily reports operationId: getDailyReportProductCount parameters: - name: date in: query required: true schema: {type: string} - name: date_to in: query required: false schema: {type: string} - name: department_id in: query required: true schema: {type: integer} - name: product_id in: query required: true schema: {type: integer} responses: '200': description: Success content: application/json: schema: {} /departments/daily-reports/transaction-count: get: tags: - Departments summary: Get transaction count for daily reports operationId: getDailyReportTransactionCount parameters: - name: date in: query required: true schema: {type: string} - name: date_to in: query required: false schema: {type: string} - name: department_id in: query required: true schema: {type: integer} responses: '200': description: Success content: application/json: schema: $ref: '#/components/schemas/DepartmentDailyReportTransactionCountResponse' /departments/daily-reports/outside-hours-trend: get: tags: - Departments summary: Get outside-hours trend for daily reports operationId: getDailyReportOutsideHoursTrend parameters: - name: date in: query required: true schema: {type: string} - name: date_to in: query required: true schema: {type: string} - name: department_ids in: query required: true schema: type: string responses: '200': description: Success content: application/json: schema: $ref: '#/components/schemas/DepartmentDailyReportOutsideHoursTrendResponse' /departments/daily-reports/bookings-count: get: tags: - Departments summary: Get bookings count for daily reports operationId: getDailyReportBookingsCount parameters: - name: date in: query required: true schema: {type: string} - name: department_id in: query required: true schema: {type: integer} responses: '200': description: Success content: application/json: schema: {} # Account Security - Passkeys /account/security/passkeys: get: tags: - Security summary: List passkeys for the authenticated user operationId: listPasskeys responses: '200': description: A list of passkeys content: application/json: schema: type: array items: $ref: '#/components/schemas/Passkey' '400': description: Invalid session or request content: application/json: schema: $ref: '#/components/schemas/Error' post: tags: - Security summary: Create/add a passkey for the authenticated user operationId: createPasskey requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/PasskeyCreateRequest' responses: '200': description: Passkey created content: application/json: schema: type: object properties: id: type: integer '400': description: Invalid session or request content: application/json: schema: $ref: '#/components/schemas/Error' /account/security/passkeys/{id}: patch: tags: - Security summary: Rename a passkey operationId: renamePasskey parameters: - in: path name: id required: true schema: type: integer requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/PasskeyRenameRequest' responses: '200': description: Passkey renamed content: application/json: schema: {} '404': description: Not found content: application/json: schema: $ref: '#/components/schemas/Error' delete: tags: - Security summary: Delete a passkey operationId: deletePasskey parameters: - in: path name: id required: true schema: type: integer responses: '200': description: Passkey deleted content: application/json: schema: {} '404': description: Not found content: application/json: schema: $ref: '#/components/schemas/Error' /superuser/releases/operations: get: tags: - Release Manager summary: List release operation runs operationId: listReleaseOperations parameters: - in: query name: channel_id schema: type: integer - in: query name: operation_type schema: type: string - in: query name: status schema: type: string - in: query name: limit schema: type: integer minimum: 1 maximum: 200 responses: '200': description: Release operation runs content: application/json: schema: type: object properties: success: type: boolean data: type: array items: type: object additionalProperties: true '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' /superuser/releases/operations/{id}: get: tags: - Release Manager summary: Get release operation details operationId: getReleaseOperation parameters: - in: path name: id required: true schema: type: integer responses: '200': description: Release operation details content: application/json: schema: type: object properties: success: type: boolean data: type: object additionalProperties: true '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' /superuser/releases/test-runs: post: tags: - Release Manager summary: Run Release Manager diagnostics operationId: runReleaseTest requestBody: required: false content: application/json: schema: type: object additionalProperties: true responses: '202': description: Release test operation started content: application/json: schema: type: object properties: success: type: boolean data: type: object additionalProperties: true '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '409': $ref: '#/components/responses/Conflict' /superuser/releases/channels/{id}/sync: post: tags: - Release Manager summary: Sync latest branch commits into a release channel operationId: syncReleaseChannel parameters: - in: path name: id required: true schema: type: integer responses: '202': description: Channel sync operation started content: application/json: schema: type: object properties: success: type: boolean data: type: object additionalProperties: true '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '409': $ref: '#/components/responses/Conflict' /superuser/releases/issues/actions: post: tags: - Release Manager summary: Run a Release Manager issue action operationId: runReleaseIssueAction requestBody: required: true content: application/json: schema: type: object properties: issue_key: type: string action_id: type: string inputs: type: object additionalProperties: true confirm: type: boolean additionalProperties: true responses: '200': description: Issue action result content: application/json: schema: type: object properties: success: type: boolean data: type: object additionalProperties: true '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' components: securitySchemes: BearerAuth: type: http scheme: bearer bearerFormat: JWT description: JWT token obtained from /auth/login or /auth/employee/login parameters: PageParam: name: page in: query description: Page number for pagination schema: type: integer minimum: 1 default: 1 PerPageParam: name: per_page in: query description: Number of items per page schema: type: integer minimum: 1 maximum: 100 default: 30 SearchParam: name: search in: query description: Search query string schema: type: string LimitParam: name: limit in: query description: Number of items per page schema: type: integer minimum: 1 maximum: 1000 default: 100 FiltersParam: name: filters in: query description: Filters for the list (e.g., module:selfserve,status_code:200) schema: type: string XCustomerNumber: name: X-Customer-Number in: header required: false description: | Target customer number for subuser requests. Ignored for classic user sessions. Required on customer-scoped endpoints when authenticated as a subuser unless the target customer can be inferred from context. schema: type: integer SuperuserReplicationKindParam: name: kind in: path required: true schema: type: string enum: [databases, redis, minio] SuperuserReplicationHostIdParam: name: id in: path required: true schema: type: integer minimum: 1 responses: BadRequest: description: Bad request - Invalid input parameters content: application/json: schema: $ref: '#/components/schemas/Error' Conflict: description: Conflict - Request could not be completed due to current resource state content: application/json: schema: $ref: '#/components/schemas/Error' PricingConflict: description: Pricing is unavailable in the current state or `expected_revision` is stale. Stale writes return code `pricing_revision_conflict` and the current revision. content: application/json: schema: oneOf: - $ref: '#/components/schemas/Error' - $ref: '#/components/schemas/PricingRevisionConflictResponse' Unauthorized: description: Unauthorized - Invalid or missing authentication token content: application/json: schema: $ref: '#/components/schemas/Error' Forbidden: description: Forbidden - Insufficient permissions content: application/json: schema: $ref: '#/components/schemas/Error' NotFound: description: Not found - Resource does not exist content: application/json: schema: $ref: '#/components/schemas/Error' ServiceUnavailable: description: Service unavailable - Required async queue dependencies are unavailable content: application/json: schema: $ref: '#/components/schemas/Error' InternalServerError: description: Internal server error content: application/json: schema: $ref: '#/components/schemas/Error' schemas: Error: type: object properties: error: type: string description: Error message code: type: integer description: HTTP status code CustomerProductImpactAttribute: type: string enum: [restrictAdditionalServices, restrictTankCleaning, restrictSpotFree, restrictInteriorCleaning, onlyTankCleaning] CustomerRuleProductCollection: type: object required: [id, name, sort_order, product_ids] properties: id: { type: integer } name: { type: string, minLength: 1, maxLength: 191 } sort_order: { type: integer } product_ids: type: array uniqueItems: true items: { type: integer } CustomerRuleProductCollectionInput: type: object required: [name, sort_order, product_ids] properties: id: { type: integer, nullable: true } name: { type: string, minLength: 1, maxLength: 191 } sort_order: { type: integer } product_ids: type: array uniqueItems: true items: { type: integer } CustomerRuleProductRestriction: type: object required: [attribute, version, collections, disabled_product_ids] properties: attribute: { $ref: '#/components/schemas/CustomerProductImpactAttribute' } version: { type: integer, minimum: 1 } collections: type: array items: { $ref: '#/components/schemas/CustomerRuleProductCollection' } disabled_product_ids: type: array uniqueItems: true items: { type: integer } CustomerRuleProductRestrictionUpdateRequest: type: object required: [version, collections] properties: version: { type: integer, minimum: 1 } collections: type: array items: { $ref: '#/components/schemas/CustomerRuleProductCollectionInput' } CustomerRuleProduct: type: object required: [id, name, category_id, category_name, active] properties: id: { type: integer } name: { type: string } category_id: { type: integer } category_name: { type: string } active: { type: boolean } CustomerRuleProductRestrictionListResponse: type: object required: [success, data] properties: success: { type: boolean } data: type: object required: [rules, products] properties: rules: type: array items: { $ref: '#/components/schemas/CustomerRuleProductRestriction' } products: type: array items: { $ref: '#/components/schemas/CustomerRuleProduct' } CustomerRuleProductRestrictionResponse: type: object required: [success, data] properties: success: { type: boolean } data: { $ref: '#/components/schemas/CustomerRuleProductRestriction' } CustomerAttribute: type: object required: [id, user_id, attribute, product_restriction] properties: id: { type: integer } user_id: { type: integer } attribute: { type: string } created_at: { type: string, nullable: true } product_restriction: nullable: true allOf: - $ref: '#/components/schemas/CustomerRuleProductRestriction' CustomerAttributesResponse: type: object required: [success, data] properties: success: { type: boolean } data: type: array items: { $ref: '#/components/schemas/CustomerAttribute' } CustomerAttributeMutationRequest: type: object required: [attribute] properties: user_id: { type: integer } customer_number: { type: integer } attribute: { type: string } anyOf: - required: [user_id] - required: [customer_number] CustomerRuleProductRestrictedResponse: type: object required: [success, data] properties: success: { type: boolean, enum: [false] } data: type: object required: [code, message, product_id, rules, collections] properties: code: { type: string, enum: [CUSTOMER_RULE_PRODUCT_RESTRICTED] } message: { type: string } product_id: { type: integer } rules: type: array items: { $ref: '#/components/schemas/CustomerProductImpactAttribute' } collections: type: array items: { type: integer } DepartmentCustomerPricingUpdateRequest: type: object required: - department_id - overrides properties: department_id: type: integer minimum: 1 user_id: type: integer minimum: 1 customer_number: type: integer minimum: 1 expected_revision: $ref: '#/components/schemas/PricingRevision' overrides: type: array items: $ref: '#/components/schemas/DepartmentCustomerPricingOverrideInput' DepartmentCustomerPricingLimitedUpdateRequest: type: object required: - overrides properties: user_id: type: integer minimum: 1 customer_number: type: integer minimum: 1 expected_revision: $ref: '#/components/schemas/PricingRevision' overrides: type: array items: $ref: '#/components/schemas/DepartmentCustomerPricingOverrideInput' DepartmentCustomerPricingOverrideInput: type: object required: - is_category - product_or_category_id properties: is_category: type: boolean product_or_category_id: oneOf: - type: integer - type: string description: Product id, category id, or `global` for the customer-wide department discount. discount: type: integer minimum: 0 maximum: 100 percentage: type: integer minimum: 0 maximum: 100 fixed_price: type: integer nullable: true minimum: 0 description: Product-only fixed price. A row must use either a positive discount or a fixed price, not both. DepartmentCustomerPricingOverride: allOf: - $ref: '#/components/schemas/DepartmentCustomerPricingOverrideInput' - type: object properties: id: type: integer department_id: type: integer user_id: type: integer percentage: type: integer minimum: 0 maximum: 100 created_at: type: string updated_at: type: string DepartmentCustomerPricingProduct: type: object properties: id: type: integer name: type: string description: type: string category: type: integer apply_category_discount: type: boolean base_price: type: integer department_price: type: integer nullable: true effective_price: type: integer missing_department_price: type: boolean DepartmentCustomerPricingCategory: type: object properties: id: type: integer name: type: string description: type: string products: type: array items: $ref: '#/components/schemas/DepartmentCustomerPricingProduct' DepartmentCustomerPricingResponse: type: object properties: success: type: boolean data: type: object properties: department: type: object properties: id: type: integer name: type: string description: type: string custom_pricing_only: type: boolean customer: type: object properties: id: type: integer customer_number: type: integer display_name: type: string revision: $ref: '#/components/schemas/PricingRevision' overrides: type: array items: $ref: '#/components/schemas/DepartmentCustomerPricingOverride' categories: type: array items: $ref: '#/components/schemas/DepartmentCustomerPricingCategory' meta: type: object additionalProperties: true PricingRevision: type: string pattern: '^[a-f0-9]{64}$' description: Opaque SHA-256 content revision. Return it as `expected_revision` on the next update. PricingRevisionConflictResponse: type: object required: [success, data] properties: success: type: boolean enum: [false] data: type: object required: [message, code, current_revision] properties: message: type: string enum: [Pricing has changed. Reload and try again.] code: type: string enum: [pricing_revision_conflict] current_revision: $ref: '#/components/schemas/PricingRevision' meta: type: object additionalProperties: true LimitedBackofficeDepartmentPriceInput: type: object required: [product_id, price] properties: product_id: type: integer minimum: 1 price: type: integer minimum: 0 LimitedBackofficeDepartmentPricesUpdateRequest: type: object required: [prices] properties: expected_revision: $ref: '#/components/schemas/PricingRevision' prices: type: array minItems: 1 items: $ref: '#/components/schemas/LimitedBackofficeDepartmentPriceInput' LimitedBackofficeDepartmentPricesResponse: type: object properties: success: type: boolean data: type: object properties: department: type: object properties: id: { type: integer } name: { type: string } description: { type: string } custom_pricing_only: { type: boolean } revision: $ref: '#/components/schemas/PricingRevision' categories: type: array items: type: object properties: id: { type: integer } name: { type: string } description: { type: string } products: type: array items: type: object properties: id: { type: integer } name: { type: string } description: { type: string } price: { type: integer } meta: type: object additionalProperties: true includes: type: object additionalProperties: true ErrorReportSubmissionRequest: type: object required: - before_error - expected - actual - data_collection_accepted properties: before_error: type: string maxLength: 4000 description: What the user was doing before the error occurred expected: type: string maxLength: 4000 description: What the user expected would happen actual: type: string maxLength: 4000 description: What actually happened data_collection_accepted: type: boolean description: Required acceptance of collecting diagnostic error data and a screenshot when one can be attached screenshot: type: string nullable: true description: Optional PNG, JPEG, or WebP data URI of the current app viewport. Reports are accepted without an attachment when capture or upload fails. route_path: type: string nullable: true page_url: type: string nullable: true release_trace_id: type: string nullable: true request_errors: type: array items: type: object additionalProperties: true vue_errors: type: array items: type: object additionalProperties: true context: type: object additionalProperties: true ErrorReportStatusUpdateRequest: type: object required: - status properties: status: type: string enum: [open, resolved] resolution_note: type: string nullable: true maxLength: 2000 ErrorReportResponse: type: object properties: success: type: boolean data: $ref: '#/components/schemas/ErrorReport' meta: type: object additionalProperties: true includes: type: object additionalProperties: true ErrorReportListResponse: type: object properties: success: type: boolean data: type: object properties: items: type: array items: $ref: '#/components/schemas/ErrorReport' counts: type: object properties: open: type: integer resolved: type: integer all: type: integer limit: type: integer offset: type: integer meta: type: object additionalProperties: true includes: type: object additionalProperties: true ErrorReport: type: object properties: id: type: integer status: type: string enum: [open, resolved] reporter: type: object additionalProperties: true route_path: type: string nullable: true page_url: type: string nullable: true release_trace_id: type: string nullable: true frontend_version: type: string nullable: true api_version: type: string nullable: true screenshot: type: object nullable: true additionalProperties: true answers: type: object properties: before_error: type: string expected: type: string actual: type: string request_error_count: type: integer vue_error_count: type: integer request_errors: type: array items: type: object additionalProperties: true vue_errors: type: array items: type: object additionalProperties: true runtime_context: type: object additionalProperties: true resolved_at: type: string nullable: true resolved_by_user_id: type: integer nullable: true created_at: type: string updated_at: type: string nullable: true SuperuserSystemStatusResponse: type: object properties: success: type: boolean data: $ref: '#/components/schemas/SuperuserSystemStatusPayload' meta: type: object additionalProperties: true includes: type: object additionalProperties: true required: - success - data - meta - includes SuperuserReplicationResponse: type: object properties: success: type: boolean data: $ref: '#/components/schemas/SuperuserReplicationSummary' meta: type: object additionalProperties: true includes: type: object additionalProperties: true SuperuserReplicationHostResponse: type: object properties: success: type: boolean data: $ref: '#/components/schemas/SuperuserReplicationHost' meta: type: object additionalProperties: true includes: type: object additionalProperties: true SuperuserReplicationOperationResponse: type: object properties: success: type: boolean data: type: object additionalProperties: true meta: type: object additionalProperties: true includes: type: object additionalProperties: true SuperuserCoolifyResponse: type: object properties: success: type: boolean data: type: object properties: generated_at: type: string format: date-time instances: type: array items: $ref: '#/components/schemas/SuperuserCoolifyInstance' targets: type: array items: $ref: '#/components/schemas/SuperuserCoolifyTarget' availability: type: object additionalProperties: true load_balancer: $ref: '#/components/schemas/SuperuserCoolifyLoadBalancer' meta: type: object additionalProperties: true includes: type: object additionalProperties: true SuperuserCoolifyInstanceResponse: type: object properties: success: type: boolean data: $ref: '#/components/schemas/SuperuserCoolifyInstance' meta: type: object additionalProperties: true includes: type: object additionalProperties: true SuperuserCoolifyTargetsResponse: type: object properties: success: type: boolean data: type: array items: $ref: '#/components/schemas/SuperuserCoolifyTarget' meta: type: object additionalProperties: true includes: type: object additionalProperties: true SuperuserCoolifyOperationResponse: type: object properties: success: type: boolean data: type: object additionalProperties: true meta: type: object additionalProperties: true includes: type: object additionalProperties: true SuperuserCoolifyLoadBalancerResponse: type: object properties: success: type: boolean data: $ref: '#/components/schemas/SuperuserCoolifyLoadBalancer' meta: type: object additionalProperties: true includes: type: object additionalProperties: true SuperuserCoolifyLoadBalancerReconcileResponse: type: object properties: success: type: boolean data: type: object properties: ok: type: boolean dry_run: type: boolean mutated: type: boolean config: $ref: '#/components/schemas/SuperuserCoolifyLoadBalancerConfig' load_balancer: $ref: '#/components/schemas/SuperuserCoolifyLoadBalancerPublicState' drift: type: object additionalProperties: true planned: type: array items: type: object additionalProperties: true applied: type: array items: type: object additionalProperties: true skipped: type: array items: type: object additionalProperties: true errors: type: array items: type: object additionalProperties: true gateways: type: array items: $ref: '#/components/schemas/SuperuserCoolifyGateway' meta: type: object additionalProperties: true includes: type: object additionalProperties: true SuperuserCoolifyGatewayRouteDeployResponse: type: object properties: success: type: boolean data: type: object properties: ok: type: boolean dry_run: type: boolean mutated: type: boolean public_host: type: string public_url: type: string planned: type: array items: type: object additionalProperties: true applied: type: array items: type: object additionalProperties: true skipped: type: array items: type: object additionalProperties: true errors: type: array items: type: object additionalProperties: true warnings: type: array items: type: string coverage: type: object additionalProperties: true gateways: type: array items: $ref: '#/components/schemas/SuperuserCoolifyGateway' meta: type: object additionalProperties: true includes: type: object additionalProperties: true SuperuserCoolifyGatewaysResponse: type: object properties: success: type: boolean data: type: array items: $ref: '#/components/schemas/SuperuserCoolifyGateway' meta: type: object additionalProperties: true includes: type: object additionalProperties: true SuperuserCoolifyGatewayResponse: type: object properties: success: type: boolean data: $ref: '#/components/schemas/SuperuserCoolifyGateway' meta: type: object additionalProperties: true includes: type: object additionalProperties: true SuperuserCoolifyLoadBalancer: type: object properties: configured: type: boolean status: type: string enum: [not_configured, ok, degraded, down] config: $ref: '#/components/schemas/SuperuserCoolifyLoadBalancerConfig' gateways: type: array items: $ref: '#/components/schemas/SuperuserCoolifyGateway' load_balancer: nullable: true allOf: - $ref: '#/components/schemas/SuperuserCoolifyLoadBalancerPublicState' drift: type: object additionalProperties: true last_error: type: string nullable: true SuperuserCoolifyLoadBalancerConfig: type: object properties: automation_enabled: type: boolean automation_mode: type: string enum: [report_only, enforce] load_balancer_id: type: string public_gateway_host: type: string token_set: type: boolean token_source: type: string nullable: true required_services: type: array items: $ref: '#/components/schemas/SuperuserCoolifyLoadBalancerService' SuperuserCoolifyLoadBalancerPublicState: type: object properties: id: type: integer nullable: true name: type: string ipv4: type: string nullable: true ipv6: type: string nullable: true location: type: string nullable: true algorithm: type: string nullable: true targets: type: array items: type: string services: type: array items: $ref: '#/components/schemas/SuperuserCoolifyLoadBalancerService' SuperuserCoolifyLoadBalancerService: type: object properties: protocol: type: string enum: [http, tcp] listen_port: type: integer destination_port: type: integer proxyprotocol: type: boolean SuperuserCoolifyGateway: type: object properties: id: type: integer instance_id: type: integer nullable: true hostname: type: string target_ip: type: string enabled: type: boolean priority: type: integer health_state: type: string lb_state: type: string last_probe: type: object nullable: true additionalProperties: true last_probed_at: type: string nullable: true last_reconciled_at: type: string nullable: true created_at: type: string nullable: true updated_at: type: string nullable: true SuperuserCoolifyGatewaySaveRequest: type: object required: - hostname - target_ip properties: id: type: integer instance_id: type: integer nullable: true hostname: type: string target_ip: type: string enabled: type: boolean default: true priority: type: integer default: 100 SuperuserCoolifyInstance: type: object properties: id: type: integer label: type: string base_url: type: string api_token_set: type: boolean default_project_uuid: type: string nullable: true default_environment_uuid: type: string nullable: true default_environment_name: type: string nullable: true default_server_uuid: type: string nullable: true default_destination_uuid: type: string nullable: true status: type: string last_checked_at: type: string nullable: true last_error: type: string nullable: true SuperuserCoolifyPlacementResponse: type: object properties: success: type: boolean data: type: object properties: generated_at: type: string instance: $ref: '#/components/schemas/SuperuserCoolifyInstance' servers: type: array items: $ref: '#/components/schemas/SuperuserCoolifyPlacementServer' projects: type: array items: $ref: '#/components/schemas/SuperuserCoolifyPlacementProject' environments: type: array items: $ref: '#/components/schemas/SuperuserCoolifyPlacementEnvironment' destination_discovery_supported: type: boolean errors: type: object additionalProperties: true SuperuserCoolifyPlacementServer: type: object properties: id: type: integer nullable: true uuid: type: string name: type: string description: type: string ip: type: string user: type: string port: type: integer nullable: true proxy_type: type: string swarm_cluster: type: string is_reachable: type: boolean nullable: true is_usable: type: boolean nullable: true SuperuserCoolifyPlacementProject: type: object properties: id: type: integer nullable: true uuid: type: string name: type: string description: type: string SuperuserCoolifyPlacementEnvironment: type: object properties: id: type: integer nullable: true uuid: type: string name: type: string description: type: string project_id: type: integer nullable: true project_uuid: type: string project_name: type: string SuperuserCoolifyTarget: type: object properties: id: type: integer instance_id: type: integer instance_label: type: string kind: type: string enum: [database, redis, minio] label: type: string role: type: string enum: [replica] server_uuid: type: string nullable: true project_uuid: type: string nullable: true environment_uuid: type: string nullable: true environment_name: type: string nullable: true destination_uuid: type: string nullable: true resource_uuid: type: string nullable: true resource_type: type: string resource_name: type: string nullable: true deployment_status: type: string availability_state: type: string enum: [protected, degraded, failover_ready, failover_blocked, failing_over, destructive_action_required] last_reconcile_status: type: string nullable: true replication: type: object additionalProperties: true SuperuserCoolifyInstanceCreateRequest: type: object required: - label - base_url - api_token properties: label: type: string base_url: type: string api_token: type: string format: password SuperuserCoolifyTargetCreateRequest: allOf: - $ref: '#/components/schemas/SuperuserReplicationHostCreateRequest' - type: object required: - kind - host properties: instance_id: type: integer kind: type: string enum: [database, redis, minio] role: type: string enum: [replica] default: replica server_uuid: type: string project_uuid: type: string environment_uuid: type: string environment_name: type: string destination_uuid: type: string deploy: type: boolean default: false SuperuserReplicationComposeTemplateResponse: type: object properties: success: type: boolean data: $ref: '#/components/schemas/SuperuserReplicationComposeTemplate' meta: type: object additionalProperties: true includes: type: object additionalProperties: true SuperuserReplicationComposeTemplate: type: object properties: kind: type: string enum: [database, redis, minio] engine: type: string enum: [mariadb, redis, minio] role: type: string enum: [primary, replica] service_name: type: string host_port: type: integer console_port: type: integer nullable: true server_id: type: integer nullable: true compose: type: string description: Complete docker-compose.yml content with secret environment placeholders. env: type: string description: Example .env content for the placeholders used by compose. seed_command: type: string description: One-time MariaDB seed command to initialize a replica from the primary before provisioning. credentials: $ref: '#/components/schemas/SuperuserReplicationGeneratedCredentials' steps: type: array items: type: string SuperuserReplicationGeneratedCredentials: type: object properties: label: type: string host: type: string port: type: integer endpoint: type: string scheme: type: string enum: [http, https] buckets: type: array items: type: string console_port: type: integer replication_transfer_limit: type: string nullable: true space_headroom_percent: type: number format: float database: oneOf: - type: string - type: integer username: type: string password: type: string format: password admin_username: type: string admin_password: type: string format: password replication_username: type: string replication_password: type: string format: password ssl_mode: type: string allow_preseeded_replica: type: boolean SuperuserReplicationSummary: type: object properties: generated_at: type: string format: date-time database: $ref: '#/components/schemas/SuperuserReplicationKindSummary' redis: $ref: '#/components/schemas/SuperuserReplicationKindSummary' minio: $ref: '#/components/schemas/SuperuserReplicationKindSummary' write_freeze: type: object additionalProperties: true SuperuserReplicationKindSummary: type: object properties: primary: $ref: '#/components/schemas/SuperuserReplicationHost' nullable: true hosts: type: array items: $ref: '#/components/schemas/SuperuserReplicationHost' replication: $ref: '#/components/schemas/SuperuserReplicationStatus' SuperuserReplicationStatus: type: object properties: status: type: string enum: [ok, degraded, down, not_configured] min_percent: type: number format: float average_percent: type: number format: float replicas: type: array items: $ref: '#/components/schemas/SuperuserReplicationHost' blockers: type: array items: type: string SuperuserReplicationHost: type: object properties: id: type: integer kind: type: string enum: [database, redis, minio] label: type: string host: type: string port: type: integer database: oneOf: - type: string - type: integer nullable: true endpoint: type: string nullable: true scheme: type: string enum: [http, https] nullable: true buckets: type: array items: type: string console_port: type: integer nullable: true replication_transfer_limit: type: string nullable: true description: MinIO replication and seed bandwidth cap passed to mc --limit-upload/--limit-download, for example 25Mi. Use 0 to disable. space_headroom_percent: type: number format: float nullable: true role: type: string enum: [primary, replica, inactive] status: type: string replication_source_id: type: integer nullable: true replication_percent: type: number format: float last_status: type: object additionalProperties: true credential_summary: type: object additionalProperties: true deployment_provider: type: string enum: [manual, coolify] coolify: type: object nullable: true additionalProperties: true availability_state: type: string nullable: true enum: [protected, degraded, failover_ready, failover_blocked, failing_over, destructive_action_required] SuperuserReplicationHostCreateRequest: type: object required: - host - port properties: label: type: string host: type: string description: Hostname or MinIO endpoint. MinIO hosts may include http(s) scheme; the backend stores the host without scheme. endpoint: type: string description: Optional MinIO endpoint alias for host. port: type: integer database: oneOf: - type: string - type: integer username: type: string description: Database/Redis username or MinIO access key. password: type: string format: password description: Database/Redis password or MinIO secret key. scheme: type: string enum: [http, https] description: MinIO endpoint scheme. buckets: type: array items: type: string description: MinIO buckets to replicate. console_port: type: integer description: Optional MinIO console port for UI display. replication_transfer_limit: type: string description: Optional MinIO replication and seed bandwidth cap. Defaults to 25Mi. Use 0 to disable. space_headroom_percent: type: number format: float description: MinIO free-space headroom required before provisioning. Defaults to 20. admin_username: type: string admin_password: type: string format: password replication_username: type: string replication_password: type: string format: password ssl_mode: type: string deployment_provider: type: string enum: [manual, coolify] options: type: object properties: allow_preseeded_replica: type: boolean description: Allow configuring replication when the replica has already been safely seeded outside the orchestrator. Required for MariaDB, which does not support MySQL Clone. scheme: type: string enum: [http, https] buckets: type: array items: type: string console_port: type: integer replication_transfer_limit: type: string space_headroom_percent: type: number format: float additionalProperties: true SuperuserReplicationHostRenameRequest: type: object required: - label properties: label: type: string minLength: 1 maxLength: 128 SuperuserReplicationUnsavedCredentialTestRequest: allOf: - $ref: '#/components/schemas/SuperuserReplicationHostCreateRequest' - type: object required: - kind properties: kind: type: string enum: [database, databases, mysql, redis, minio, s3, object-storage, object_storage] role: type: string enum: [primary, replica] SuperuserReplicationComposeTemplateRequest: type: object properties: kind: type: string enum: [database, databases, mysql, redis, minio, s3, object-storage, object_storage] default: database role: type: string enum: [primary, replica] default: replica service_name: type: string volume_name: type: string image: type: string database: type: string description: MariaDB database name to create on first startup. username: type: string description: MariaDB application username to create on first startup. password: type: string format: password description: Optional application password to reuse instead of generating one. admin_password: type: string format: password description: Optional MariaDB root password to reuse instead of generating one. replication_username: type: string description: Replication username to place in generated credentials. replication_password: type: string format: password description: Optional replication password to reuse instead of generating one. host_port: type: integer minimum: 1 maximum: 65535 server_id: type: integer minimum: 1 description: MariaDB server-id. Must be unique across the primary and replicas. primary_host: type: string description: Redis primary host used when generating a Redis replica template. primary_port: type: integer minimum: 1 maximum: 65535 description: Redis primary port used when generating a Redis replica template. primary_password: type: string format: password description: Redis primary password used in the generated Redis replica .env file. If omitted, the .env keeps the value blank for manual entry. primary_username: type: string description: Optional Redis primary ACL username used in the generated Redis replica .env file. Leave blank or default for the default Redis user. buckets: type: array items: type: string description: MinIO buckets to create, version, and replicate. console_port: type: integer minimum: 1 maximum: 65535 description: MinIO console port exposed by the generated compose service. replication_transfer_limit: type: string description: MinIO replication and seed bandwidth cap included in generated credentials. Defaults to 25Mi. Use 0 to disable. SuperuserSystemStatusPayload: type: object properties: overall_status: $ref: '#/components/schemas/SuperuserSystemStatusEnum' generated_at: type: string format: date-time refresh_after_seconds: type: integer runtime: type: object properties: cpu: $ref: '#/components/schemas/SuperuserRuntimeMetric' memory: $ref: '#/components/schemas/SuperuserRuntimeMetric' disk: $ref: '#/components/schemas/SuperuserRuntimeMetric' dependencies: type: object properties: database: $ref: '#/components/schemas/SuperuserDependencyStatus' redis: $ref: '#/components/schemas/SuperuserDependencyStatus' minio: $ref: '#/components/schemas/SuperuserMinioDependencyStatus' modules: type: array items: $ref: '#/components/schemas/SuperuserModuleStatus' sessions: $ref: '#/components/schemas/SuperuserSessionStatus' warnings: type: array items: type: string required: - overall_status - generated_at - refresh_after_seconds - runtime - dependencies - modules - sessions - warnings SuperuserSystemStatusEnum: type: string enum: [ok, degraded, down] SuperuserModuleStatusEnum: type: string enum: [disabled, not_configured, configured, ok, degraded, down] SuperuserModuleUsage: type: object properties: provider: type: string enum: [licenseplaterecognizer] calls_used: type: integer minimum: 0 quota_calls: type: integer minimum: 1 calls_remaining: type: integer minimum: 0 usage_percent: type: number format: float minimum: 0 version: type: string nullable: true required: - provider - calls_used - quota_calls - calls_remaining - usage_percent SuperuserRuntimeMetric: type: object properties: status: $ref: '#/components/schemas/SuperuserSystemStatusEnum' usage_percent: type: number format: float nullable: true used_bytes: type: integer nullable: true free_bytes: type: integer nullable: true total_bytes: type: integer nullable: true path: type: string nullable: true source: type: string nullable: true checked_at: type: string format: date-time SuperuserDependencyStatus: type: object properties: status: $ref: '#/components/schemas/SuperuserSystemStatusEnum' latency_ms: type: number format: float nullable: true database: oneOf: - type: integer - type: string nullable: true server_version: type: string nullable: true http_status: type: integer nullable: true checked_at: type: string format: date-time error: type: string nullable: true replication: $ref: '#/components/schemas/SuperuserReplicationStatus' SuperuserMinioDependencyStatus: type: object properties: status: $ref: '#/components/schemas/SuperuserSystemStatusEnum' latency_ms: type: number format: float nullable: true endpoint: type: string nullable: true http_status: type: integer nullable: true buckets: type: array items: type: object properties: name: type: string status: type: string error: type: string nullable: true checked_at: type: string format: date-time error: type: string nullable: true replication: $ref: '#/components/schemas/SuperuserReplicationStatus' SuperuserModuleStatus: type: object properties: key: type: string enabled: type: boolean configured: type: boolean probe_supported: type: boolean status: $ref: '#/components/schemas/SuperuserModuleStatusEnum' status_reason: type: string nullable: true status_reason_key: type: string nullable: true status_reason_params: type: object additionalProperties: true usage: $ref: '#/components/schemas/SuperuserModuleUsage' checked_at: type: string format: date-time required: - key - enabled - configured - probe_supported - status - checked_at SuperuserSessionStatus: type: object properties: active_window_minutes: type: integer active_users: type: integer active_sessions: type: integer recent_sessions: type: array items: type: object properties: session_kind: type: string principal_id: type: integer display_name: type: string context_label: type: string nullable: true customer_number_context: type: integer nullable: true device_type: type: string user_agent: type: string last_route: type: string first_seen_at: type: string format: date-time nullable: true last_seen_at: type: string format: date-time nullable: true active: type: boolean required: - active_window_minutes - active_users - active_sessions - recent_sessions SystemSearchEntityType: type: string enum: - objects - module_config - orders - order_items - customers - employees - users - subusers - customer_discounts - customer_fixed_prices - departments - permissions - roles - invoices - vehicles - bookings - bookings_new - branding - categories - currency_conversion_rates - customer_codes - customer_default_department - customer_notes - customer_vehicles_addons - department_categories - department_daily_reports - department_gates - department_goals - department_lanes - department_notification_sms - department_relays - department_selfserve_condition_rules - department_selfserve_conditions - department_selfserve_questions - department_selfserve_tasks - department_selfserve_vehicle_conditions - department_time_bookings_entries - department_time_bookings_opening_hours - department_time_bookings_types - department_variables - fxratesapi_conversion_rates - module_action_logs - motorapi_lookups - notifications - order_bookings - plate_scanners - plate_scans - product_options - products - stripe_module_customers - stripe_module_orders - stripe_payment_intents - subuser_grants - xlvask_customers - xlvask_potential_order_matches - xlvask_usage_log_wash_items - xlvask_usage_logs - xlvask_vehicle_types - xlvask_vehicles SystemSearchRequest: type: object required: - query properties: query: type: string description: Free-text query to search for. Customer lookups include local e-conomic index fields and deterministic lexical synonym expansion (for example `rabat` -> `discount`). include_types: type: array items: $ref: '#/components/schemas/SystemSearchEntityType' description: Limit search to these entity types. exclude_types: type: array items: $ref: '#/components/schemas/SystemSearchEntityType' description: Exclude these entity types from search. include_associations: type: boolean default: true description: Include associated records for matched core entities. max_results: type: integer minimum: 1 maximum: 50 default: 50 description: Maximum number of relevance-ranked results to return. Values above 50 are capped to 50. SystemSearchResult: type: object properties: entity_type: $ref: '#/components/schemas/SystemSearchEntityType' entity_id: type: string title: type: string description: type: string customer_number: type: integer nullable: true department_id: type: integer nullable: true score: type: integer association_reason: type: string nullable: true payload: type: object additionalProperties: true required: - entity_type - entity_id - title - score SystemSearchMeta: type: object properties: query: type: string max_results: type: integer returned: type: integer truncated: type: boolean allowed_types: type: array items: $ref: '#/components/schemas/SystemSearchEntityType' cache: type: object properties: hit: type: boolean required: [hit] required: - query - max_results - returned - truncated - allowed_types - cache SystemSearchPayload: type: object properties: results: type: array items: $ref: '#/components/schemas/SystemSearchResult' grouped_results: type: object additionalProperties: type: array items: $ref: '#/components/schemas/SystemSearchResult' meta: $ref: '#/components/schemas/SystemSearchMeta' required: - results - grouped_results - meta SystemSearchResponse: type: object properties: success: type: boolean data: $ref: '#/components/schemas/SystemSearchPayload' meta: type: object additionalProperties: true includes: type: object additionalProperties: true required: - success - data - meta - includes SystemSearchCacheClearResponse: type: object properties: success: type: boolean data: type: object properties: message: type: string query_cache_cleared: type: boolean intent_cache_cleared: type: boolean required: - message - query_cache_cleared - intent_cache_cleared meta: type: object additionalProperties: true includes: type: object additionalProperties: true required: - success - data - meta - includes SystemSearchCacheRebuildRequest: type: object properties: scope: type: string enum: [all, types, dirty] default: all types: type: array items: $ref: '#/components/schemas/SystemSearchEntityType' SystemSearchCacheRebuildResponse: type: object properties: success: type: boolean data: type: object properties: message: type: string request: type: object properties: scope: type: string enum: [all, types, dirty] types: type: array items: $ref: '#/components/schemas/SystemSearchEntityType' requested_at: type: integer required: - scope - types - requested_at query_cache_cleared: type: boolean intent_cache_cleared: type: boolean required: - message - request - query_cache_cleared - intent_cache_cleared meta: type: object additionalProperties: true includes: type: object additionalProperties: true required: - success - data - meta - includes ModuleConfigValue: oneOf: - type: string - type: integer - type: number - type: boolean - type: array items: {} - type: object additionalProperties: true nullable: true ModuleConfigEnvelopeBase: type: object properties: success: type: boolean meta: oneOf: - type: array items: {} - type: object additionalProperties: true includes: oneOf: - type: array items: {} - type: object additionalProperties: true required: - success - meta - includes EconomicConfigEntry: type: object properties: module: { type: string, enum: [economic] } variable: { type: string, enum: [adminFeeMonthly, adminFeeOrder, feeProductId, invoiceLayoutNumber, paymentTermsNumber, transactionDraftCustomerNumber, defaultDepartmentId] } type: { type: string, enum: [string, int] } value: oneOf: - type: string - type: integer nullable: true required: [module, variable, type, value] RecaptchaConfigEntry: type: object properties: module: { type: string, enum: [reCAPTCHA] } variable: { type: string, enum: [enabled, secret_key_v2, site_key_v2] } type: { type: string, enum: [bool, string] } value: oneOf: - type: boolean - type: string required: [module, variable, type, value] EmailConfigEntry: type: object properties: module: { type: string, enum: [Email] } variable: type: string enum: [enabled, mailersend_api_key, mailersend_enabled, smtp_encryption, smtp_from, smtp_from_name, smtp_host, smtp_password, smtp_port, smtp_reply_to, smtp_reply_to_name, smtp_username] type: { type: string, enum: [bool, string, int] } value: oneOf: - type: boolean - type: string - type: integer required: [module, variable, type, value] SlackConfigEntry: type: object properties: module: { type: string, enum: [Slack] } variable: { type: string, enum: [customer_registration_webhook_url, internal_department_goal_progress_webhook_url, internal_department_ids] } type: { type: string, enum: [string] } value: oneOf: - type: string example: https://hooks.slack.com/services/... - type: string example: '[1,2,3]' required: [module, variable, type, value] SlackConfigTestResult: type: object properties: configured: { type: boolean } sent: { type: boolean } message: { type: string } required: [configured, sent, message] SlackInternalDepartmentGoalProgressDepartment: type: object properties: id: { type: integer } name: { type: string } order_priority: { type: integer } required: [id, name, order_priority] SlackInternalDepartmentGoalProgressConfig: type: object properties: internal_department_goal_progress_webhook_url: type: string example: https://hooks.slack.com/services/... internal_department_ids: type: array items: { type: integer } example: [1, 2, 3] departments: type: array items: $ref: '#/components/schemas/SlackInternalDepartmentGoalProgressDepartment' required: [internal_department_goal_progress_webhook_url, internal_department_ids, departments] SlackInternalDepartmentGoalProgressConfigUpdate: type: object properties: internal_department_goal_progress_webhook_url: type: string example: https://hooks.slack.com/services/... internal_department_ids: type: array items: { type: integer } example: [1, 2, 3] required: [internal_department_goal_progress_webhook_url, internal_department_ids] BackupsConfigEntry: type: object properties: module: { type: string, enum: [Backups] } variable: { type: string, enum: [enabled] } type: { type: string, enum: [bool] } value: { type: boolean } required: [module, variable, type, value] BirdConfigEntry: type: object properties: module: { type: string, enum: [bird] } variable: { type: string, enum: [api_key, enabled, server_url, workplaceId, channelId] } type: { type: string, enum: [string, bool] } value: oneOf: - type: string - type: boolean required: [module, variable, type, value] MotorApiConfigEntry: type: object properties: module: { type: string, enum: [motorapi] } variable: { type: string, enum: [daily_limit, enabled, secret_key] } type: { type: string, enum: [int, bool, string] } value: oneOf: - type: integer - type: boolean - type: string required: [module, variable, type, value] StripeConfigEntry: type: object properties: module: { type: string, enum: [Stripe] } variable: { type: string, enum: [economic_customer_number, enabled, publishable_key, secret_key] } type: { type: string, enum: [int, bool, string] } value: oneOf: - type: integer - type: boolean - type: string required: [module, variable, type, value] FxRatesApiConfigEntry: type: object properties: module: { type: string, enum: [fxratesapi] } variable: { type: string, enum: [daily_limit, enabled, secret_key] } type: { type: string, enum: [int, bool, string] } value: oneOf: - type: integer - type: boolean - type: string required: [module, variable, type, value] WeatherApiConfigEntry: type: object properties: module: { type: string, enum: [weatherapi] } variable: { type: string, enum: [enabled, secret_key] } type: { type: string, enum: [bool, string] } value: oneOf: - type: boolean - type: string required: [module, variable, type, value] WorkfeedConfigEnabledEntry: type: object properties: module: { type: string, enum: [workfeed] } variable: { type: string, enum: [enabled] } type: { type: string, enum: [bool] } value: { type: boolean } required: [module, variable, type, value] WorkfeedConfigApiUrlEntry: type: object properties: module: { type: string, enum: [workfeed] } variable: { type: string, enum: [api_url] } type: { type: string, enum: [string] } value: { type: string, example: "https://europe-west1-production-eu-327a3.cloudfunctions.net/api" } required: [module, variable, type, value] WorkfeedConfigApiKeyEntry: type: object properties: module: { type: string, enum: [workfeed] } variable: { type: string, enum: [api_key] } type: { type: string, enum: [string] } value: { type: string, example: "wf_live_xxxxxxxxxxxxxxxxx" } required: [module, variable, type, value] WorkfeedConfigCompanyIdEntry: type: object properties: module: { type: string, enum: [workfeed] } variable: { type: string, enum: [CompanyID] } type: { type: string, enum: [string] } value: { type: string, example: "123456" } required: [module, variable, type, value] WorkfeedConfigEntry: oneOf: - $ref: '#/components/schemas/WorkfeedConfigEnabledEntry' - $ref: '#/components/schemas/WorkfeedConfigApiUrlEntry' - $ref: '#/components/schemas/WorkfeedConfigApiKeyEntry' - $ref: '#/components/schemas/WorkfeedConfigCompanyIdEntry' discriminator: propertyName: variable mapping: enabled: '#/components/schemas/WorkfeedConfigEnabledEntry' api_url: '#/components/schemas/WorkfeedConfigApiUrlEntry' api_key: '#/components/schemas/WorkfeedConfigApiKeyEntry' CompanyID: '#/components/schemas/WorkfeedConfigCompanyIdEntry' GatewayApiConfigEntry: type: object properties: module: { type: string, enum: [GatewayAPI] } variable: { type: string, enum: [api_key, api_secret, api_token, enabled, sender] } type: { type: string, enum: [string, bool] } value: oneOf: - type: string - type: boolean required: [module, variable, type, value] XlvaskConfigEntry: type: object properties: module: { type: string, enum: [xlvask] } variable: { type: string, enum: [enabled, password, synchronization_enabled, username] } type: { type: string, enum: [bool, string] } value: oneOf: - type: boolean - type: string required: [module, variable, type, value] EntraConfigEntry: type: object properties: module: { type: string, enum: [Entra] } variable: { type: string, enum: [enabled, entra_client_id, entra_client_secret, entra_tenant_id] } type: { type: string, enum: [bool, string] } value: oneOf: - type: boolean - type: string required: [module, variable, type, value] LimbleConfigEntry: type: object properties: module: { type: string, enum: [limble] } variable: { type: string, enum: [client_id, client_secret, enabled, webhooks_enabled] } type: { type: string, enum: [string, bool] } value: oneOf: - type: string - type: boolean required: [module, variable, type, value] OcrSpaceConfigEntry: type: object properties: module: { type: string, enum: [ocrSpace] } variable: { type: string, enum: [api_key, enabled] } type: { type: string, enum: [string, bool] } value: oneOf: - type: string - type: boolean required: [module, variable, type, value] OpenAiConfigEntry: type: object properties: module: { type: string, enum: [openAI] } variable: { type: string, enum: [api_key, enabled] } type: { type: string, enum: [string, bool] } value: oneOf: - type: string - type: boolean required: [module, variable, type, value] LicensePlateRecognizerConfigEntry: type: object properties: module: { type: string, enum: [licenseplaterecognizer] } variable: { type: string, enum: [api_key, enabled] } type: { type: string, enum: [string, bool] } value: oneOf: - type: string - type: boolean required: [module, variable, type, value] VirkdataConfigEntry: type: object properties: module: { type: string, enum: [virkdata] } variable: { type: string, enum: [enabled, monthly_limit, secret_key] } type: { type: string, enum: [bool, int, string] } value: oneOf: - type: boolean - type: integer - type: string required: [module, variable, type, value] ShellyConfigEntry: type: object properties: module: { type: string, enum: [shelly] } variable: { type: string, enum: [enabled, secret_key, server_url] } type: { type: string, enum: [bool, string] } value: oneOf: - type: boolean - type: string required: [module, variable, type, value] SelfServeConfigEntry: type: object properties: module: { type: string, enum: [selfserve] } variable: { type: string, enum: [enabled, minute_product, machine_wash_minutes_included] } type: { type: string, enum: [bool, int] } value: oneOf: - type: boolean - type: integer required: [module, variable, type, value] EconomicConfigListResponse: allOf: - $ref: '#/components/schemas/ModuleConfigEnvelopeBase' - type: object properties: data: { type: array, items: { $ref: '#/components/schemas/EconomicConfigEntry' } } required: [data] RecaptchaConfigListResponse: allOf: - $ref: '#/components/schemas/ModuleConfigEnvelopeBase' - type: object properties: data: { type: array, items: { $ref: '#/components/schemas/RecaptchaConfigEntry' } } required: [data] EmailConfigListResponse: allOf: - $ref: '#/components/schemas/ModuleConfigEnvelopeBase' - type: object properties: data: { type: array, items: { $ref: '#/components/schemas/EmailConfigEntry' } } required: [data] SlackConfigListResponse: allOf: - $ref: '#/components/schemas/ModuleConfigEnvelopeBase' - type: object properties: data: { type: array, items: { $ref: '#/components/schemas/SlackConfigEntry' } } required: [data] SlackConfigTestResponse: allOf: - $ref: '#/components/schemas/ModuleConfigEnvelopeBase' - type: object properties: data: { $ref: '#/components/schemas/SlackConfigTestResult' } required: [data] SlackInternalDepartmentGoalProgressConfigResponse: allOf: - $ref: '#/components/schemas/ModuleConfigEnvelopeBase' - type: object properties: data: { $ref: '#/components/schemas/SlackInternalDepartmentGoalProgressConfig' } required: [data] BackupsConfigListResponse: allOf: - $ref: '#/components/schemas/ModuleConfigEnvelopeBase' - type: object properties: data: { type: array, items: { $ref: '#/components/schemas/BackupsConfigEntry' } } required: [data] BirdConfigListResponse: allOf: - $ref: '#/components/schemas/ModuleConfigEnvelopeBase' - type: object properties: data: { type: array, items: { $ref: '#/components/schemas/BirdConfigEntry' } } required: [data] MotorApiConfigListResponse: allOf: - $ref: '#/components/schemas/ModuleConfigEnvelopeBase' - type: object properties: data: { type: array, items: { $ref: '#/components/schemas/MotorApiConfigEntry' } } required: [data] StripeConfigListResponse: allOf: - $ref: '#/components/schemas/ModuleConfigEnvelopeBase' - type: object properties: data: { type: array, items: { $ref: '#/components/schemas/StripeConfigEntry' } } required: [data] FxRatesApiConfigListResponse: allOf: - $ref: '#/components/schemas/ModuleConfigEnvelopeBase' - type: object properties: data: { type: array, items: { $ref: '#/components/schemas/FxRatesApiConfigEntry' } } required: [data] WeatherApiConfigListResponse: allOf: - $ref: '#/components/schemas/ModuleConfigEnvelopeBase' - type: object properties: data: { type: array, items: { $ref: '#/components/schemas/WeatherApiConfigEntry' } } required: [data] WorkfeedConfigListResponse: allOf: - $ref: '#/components/schemas/ModuleConfigEnvelopeBase' - type: object properties: data: { type: array, items: { $ref: '#/components/schemas/WorkfeedConfigEntry' } } required: [data] example: success: true data: - module: workfeed variable: enabled type: bool value: true - module: workfeed variable: api_url type: string value: https://europe-west1-production-eu-327a3.cloudfunctions.net/api - module: workfeed variable: api_key type: string value: wf_live_xxxxxxxxxxxxxxxxx - module: workfeed variable: CompanyID type: string value: "123456" meta: [] includes: [] GatewayApiConfigListResponse: allOf: - $ref: '#/components/schemas/ModuleConfigEnvelopeBase' - type: object properties: data: { type: array, items: { $ref: '#/components/schemas/GatewayApiConfigEntry' } } required: [data] XlvaskConfigListResponse: allOf: - $ref: '#/components/schemas/ModuleConfigEnvelopeBase' - type: object properties: data: { type: array, items: { $ref: '#/components/schemas/XlvaskConfigEntry' } } required: [data] EntraConfigListResponse: allOf: - $ref: '#/components/schemas/ModuleConfigEnvelopeBase' - type: object properties: data: { type: array, items: { $ref: '#/components/schemas/EntraConfigEntry' } } required: [data] LimbleConfigListResponse: allOf: - $ref: '#/components/schemas/ModuleConfigEnvelopeBase' - type: object properties: data: { type: array, items: { $ref: '#/components/schemas/LimbleConfigEntry' } } required: [data] OcrSpaceConfigListResponse: allOf: - $ref: '#/components/schemas/ModuleConfigEnvelopeBase' - type: object properties: data: { type: array, items: { $ref: '#/components/schemas/OcrSpaceConfigEntry' } } required: [data] OpenAiConfigListResponse: allOf: - $ref: '#/components/schemas/ModuleConfigEnvelopeBase' - type: object properties: data: { type: array, items: { $ref: '#/components/schemas/OpenAiConfigEntry' } } required: [data] LicensePlateRecognizerConfigListResponse: allOf: - $ref: '#/components/schemas/ModuleConfigEnvelopeBase' - type: object properties: data: { type: array, items: { $ref: '#/components/schemas/LicensePlateRecognizerConfigEntry' } } required: [data] VirkdataConfigListResponse: allOf: - $ref: '#/components/schemas/ModuleConfigEnvelopeBase' - type: object properties: data: { type: array, items: { $ref: '#/components/schemas/VirkdataConfigEntry' } } required: [data] ShellyConfigListResponse: allOf: - $ref: '#/components/schemas/ModuleConfigEnvelopeBase' - type: object properties: data: { type: array, items: { $ref: '#/components/schemas/ShellyConfigEntry' } } required: [data] SelfServeConfigListResponse: allOf: - $ref: '#/components/schemas/ModuleConfigEnvelopeBase' - type: object properties: data: { type: array, items: { $ref: '#/components/schemas/SelfServeConfigEntry' } } required: [data] DepartmentWeatherStatus: type: string description: Productivity health for the slot. `unknown` is returned when the slot has not started yet, has no employee-hours, or one or more selected departments are missing department weather targets. enum: [unknown, healthy, degraded, unhealthy] DepartmentWeatherCondition: type: string enum: [clear, mostly_clear, partly_cloudy, mostly_cloudy, overcast, rain, showers, thunderstorm, snow, fog] DepartmentWeatherTarget: type: object properties: department_id: type: integer minimum: 1 degraded_threshold: type: number nullable: true minimum: 0 description: Minimum washes per hour ratio required for `degraded`. `null` when target is not configured. healthy_threshold: type: number nullable: true minimum: 0 description: Minimum washes per hour ratio required for `healthy`. `null` when target is not configured. configured: type: boolean description: Whether both weather status thresholds are configured and valid for the department. required: [department_id, degraded_threshold, healthy_threshold, configured] DepartmentWeatherTargetUpsertRequest: type: object required: [department_id, degraded_threshold, healthy_threshold] properties: department_id: type: integer minimum: 1 degraded_threshold: type: number minimum: 0 healthy_threshold: type: number minimum: 0 description: Must be greater than or equal to `degraded_threshold`. DepartmentWeatherTimelineEntry: type: object properties: date: type: string format: date description: Calendar date for the hourly slot (`YYYY-MM-DD`). example: '2026-03-24' time: type: string description: Hour label for the slot in 24-hour format (`HH:00`). example: '01:00' current: type: boolean description: True when this slot matches the current server hour. example: false weather: $ref: '#/components/schemas/DepartmentWeatherCondition' washes: type: integer minimum: 0 example: 0 hours: type: number format: float minimum: 0 example: 2.5 description: Sum of Workfeed employee-hours in the department for this exact hour slot status: $ref: '#/components/schemas/DepartmentWeatherStatus' required: [date, time, current, weather, washes, hours, status] WeatherApiObjectResponse: allOf: - $ref: '#/components/schemas/ModuleConfigEnvelopeBase' - type: object properties: data: type: object additionalProperties: true required: [data] WorkfeedDepartment: type: object properties: id: { type: string, example: "PKHaOSgFA4uguOqmLfWv" } name: { type: string, example: "API Testing Account 😎" } isDeleted: { type: boolean, example: false } createTime: { type: string, format: date-time, example: "2023-10-25T09:43:06.650Z" } updateTime: { type: string, format: date-time, example: "2023-10-25T09:43:06.650Z" } additionalProperties: true WorkfeedEmployee: type: object properties: id: { type: string, example: "J9QAIiTG0nRC1OsC5YvHfLWdDHn1" } firstname: { type: string, example: "API 2" } lastname: { type: string, example: "Test 2" } email: { type: string, nullable: true, example: "test2@example.com" } phone: { type: string, nullable: true, example: "12345678" } roleIDs: type: array items: { type: string } example: ["hLbEKIPTlMh3ehotXl0w"] departmentIDs: type: array items: { type: string } example: ["PKHaOSgFA4uguOqmLfWv"] primaryDepartmentID: { type: string, nullable: true } street: { type: string, nullable: true, example: "" } city: { type: string, nullable: true, example: "" } zip: { type: string, nullable: true, example: "" } accessLevel: { type: string, nullable: true, example: "employee" } wage: { type: number, nullable: true } minHours: { type: number, nullable: true } maxHours: { type: number, nullable: true } isDeleted: { type: boolean, example: false } ssn: { type: string, nullable: true, example: "" } imageURL: { type: string, nullable: true, format: uri } createTime: { type: string, format: date-time, example: "2023-10-28T18:22:45.695Z" } updateTime: { type: string, format: date-time, example: "2023-10-28T18:34:37.191Z" } additionalProperties: true WorkfeedShiftComment: type: object properties: message: { type: string, nullable: true, example: "Comments! 😍" } creatorID: { type: string, nullable: true, example: "API" } createdOn: { type: string, format: date-time, nullable: true, example: "2023-10-27T09:43:36.542Z" } additionalProperties: true WorkfeedShiftCustomBreak: type: object properties: creatorID: { type: string, nullable: true, example: "automatic" } duration: { type: number, nullable: true, example: 1 } createdOn: { type: string, format: date-time, nullable: true, example: "2023-10-27T09:10:30.336Z" } additionalProperties: true WorkfeedShiftApproval: type: object properties: approver: { type: string, nullable: true, example: "automatic" } date: { type: string, format: date-time, nullable: true, example: "2023-12-27T09:10:30.336Z" } originalStart: { type: string, format: date-time, nullable: true, example: "2023-12-07T09:10:30.336Z" } originalEnd: { type: string, format: date-time, nullable: true, example: "2023-12-08T09:10:30.336Z" } additionalProperties: true WorkfeedShift: type: object properties: id: { type: string, example: "Trcu7MKFomu5y5zv1B8G" } start: { type: string, format: date-time, example: "2023-10-23T08:00:00.000Z" } end: { type: string, format: date-time, example: "2023-10-23T16:00:00.000Z" } employeeID: { type: string, nullable: true } roleID: { type: string, nullable: true, example: "hLbEKIPTlMh3ehotXl0w" } departmentID: { type: string, nullable: true, example: "PKHaOSgFA4uguOqmLfWv" } released: { type: boolean, example: false } isForSale: { type: boolean, nullable: true, example: false } comment: allOf: - $ref: '#/components/schemas/WorkfeedShiftComment' nullable: true customBreak: allOf: - $ref: '#/components/schemas/WorkfeedShiftCustomBreak' nullable: true overlappingLeaveID: { type: string, nullable: true } approval: allOf: - $ref: '#/components/schemas/WorkfeedShiftApproval' nullable: true grossPay: { type: number, nullable: true } tagIDs: type: array items: { type: string } createTime: { type: string, format: date-time, example: "2023-10-27T09:10:30.336Z" } updateTime: { type: string, format: date-time, example: "2023-10-27T09:10:30.422Z" } additionalProperties: true WorkfeedEmployeeListResponse: allOf: - $ref: '#/components/schemas/ModuleConfigEnvelopeBase' - type: object properties: data: type: array items: $ref: '#/components/schemas/WorkfeedEmployee' required: [data] WorkfeedEmployeeSingleResponse: allOf: - $ref: '#/components/schemas/ModuleConfigEnvelopeBase' - type: object properties: data: $ref: '#/components/schemas/WorkfeedEmployee' required: [data] WorkfeedShiftListResponse: allOf: - $ref: '#/components/schemas/ModuleConfigEnvelopeBase' - type: object properties: data: type: array items: $ref: '#/components/schemas/WorkfeedShift' required: [data] WorkfeedShiftSingleResponse: allOf: - $ref: '#/components/schemas/ModuleConfigEnvelopeBase' - type: object properties: data: $ref: '#/components/schemas/WorkfeedShift' required: [data] WorkfeedDepartmentListResponse: allOf: - $ref: '#/components/schemas/ModuleConfigEnvelopeBase' - type: object properties: data: type: array items: $ref: '#/components/schemas/WorkfeedDepartment' required: [data] DepartmentWeatherTimelineResponse: allOf: - $ref: '#/components/schemas/ModuleConfigEnvelopeBase' - type: object properties: data: type: array description: Hourly contiguous slots from start of range (`00:00`) to end of range (`23:00`, inclusive). Defaults to yesterday+today (48 entries) when date_from/date_to are not provided. items: $ref: '#/components/schemas/DepartmentWeatherTimelineEntry' required: [data] DepartmentWeatherTargetsResponse: allOf: - $ref: '#/components/schemas/ModuleConfigEnvelopeBase' - type: object properties: data: type: array items: $ref: '#/components/schemas/DepartmentWeatherTarget' required: [data] ModuleConfigEntry: type: object properties: module: type: string description: Module name variable: type: string description: Configuration variable key type: type: string description: Stored value type in module_config value: $ref: '#/components/schemas/ModuleConfigValue' required: - module - variable - type - value ModuleConfigListResponse: type: object properties: success: type: boolean data: type: array items: $ref: '#/components/schemas/ModuleConfigEntry' meta: oneOf: - type: array items: {} - type: object additionalProperties: true includes: oneOf: - type: array items: {} - type: object additionalProperties: true required: - success - data - meta - includes ModuleConfigUpdateResponse: type: object properties: success: type: boolean data: type: boolean meta: oneOf: - type: array items: {} - type: object additionalProperties: true includes: oneOf: - type: array items: {} - type: object additionalProperties: true required: - success - data - meta - includes ModuleConfigTestResponse: type: object properties: success: type: boolean data: type: string meta: oneOf: - type: array items: {} - type: object additionalProperties: true includes: oneOf: - type: array items: {} - type: object additionalProperties: true required: - success - data - meta - includes User: type: object properties: id: type: integer description: User ID customer_number: type: integer description: e-conomic customer number display_name: type: string description: User's display name group_id: type: integer description: User group/role ID limited_backoffice_managed: type: boolean description: Whether the user is managed through limited backoffice and has locked raw role/customer edits phone_country_code: type: integer description: Phone country code phone: type: integer description: Phone number email: type: string format: email description: Email address sms_notifications_enabled: type: boolean description: SMS notifications enabled email_notifications_enabled: type: boolean description: Email notifications enabled created_at: type: string format: date-time updated_at: type: string format: date-time SubuserGrant: type: object properties: id: type: integer billing_customer_number: type: integer description: e-conomic customer number subuser: type: integer description: Subuser ID enabled: type: boolean note: type: string nullable: true permissions: type: array description: List of permission node keys items: type: string example: BOOKINGS_LIST created_at: type: string format: date-time nullable: true updated_at: type: string format: date-time nullable: true deleted_at: type: string format: date-time nullable: true SubuserManagementRow: type: object description: Chauffeur management row. Superuser list endpoints return one row per chauffeur account; grant-specific fields mirror a primary grant for backwards compatibility, and all visible customer grants are listed in `grants`. properties: id: { type: integer } username: { type: string, nullable: true } name: { type: string, nullable: true } email: { type: string, format: email, nullable: true } phone_country_code: { type: integer, nullable: true } phone: { type: integer, nullable: true } created_at: { type: string, format: date-time, nullable: true } updated_at: { type: string, format: date-time, nullable: true } suspended_at: { type: string, format: date-time, nullable: true } two_factor_enabled: { type: boolean } setup_required: { type: boolean } invite_accepted: { type: boolean } can_resend_invite: { type: boolean } profile_editable_by_manager: { type: boolean } customer_number: { type: integer } customer_name: { type: string, nullable: true } grant_id: { type: integer, nullable: true } grant_enabled: { type: boolean } grant_note: { type: string, nullable: true } grant_permissions: type: array items: { type: string } permissions: type: array items: { type: string } permission_template_key: type: string enum: [deactivated, driver, booking_coordinator, fleet_admin, custom] permission_groups: type: array items: $ref: '#/components/schemas/SubuserPermissionGroup' grant_created_at: { type: string, format: date-time, nullable: true } grant_updated_at: { type: string, format: date-time, nullable: true } grants: type: array description: Visible customer access grants for this chauffeur, grouped under the single chauffeur row. items: $ref: '#/components/schemas/SubuserManagementGrant' grant_count: { type: integer } customer_numbers: type: array items: { type: integer } access_state: type: string enum: [active, pending_setup, disabled, inactive] SubuserManagementGrant: type: object properties: grant_id: { type: integer } customer_number: { type: integer } customer_name: { type: string, nullable: true } grant_enabled: { type: boolean } grant_note: { type: string, nullable: true } grant_permissions: type: array items: { type: string } permissions: type: array items: { type: string } permission_template_key: type: string enum: [deactivated, driver, booking_coordinator, fleet_admin, custom] permission_groups: type: array items: $ref: '#/components/schemas/SubuserPermissionGroup' grant_created_at: { type: string, format: date-time, nullable: true } grant_updated_at: { type: string, format: date-time, nullable: true } access_state: type: string enum: [active, pending_setup, disabled, inactive] SubuserManagementSummary: type: object properties: total: { type: integer } active: { type: integer } pending_setup: { type: integer } disabled: { type: integer } SubuserGrantCreateRequest: type: object required: - customer_number - subuser_id properties: customer_number: type: integer description: e-conomic customer number subuser_id: type: integer description: Subuser ID to grant permissions for enabled: type: boolean default: true note: type: string nullable: true maxLength: 65535 permissions: type: array description: Optional list of permission keys; defaults will be applied if omitted items: type: string permission_template_key: type: string enum: [deactivated, driver, booking_coordinator, fleet_admin] SubuserGrantUpdateRequest: type: object properties: enabled: type: boolean note: type: string nullable: true maxLength: 65535 permissions: type: array items: type: string permission_template_key: type: string enum: [deactivated, driver, booking_coordinator, fleet_admin] SubuserPermissionGroup: type: object properties: key: type: string capabilities: type: array items: type: string SubuserPermissionTemplate: type: object properties: key: type: string enum: [deactivated, driver, booking_coordinator, fleet_admin] label: type: string description: type: string enabled: type: boolean permissions: type: array items: type: string permission_groups: type: array items: $ref: '#/components/schemas/SubuserPermissionGroup' SubuserGrantSummary: type: object description: Summary of a subuser grant grouped by billing customer number properties: billing_customer_number: type: integer description: e-conomic customer number this grant applies to permissions: type: array description: List of permission node keys enabled for this customer items: type: string SubuserSelf: type: object description: Authenticated subuser profile with enabled grants properties: id: type: integer username: type: string name: type: string nullable: true email: type: string format: email nullable: true phone_country_code: type: integer nullable: true phone: type: integer nullable: true grants: type: array description: Enabled, non-deleted grants for the subuser grouped by billing customer number items: $ref: '#/components/schemas/SubuserGrantSummary' created_at: type: string format: date-time nullable: true updated_at: type: string format: date-time nullable: true suspended_at: type: string format: date-time nullable: true two_factor_enabled: type: boolean description: Indicates if 2FA is enabled for this account PermissionNode: type: object properties: key: type: string description: Permission node key name: type: string description: type: string type: type: string description: Permission type (e.g., TOGGLE) default: type: boolean PermissionNodeGroup: type: object properties: group: type: string description: type: string nodes: type: array items: $ref: '#/components/schemas/PermissionNode' UserCreate: type: object required: - customer_number - password properties: customer_number: type: integer password: type: string format: password display_name: type: string group_id: type: integer email: type: string format: email phone: type: integer phone_country_code: type: integer UserUpdate: type: object properties: id: type: integer customer_number: type: integer display_name: type: string group_id: type: integer email: type: string format: email phone: type: integer phone_country_code: type: integer Order: type: object properties: id: type: integer description: Order ID customer_id: type: integer description: Customer number customer_name: type: string description: Customer name user_id: type: integer description: User ID cashier_id: type: integer description: Cashier user ID cashier_name: type: string description: Cashier name department_id: type: integer description: Department ID status: type: string description: Order status total_net_amount: type: number format: float description: Total order amount po: type: string description: Purchase order number nullable: true lane: type: string description: Lane information nullable: true created_at: type: string format: date-time updated_at: type: string format: date-time EconomicTransferQueueStatus: type: string enum: - QUEUED - PROCESSING - COMPLETED - FAILED EconomicTransferQueueJob: type: object properties: id: type: integer transfer_type: type: string enum: - ORDER_DRAFT_EXPORT - ORDER_INVOICE_EXPORT - COLLECTED_INVOICE_EXPORT status: $ref: '#/components/schemas/EconomicTransferQueueStatus' progress_percent: type: integer minimum: 0 maximum: 100 progress_message: type: string nullable: true attempts: type: integer minimum: 0 max_attempts: type: integer minimum: 1 error_message: type: string nullable: true payload: type: object nullable: true additionalProperties: true result: type: object nullable: true additionalProperties: true details_summary: type: object nullable: true additionalProperties: true created_by: type: integer nullable: true created_at: type: string format: date-time nullable: true updated_at: type: string format: date-time nullable: true started_at: type: string format: date-time nullable: true completed_at: type: string format: date-time nullable: true next_retry_at: type: string format: date-time nullable: true required: - id - transfer_type - status - progress_percent - attempts - max_attempts EconomicTransferQueueEnqueueResponse: type: object properties: success: type: boolean data: type: object properties: message: type: string job_id: type: integer minimum: 1 job: $ref: '#/components/schemas/EconomicTransferQueueJob' required: - message - job_id - job meta: oneOf: - type: array items: {} - type: object additionalProperties: true includes: oneOf: - type: array items: {} - type: object additionalProperties: true required: - success - data - meta - includes EconomicTransferSynchronousFallbackResponse: type: object properties: success: type: boolean data: type: object properties: message: type: string mode: type: string enum: [synchronous_fallback] result: type: object additionalProperties: true required: - message - mode - result meta: oneOf: - type: array items: {} - type: object additionalProperties: true includes: oneOf: - type: array items: {} - type: object additionalProperties: true required: - success - data - meta - includes EconomicTransferQueueStatusResponse: type: object properties: success: type: boolean data: $ref: '#/components/schemas/EconomicTransferQueueJob' meta: oneOf: - type: array items: {} - type: object additionalProperties: true includes: oneOf: - type: array items: {} - type: object additionalProperties: true required: - success - data - meta - includes EconomicTransferQueueRetryResponse: type: object properties: success: type: boolean data: type: object properties: message: type: string job: $ref: '#/components/schemas/EconomicTransferQueueJob' required: - message - job meta: oneOf: - type: array items: {} - type: object additionalProperties: true includes: oneOf: - type: array items: {} - type: object additionalProperties: true required: - success - data - meta - includes EconomicTransferQueueRunResponse: type: object properties: success: type: boolean data: type: object properties: message: type: string processed: type: integer minimum: 0 completed: type: integer minimum: 0 failed: type: integer minimum: 0 jobs: type: array items: type: integer minimum: 1 limit: type: integer minimum: 1 maximum: 10 transfer_type: type: string enum: - COLLECTED_INVOICE_EXPORT required: - message - processed - completed - failed - jobs - limit - transfer_type meta: oneOf: - type: array items: {} - type: object additionalProperties: true includes: oneOf: - type: array items: {} - type: object additionalProperties: true required: - success - data - meta - includes EconomicTransferQueueListResponse: type: object properties: success: type: boolean data: type: object properties: items: type: array items: $ref: '#/components/schemas/EconomicTransferQueueJob' count: type: integer minimum: 0 total: type: integer minimum: 0 limit: type: integer minimum: 1 offset: type: integer minimum: 0 has_more: type: boolean required: - items - count - total - limit - offset - has_more meta: oneOf: - type: array items: {} - type: object additionalProperties: true includes: oneOf: - type: array items: {} - type: object additionalProperties: true required: - success - data - meta - includes EconomicTransferQueueMonitorResponse: type: object properties: success: type: boolean data: type: object properties: jobs: type: array items: $ref: '#/components/schemas/EconomicTransferQueueJob' counts: type: object properties: queued: type: integer minimum: 0 in_progress: type: integer minimum: 0 failed: type: integer minimum: 0 completed: type: integer minimum: 0 total: type: integer minimum: 0 required: - queued - in_progress - failed - completed - total progress_percent: type: integer minimum: 0 maximum: 100 limit: type: integer minimum: 1 maximum: 100 required: - jobs - counts - progress_percent - limit meta: oneOf: - type: array items: {} - type: object additionalProperties: true includes: oneOf: - type: array items: {} - type: object additionalProperties: true required: - success - data - meta - includes EconomicTransferQueueDismissResponse: type: object properties: success: type: boolean data: type: object properties: message: type: string job: $ref: '#/components/schemas/EconomicTransferQueueJob' required: - message - job meta: oneOf: - type: array items: {} - type: object additionalProperties: true includes: oneOf: - type: array items: {} - type: object additionalProperties: true required: - success - data - meta - includes EconomicTransferQueueDismissTerminalResponse: type: object properties: success: type: boolean data: type: object properties: message: type: string dismissed_count: type: integer minimum: 0 required: - message - dismissed_count meta: oneOf: - type: array items: {} - type: object additionalProperties: true includes: oneOf: - type: array items: {} - type: object additionalProperties: true required: - success - data - meta - includes OrderAttachmentDownloadLinkResponse: type: object required: [success, data, meta, includes] properties: success: type: boolean enum: [true] data: type: object required: [download_link] properties: download_link: type: string format: uri pattern: '^https://' meta: type: object additionalProperties: true includes: type: object additionalProperties: true InvoicingPeriodResponseEnvelope: type: object required: [success, data, meta, includes] properties: success: type: boolean enum: [true] data: $ref: '#/components/schemas/InvoicingPeriodData' meta: type: object additionalProperties: true includes: type: object additionalProperties: true InvoicingPeriodData: type: object required: [dateFrom, dateTo, types] properties: dateFrom: type: string format: date dateTo: type: string format: date types: type: object additionalProperties: type: array items: $ref: '#/components/schemas/InvoicingPeriodCustomer' InvoicingPeriodCustomer: type: object required: [customer_number, customer_name, transactions, invoice_collections] additionalProperties: true properties: customer_number: type: integer customer_name: type: string transactions: type: array items: $ref: '#/components/schemas/InvoicingPeriodTransaction' invoice_collections: type: array items: $ref: '#/components/schemas/InvoicingPeriodInvoiceCollection' InvoicingPeriodTransaction: type: object required: [id, booked, invoice_state] additionalProperties: true properties: id: type: integer invoice_collection_id: type: integer nullable: true booked: type: boolean invoice_state: type: string enum: [open, closed, economic_draft, economic_booked] completed_at: type: string format: date-time nullable: true amount: type: number format: float InvoicingPeriodInvoiceCollection: type: object required: - id - invoice_collection_id - customer_number - state - order_ids - order_count - total_net_amount additionalProperties: true properties: id: type: integer invoice_collection_id: type: integer customer_number: type: integer name: type: string external_id: type: string nullable: true booked_invoice_id: type: integer nullable: true state: type: string enum: [open, closed, economic_draft, economic_booked] order_ids: type: array items: type: integer order_count: type: integer minimum: 0 total_net_amount: type: number format: float CollectedInvoiceEconomicCompareResponse: type: object description: Result of comparing a collected invoice with its E-conomic counterpart properties: collected_invoice_id: type: integer description: The internal collected invoice ID example: 123 draft_id: type: integer nullable: true description: E-conomic draft invoice ID, if present example: 456 booked_id: type: integer nullable: true description: E-conomic booked invoice ID, if present example: 28368 warnings: type: array description: List of warnings detected during comparison items: type: string example: - "Total amount mismatch for draft invoice ID 456: E-Conomic total is 867.5, internal total is 694" draft_total: type: number format: float nullable: true description: Total amount from the E-conomic draft (gross) example: 867.5 booked_total: type: number format: float nullable: true description: Total amount from the E-conomic booked invoice (gross minus VAT if applicable) example: 694 difference: type: number format: float nullable: true description: Selected e-conomic total (draft when available, otherwise booked) minus internal_total example: 0 internal_total: type: number format: float description: Internal total amount for the collected invoice example: 694 required: - collected_invoice_id - internal_total CollectedInvoiceEconomicPdfResponse: type: object description: Presigned PDF URL for a draft or booked e-conomic invoice attached to a collected invoice properties: collected_invoice_id: type: integer example: 123 type: type: string enum: - draft - booked example: booked economic_invoice_id: type: integer example: 28368 url: type: string format: uri required: - collected_invoice_id - type - economic_invoice_id - url CollectedInvoiceEconomicV2DetailsResponse: type: object properties: collected_invoice_id: type: integer external_id: type: string order_ids: type: array items: type: integer economic: type: object required: [draft_id, booked_id, state, available_pdf_type] properties: draft_id: type: integer nullable: true booked_id: type: integer nullable: true state: type: string enum: [draft, booked, none] description: Current authoritative e-conomic target state available_pdf_type: type: string enum: [draft, booked] nullable: true description: PDF target that callers should request customer: $ref: '#/components/schemas/CollectedInvoiceEconomicV2CustomerSummary' internal: type: object required: [normalized] properties: normalized: $ref: '#/components/schemas/EconomicV2NormalizedInvoice' draft: type: object properties: exists: type: boolean raw: type: object nullable: true additionalProperties: true normalized: allOf: - $ref: '#/components/schemas/EconomicV2NormalizedInvoice' nullable: true booked: type: object properties: exists: type: boolean raw: type: object nullable: true additionalProperties: true normalized: allOf: - $ref: '#/components/schemas/EconomicV2NormalizedInvoice' nullable: true warnings: type: array items: type: string required: - collected_invoice_id - order_ids - economic - customer - internal - draft - booked - warnings CollectedInvoiceEconomicV2CustomerSummary: type: object properties: internal_customer_number: type: integer nullable: true draft_customer_number: type: integer nullable: true booked_customer_number: type: integer nullable: true exists: type: boolean name: type: string nullable: true barred: type: boolean nullable: true required: - exists CollectedInvoiceEconomicV2CompareResponse: type: object properties: collected_invoice_id: type: integer details: $ref: '#/components/schemas/CollectedInvoiceEconomicV2DetailsResponse' comparison: $ref: '#/components/schemas/EconomicV2Comparison' warnings: type: array items: type: string required: - collected_invoice_id - details - comparison - warnings CollectedInvoiceEconomicV2CompareBulkResponse: type: object properties: requested: type: integer compared: type: integer failed: type: integer results: type: array items: $ref: '#/components/schemas/CollectedInvoiceEconomicV2CompareResponse' errors: type: array items: type: object properties: collected_invoice_id: type: integer error: type: string required: - requested - compared - failed - results - errors CollectedInvoiceEconomicV2RevenueStatisticsResponse: type: object properties: filters: type: object properties: dateFrom: type: string format: date dateTo: type: string format: date customer_numbers: type: array items: type: integer department_numbers: type: array items: type: integer currency: type: string nullable: true barred: type: string enum: [all, barred, active] max_pages: type: integer summary: $ref: '#/components/schemas/EconomicV2RevenueSummary' customers: type: array items: $ref: '#/components/schemas/EconomicV2RevenueCustomerStat' departments: type: array items: $ref: '#/components/schemas/EconomicV2RevenueDepartmentStat' currencies: type: array items: $ref: '#/components/schemas/EconomicV2RevenueCurrencyStat' warnings: type: array items: type: string required: - filters - summary - customers - departments - currencies - warnings EconomicV2RevenueSummary: type: object properties: invoice_count: type: integer line_count: type: integer unique_customers: type: integer net_amount: type: number vat_amount: type: number gross_amount: type: number average_invoice_net_amount: type: number required: - invoice_count - line_count - unique_customers - net_amount - vat_amount - gross_amount - average_invoice_net_amount EconomicV2RevenueCustomerStat: type: object properties: customer_number: type: integer customer_name: type: string nullable: true barred: type: boolean nullable: true invoice_count: type: integer net_amount: type: number vat_amount: type: number gross_amount: type: number required: - customer_number - invoice_count - net_amount - vat_amount - gross_amount EconomicV2RevenueDepartmentStat: type: object properties: department_key: type: string department_number: type: integer nullable: true invoice_count: type: integer line_count: type: integer net_amount: type: number vat_amount: type: number gross_amount: type: number required: - department_key - invoice_count - line_count - net_amount - vat_amount - gross_amount EconomicV2RevenueCurrencyStat: type: object properties: currency: type: string invoice_count: type: integer net_amount: type: number vat_amount: type: number gross_amount: type: number required: - currency - invoice_count - net_amount - vat_amount - gross_amount EconomicV2NormalizedInvoice: type: object properties: source: type: string enum: [internal, draft, booked] totals: type: object properties: net_total: type: number line_net_total: type: number line_count: type: integer billable_line_count: type: integer difference_from_line_sum: type: number nullable: true departments: $ref: '#/components/schemas/EconomicV2DepartmentDistribution' lines: type: array items: $ref: '#/components/schemas/EconomicV2NormalizedLineItem' warnings: type: array items: type: string required: - source - totals - departments - lines - warnings EconomicV2NormalizedLineItem: type: object properties: index: type: integer source: type: string source_order_id: type: integer nullable: true source_line_id: type: integer nullable: true line_type: type: string enum: [product, discount, text] billable: type: boolean product_number: type: string nullable: true product_id: type: integer nullable: true description: type: string reference: type: string quantity: type: number unit_net_price: type: number line_net_amount: type: number department_distribution: $ref: '#/components/schemas/EconomicV2DepartmentDistribution' match_key: type: string required: - source - line_type - billable - description - reference - quantity - unit_net_price - line_net_amount - department_distribution - match_key EconomicV2DepartmentDistribution: type: object additionalProperties: type: number example: "75": 100 EconomicV2Comparison: type: object properties: totals: type: object properties: internal_net_total: type: number targets: type: object properties: draft: $ref: '#/components/schemas/EconomicV2TargetComparison' booked: $ref: '#/components/schemas/EconomicV2TargetComparison' warnings: type: array items: type: string required: - totals - targets - warnings EconomicV2TargetComparison: type: object properties: target: type: string enum: [draft, booked] status: type: string enum: [exact_match, partial_mismatch, total_mismatch, missing_target] overall_match: type: boolean totals: $ref: '#/components/schemas/EconomicV2TotalsComparison' lines: type: object properties: summary: type: object properties: internal_billable_count: type: integer target_billable_count: type: integer mismatch_count: type: integer diff: type: array items: $ref: '#/components/schemas/EconomicV2LineDiffEntry' departments: type: object properties: matches: type: boolean diff: type: array items: $ref: '#/components/schemas/EconomicV2DepartmentDiffEntry' mismatch_reasons: type: array items: type: string warnings: type: array items: type: string required: - target - status - overall_match - totals - lines - departments - mismatch_reasons - warnings EconomicV2TotalsComparison: type: object properties: internal_net_total: type: number nullable: true target_net_total: type: number nullable: true difference: type: number nullable: true abs_difference: type: number nullable: true matches: type: boolean required: - matches EconomicV2LineDiffEntry: type: object properties: match_key: type: string reasons: type: array items: type: string internal_line: allOf: - $ref: '#/components/schemas/EconomicV2NormalizedLineItem' nullable: true target_line: allOf: - $ref: '#/components/schemas/EconomicV2NormalizedLineItem' nullable: true required: - match_key - reasons EconomicV2DepartmentDiffEntry: type: object properties: department_key: type: string internal_amount: type: number target_amount: type: number difference: type: number matches: type: boolean required: - department_key - internal_amount - target_amount - difference - matches InvoicingDistributionV2Transaction: type: object properties: id: type: integer date: type: string format: date-time amount: type: number booked: type: boolean department_id: type: integer excluded: type: boolean required: [id, date, amount, booked, department_id, excluded] InvoicingDistributionV2Customer: type: object properties: id: type: integer nullable: true customer_number: type: integer customer_name: type: string transactions: type: array items: $ref: '#/components/schemas/InvoicingDistributionV2Transaction' requires_action: type: boolean meta: type: object additionalProperties: true required: [customer_number, customer_name, transactions, requires_action, meta] InvoicingDistributionV2CategoryResponse: type: object properties: customers: type: array items: $ref: '#/components/schemas/InvoicingDistributionV2Customer' collective_results: type: object additionalProperties: true warnings: type: array items: type: string required: [customers, collective_results, warnings] InvoicingDistributionV2FixedPricingResponse: $ref: '#/components/schemas/InvoicingDistributionV2CategoryResponse' InvoicingDistributionV2WashSubscriptionsResponse: $ref: '#/components/schemas/InvoicingDistributionV2CategoryResponse' InvoicingDistributionV2CustomerPricesResponse: $ref: '#/components/schemas/InvoicingDistributionV2CategoryResponse' InvoicingDistributionV2BookedDepartment75Group: type: object properties: month: type: string example: '2026-01' source_category: type: string enum: [fixed_pricing, wash_subscriptions, unclassified] invoice_ids: type: array items: type: integer booked_net_amount: type: number department_distribution: $ref: '#/components/schemas/EconomicV2DepartmentDistribution' undistributed_net_amount: type: number required: - month - source_category - invoice_ids - booked_net_amount - department_distribution - undistributed_net_amount InvoicingDistributionV2BookedDepartment75Meta: type: object properties: booked_net_amount: type: number distributed_net_amount: type: number undistributed_net_amount: type: number department_distribution: $ref: '#/components/schemas/EconomicV2DepartmentDistribution' booked_groups: type: array items: $ref: '#/components/schemas/InvoicingDistributionV2BookedDepartment75Group' required: - booked_net_amount - distributed_net_amount - undistributed_net_amount - department_distribution - booked_groups InvoicingDistributionV2BookedDepartment75Customer: allOf: - $ref: '#/components/schemas/InvoicingDistributionV2Customer' - type: object properties: meta: type: object properties: booked_department_75: $ref: '#/components/schemas/InvoicingDistributionV2BookedDepartment75Meta' required: - booked_department_75 InvoicingDistributionV2BookedDepartment75CollectiveResults: type: object properties: booked_net_amount: type: number distributed_net_amount: type: number undistributed_net_amount: type: number department_distribution: $ref: '#/components/schemas/EconomicV2DepartmentDistribution' department_distribution_parsed: type: object additionalProperties: type: number required: - booked_net_amount - distributed_net_amount - undistributed_net_amount - department_distribution - department_distribution_parsed InvoicingDistributionV2BookedDepartment75Response: type: object properties: customers: type: array items: $ref: '#/components/schemas/InvoicingDistributionV2BookedDepartment75Customer' collective_results: $ref: '#/components/schemas/InvoicingDistributionV2BookedDepartment75CollectiveResults' warnings: type: array items: type: string required: [customers, collective_results, warnings] InvoicingDistributionV2AllResponse: type: object properties: fixed_pricing: $ref: '#/components/schemas/InvoicingDistributionV2FixedPricingResponse' wash_subscriptions: $ref: '#/components/schemas/InvoicingDistributionV2WashSubscriptionsResponse' customer_prices: $ref: '#/components/schemas/InvoicingDistributionV2CustomerPricesResponse' booked_department_75: $ref: '#/components/schemas/InvoicingDistributionV2BookedDepartment75Response' required: [fixed_pricing, wash_subscriptions, customer_prices, booked_department_75] PricingHistoryVersionEntry: type: object properties: id: type: integer type: type: string enum: [fixed_pricing, vehicle_subscription, discount_override] customer_number: type: integer effective_from: type: string format: date-time effective_to: type: string format: date-time nullable: true source: type: string confidence: type: number minimum: 0 maximum: 1 inferred: type: boolean metadata_json: oneOf: - type: string - type: object additionalProperties: true - type: array items: {} nullable: true required: - id - type - customer_number - effective_from - source - confidence - inferred CustomerPricingHistoryResponse: type: object properties: customer_number: type: integer fixed_pricing: type: array items: type: object additionalProperties: true vehicle_subscriptions: type: array items: type: object additionalProperties: true discount_overrides: type: array items: type: object additionalProperties: true timeline: type: array items: $ref: '#/components/schemas/PricingHistoryVersionEntry' required: - customer_number - fixed_pricing - vehicle_subscriptions - discount_overrides - timeline InvoicingWashSubscriptionsDistributionResponse: type: object properties: success: type: boolean example: true data: type: array items: $ref: '#/components/schemas/InvoicingWashSubscriptionsDistributionCustomer' meta: $ref: '#/components/schemas/InvoicingFixedPricingDistributionMeta' includes: type: object additionalProperties: true required: - success - data - meta - includes InvoicingWashSubscriptionsDistributionCustomer: type: object properties: id: type: integer nullable: true customer_number: type: integer customer_name: type: string transactions: type: array items: $ref: '#/components/schemas/InvoicingFixedPricingDistributionTransaction' requires_action: type: boolean meta: type: object properties: subscription: type: object additionalProperties: true required: - subscription required: - customer_number - customer_name - transactions - requires_action - meta InvoicingFixedPricingDistributionResponse: type: object properties: success: type: boolean example: true data: type: array items: $ref: '#/components/schemas/InvoicingFixedPricingDistributionCustomer' meta: $ref: '#/components/schemas/InvoicingFixedPricingDistributionMeta' includes: $ref: '#/components/schemas/InvoicingFixedPricingDistributionIncludes' required: - success - data - meta - includes InvoicingFixedPricingDistributionCustomer: type: object properties: id: type: integer customer_number: type: integer customer_name: type: string transactions: type: array items: $ref: '#/components/schemas/InvoicingFixedPricingDistributionTransaction' requires_action: type: boolean meta: type: object properties: fixed_pricing: $ref: '#/components/schemas/InvoicingFixedPricingDistributionFixedPricing' required: - fixed_pricing required: - id - customer_number - customer_name - transactions - requires_action - meta InvoicingFixedPricingDistributionTransaction: type: object properties: id: type: integer date: type: string description: Datetime in `YYYY-MM-DD HH:mm:ss` format. example: '2026-02-02 10:43:41' amount: type: number booked: type: boolean excluded: type: boolean required: - id - date - amount - booked - excluded InvoicingFixedPricingDistributionFixedPricing: type: object properties: customer_number: type: integer price: type: number description: type: string original_price: type: number department_totals: $ref: '#/components/schemas/InvoicingFixedPricingDistributionNumberMapOrEmptyArray' department_totals_relative: $ref: '#/components/schemas/InvoicingFixedPricingDistributionNumberMapOrEmptyArray' required: - customer_number - price - description - original_price - department_totals - department_totals_relative InvoicingFixedPricingDistributionMeta: type: object properties: date_from: type: string description: Datetime in `YYYY-MM-DD HH:mm:ss` format. example: '2026-02-01 00:00:00' date_to: type: string description: Datetime in `YYYY-MM-DD HH:mm:ss` format. example: '2026-02-28 23:59:59' required: - date_from - date_to InvoicingFixedPricingDistributionIncludes: type: object properties: debug_invoicing_period_customers_with_orders_in_date_range: $ref: '#/components/schemas/InvoicingFixedPricingDistributionExecutionTime' debug_invoicing_period_process_customer_numbers: $ref: '#/components/schemas/InvoicingFixedPricingDistributionExecutionTime' debug_invoicing_period_get_transactions_for_customers_in_date_range: $ref: '#/components/schemas/InvoicingFixedPricingDistributionExecutionTime' debug_invoicing_period_calculate_transaction_totals: $ref: '#/components/schemas/InvoicingFixedPricingDistributionExecutionTime' debug_invoicing_period_construct_customer_objects: $ref: '#/components/schemas/InvoicingFixedPricingDistributionExecutionTime' collective_fixed_pricing_results: $ref: '#/components/schemas/InvoicingFixedPricingDistributionCollectiveResults' additionalProperties: true InvoicingFixedPricingDistributionExecutionTime: type: object properties: execution_time: type: number required: - execution_time InvoicingFixedPricingDistributionCollectiveResults: type: object properties: total_fixed_price: type: number total_original_price: type: number total_department_totals: $ref: '#/components/schemas/InvoicingFixedPricingDistributionNumberMapOrEmptyArray' total_department_totals_relative: $ref: '#/components/schemas/InvoicingFixedPricingDistributionNumberMapOrEmptyArray' total_department_totals_parsed: type: object additionalProperties: type: number total_department_totals_relative_parsed: type: object additionalProperties: type: number required: - total_fixed_price - total_original_price - total_department_totals - total_department_totals_relative - total_department_totals_parsed - total_department_totals_relative_parsed InvoicingFixedPricingDistributionNumberMapOrEmptyArray: oneOf: - type: object additionalProperties: type: number - type: array maxItems: 0 SelfServeLaneStatus: type: object properties: id: type: integer description: Lane ID status: type: string description: Current lane status (e.g., IDLE, OCCUPIED) mode: type: string description: Current lane mode (e.g., AUTOMATIC, MANUAL) state: type: string description: Current lane state (e.g., READY, WASHING) wash_start_time: type: integer description: Timestamp when the wash started (0 if not washing) nullable: true elapsed_wash_time: type: integer description: Elapsed wash time in seconds nullable: true license_plate: type: string description: License plate of the vehicle in the lane nullable: true customer_number: type: integer description: Customer number associated with the current lane use nullable: true SelfServeLaneMachineRelayStatus: type: object properties: lane_id: type: integer relay: type: string enum: [MACHINE, MACHINE_PROGRAM_PICKER, MACHINE_CLEANER] relay_id: type: string online: type: boolean on: type: boolean SelfServeConfig: type: object properties: enabled: type: boolean description: Whether the self-serve module is enabled minute_product: type: integer description: The product ID used for minute-based billing machine_wash_minutes_included: type: integer description: Included machine wash minutes before minute-based billing starts SelfserveLaneService: type: string description: Allowed self-serve lane service name enum: - MACHINE SelfserveStudioNode: type: object required: [id, position, data] properties: id: type: string type: type: string nullable: true position: type: object required: [x, y] properties: x: { type: number } y: { type: number } data: type: object additionalProperties: true properties: kind: type: string enum: [question, condition, rule, task, lane, machine_type, vehicle_type, edge_gateway, relay_binding, relay, runtime_checkpoint] object_id: oneOf: - type: integer - type: string nullable: true label: type: string raw: type: object additionalProperties: true SelfserveStudioEdge: type: object required: [id, source, target] properties: id: { type: string } source: { type: string } target: { type: string } type: { type: string, nullable: true } label: { type: string, nullable: true } data: type: object additionalProperties: true SelfserveStudioLayout: type: object properties: nodes: type: object additionalProperties: type: object properties: x: { type: number } y: { type: number } viewport: type: object additionalProperties: true runtime_affecting: type: boolean enum: [false] SelfserveStudioValidation: type: object properties: valid: type: boolean errors: type: array items: { type: string } warnings: type: array items: { type: string } items: type: array items: type: object properties: severity: type: string enum: [error, warning] message: type: string stats: type: object additionalProperties: true validated_at: type: string format: date-time SelfserveConfigVersion: type: object properties: id: { type: integer } department_id: { type: integer } status: type: string enum: [DRAFT, PUBLISHED, ARCHIVED] version_number: { type: integer } config: $ref: '#/components/schemas/SelfserveStudioV2Config' validation_result: $ref: '#/components/schemas/SelfserveStudioValidation' source_version_id: { type: integer, nullable: true } created_by: { type: integer, nullable: true } published_at: { type: string, nullable: true } created_at: { type: string, nullable: true } updated_at: { type: string, nullable: true } SelfserveStudioV2Config: type: object required: [schema_version, questions, conditions, rules, tasks] properties: schema_version: type: integer enum: [2] department_id: type: integer questions: type: array items: type: object additionalProperties: true conditions: type: array items: $ref: '#/components/schemas/SelfserveStudioV2Condition' rules: type: array description: Empty in schema_version 2; retained only for backward-compatible payload shape. maxItems: 0 items: type: object tasks: type: array items: type: object additionalProperties: true v2_meta: type: object additionalProperties: true migration_issues: type: array items: type: object additionalProperties: true SelfserveStudioV2Condition: type: object required: [id, expression] properties: id: type: integer name: type: string description: type: string nullable: true expression: $ref: '#/components/schemas/SelfserveStudioV2Expression' additionalProperties: true SelfserveStudioV2Expression: oneOf: - $ref: '#/components/schemas/SelfserveStudioV2ExpressionGroup' - $ref: '#/components/schemas/SelfserveStudioV2ExpressionPredicate' SelfserveStudioV2ExpressionGroup: type: object required: [type, operator, children] properties: type: type: string enum: [group] operator: type: string enum: [ALL, ANY] children: type: array items: $ref: '#/components/schemas/SelfserveStudioV2Expression' SelfserveStudioV2ExpressionPredicate: type: object required: [type, subject_type, subject_id, operator] properties: type: type: string enum: [predicate] subject_type: type: string enum: [question, condition] subject_id: type: integer operator: type: string enum: [IS_TRUE, IS_FALSE, IS_SET, IS_TRUE_OR_NOT_SET, IS_FALSE_OR_NOT_SET] SelfserveStudioGraph: type: object required: [nodes, edges, lookups, validation, layout, versions, simulator_defaults, gateway_workspace, permissions] properties: nodes: type: array items: $ref: '#/components/schemas/SelfserveStudioNode' edges: type: array items: $ref: '#/components/schemas/SelfserveStudioEdge' lookups: type: object additionalProperties: true validation: $ref: '#/components/schemas/SelfserveStudioValidation' layout: $ref: '#/components/schemas/SelfserveStudioLayout' versions: type: array items: $ref: '#/components/schemas/SelfserveConfigVersion' active_config: allOf: - $ref: '#/components/schemas/SelfserveStudioV2Config' nullable: true draft: type: object additionalProperties: true simulator_defaults: type: object additionalProperties: true gateway_workspace: type: object additionalProperties: true permissions: type: object additionalProperties: type: boolean meta: type: object additionalProperties: true SelfserveStudioGraphOperation: type: object properties: action: type: string enum: [create, update, delete, connect, disconnect, reorder, upsert, upsert_path] entity: type: string enum: [question, condition, task, action, path] description: Standalone rule operations are not accepted for schema_version 2 drafts. id: type: integer nullable: true source: type: string nullable: true target: type: string nullable: true data: type: object additionalProperties: true items: type: array items: type: object additionalProperties: true SelfserveStudioGraphSaveRequest: type: object required: [department] properties: department: { type: integer } operations: type: array items: $ref: '#/components/schemas/SelfserveStudioGraphOperation' nodes: type: array items: $ref: '#/components/schemas/SelfserveStudioNode' layout: $ref: '#/components/schemas/SelfserveStudioLayout' SelfserveStudioLayoutSaveRequest: type: object required: [department, layout] properties: department: { type: integer } layout: $ref: '#/components/schemas/SelfserveStudioLayout' SelfserveMachineType: type: object properties: id: type: integer name: type: string description: type: string nullable: true created_at: type: string format: date-time updated_at: type: string format: date-time nullable: true SelfserveVisibleQuestion: type: object properties: id: type: integer question: type: string description: type: string condition_id: type: integer nullable: true order_priority: type: integer answer: type: boolean nullable: true SelfserveTaskDecision: type: object properties: id: type: integer task: type: string description: type: string condition_id: type: integer nullable: true order_priority: type: integer services: type: array items: $ref: '#/components/schemas/SelfserveLaneService' buttons: type: array items: type: integer SelfserveWashSession: type: object properties: id: type: integer lane_id: type: integer department_id: type: integer machine_type_id: type: integer nullable: true customer_number: type: integer nullable: true vehicle_id: type: integer nullable: true vehicle_type_id: type: integer nullable: true reg: type: string status: type: string enum: - PENDING_QUESTIONS - READY_FOR_MACHINE_START - MACHINE_NOT_ALLOWED - MACHINE_RELAY_ENABLED - MACHINE_STARTED - COMPLETED - FORCE_STOPPED allowed: type: boolean machine_relay_enabled: type: boolean machine_relay_enabled_at: type: string format: date-time nullable: true machine_start_triggered: type: boolean machine_start_triggered_at: type: string format: date-time nullable: true wash_started_at: type: string format: date-time nullable: true order_id: type: integer nullable: true completed_at: type: string format: date-time nullable: true metadata: type: object additionalProperties: true created_at: type: string format: date-time updated_at: type: string format: date-time nullable: true SelfserveWashQuestionAnswer: type: object properties: question_id: type: integer question: type: string answer: type: boolean nullable: true answered_at: type: string format: date-time nullable: true SelfserveWashTaskSnapshot: type: object properties: task_id: type: integer nullable: true task: type: string description: type: string nullable: true services: type: array items: $ref: '#/components/schemas/SelfserveLaneService' buttons: type: array items: type: integer SelfserveWashEvent: type: object properties: id: type: integer type: type: string enum: - SESSION_SYNCED - MACHINE_RELAY_ENABLED - MACHINE_START_TRIGGERED - SESSION_COMPLETED - SESSION_FORCE_STOPPED payload: type: object additionalProperties: true nullable: true created_at: type: string format: date-time SelfserveStudioSimulationDebug: type: object required: [summary, parameters, stages, questions, conditions, rules, tasks, hardware, graph_annotations, recommendations] properties: summary: type: object additionalProperties: true parameters: type: object additionalProperties: true stages: type: array items: type: object additionalProperties: true questions: type: array items: type: object additionalProperties: true conditions: type: array items: type: object additionalProperties: true rules: type: array items: type: object additionalProperties: true tasks: type: array items: type: object additionalProperties: true actions: type: array items: type: object additionalProperties: true dynamic_image_buttons: type: array items: type: object additionalProperties: true decisions: type: array items: type: object required: [kind, id, label, state, reason, node_ids, causes] properties: kind: type: string id: oneOf: - type: integer - type: string nullable: true label: type: string state: type: string reason: type: string node_ids: type: array items: type: string causes: type: array items: type: object additionalProperties: true additionalProperties: true hardware: type: object additionalProperties: true graph_annotations: type: object properties: nodes: type: object additionalProperties: type: object additionalProperties: true edges: type: object additionalProperties: type: object additionalProperties: true recommendations: type: array items: type: object additionalProperties: true SelfserveStudioSimulationResponse: allOf: - $ref: '#/components/schemas/SelfserveVehicleAllowedResponse' - type: object properties: simulator_version: { type: integer } dry_run: { type: boolean, enum: [true] } mode: { type: string, enum: [full_dry_run] } config_source: { type: string, enum: [draft, published] } debug: $ref: '#/components/schemas/SelfserveStudioSimulationDebug' SelfserveStudioPathOutcomesRequest: type: object required: [department] properties: department: type: integer lane_id: type: integer nullable: true vehicle_type_id: type: integer nullable: true config_source: type: string enum: [draft, published] default: draft hardware_mode: type: string enum: [studio, real, none] default: studio include_hardware: type: boolean default: true max_states: type: integer minimum: 1 maximum: 2048 default: 2048 nullable: true description: Optional debug cap for explored states. Omitted and larger values are capped at 2048. path_sample_limit: type: integer minimum: 1 maximum: 200 default: 200 nullable: true description: Optional cap for returned path rows. Omitted and larger values are capped at 200. SelfserveStudioPathOutcomesResponse: type: object required: [scope, summary, outcomes, paths, warnings, truncated, progress] properties: scope: type: object additionalProperties: true summary: type: object required: [state_count, terminal_path_count, outcome_count, question_count, max_states, path_sample_count] properties: state_count: { type: integer } terminal_path_count: { type: integer } outcome_count: { type: integer } question_count: { type: integer } question_ids: type: array items: { type: integer } max_states: { type: integer } path_sample_count: { type: integer } confirmations: $ref: '#/components/schemas/SelfserveStudioPathConfirmationSummary' outcomes: type: array items: $ref: '#/components/schemas/SelfserveStudioPathOutcome' paths: type: array items: $ref: '#/components/schemas/SelfserveStudioPathResult' warnings: type: array items: { type: string } truncated: type: boolean progress: $ref: '#/components/schemas/SelfserveStudioPathProgress' confirmations: type: object properties: summary: $ref: '#/components/schemas/SelfserveStudioPathConfirmationSummary' removed: type: array items: $ref: '#/components/schemas/SelfserveStudioPathConfirmation' SelfserveStudioPathProgress: type: object required: [complete, percent, state_count, pending_state_count, terminal_path_count] properties: complete: { type: boolean } percent: type: integer minimum: 0 maximum: 100 state_count: { type: integer } pending_state_count: { type: integer } terminal_path_count: { type: integer } scenario_index: type: integer nullable: true scenario_count: type: integer nullable: true SelfserveStudioPathOutcome: type: object required: [id, summary, path_count, allowed, services, tasks, signals, sample_chains, node_ids] properties: id: { type: string } summary: { type: string } path_count: { type: integer } allowed: { type: boolean } services: type: array items: $ref: '#/components/schemas/SelfserveLaneService' tasks: type: array items: $ref: '#/components/schemas/SelfserveStudioPathTask' signals: type: array items: $ref: '#/components/schemas/SelfserveStudioPathSignal' sample_chains: type: array items: $ref: '#/components/schemas/SelfserveStudioPathSampleChain' scopes: type: array items: type: object additionalProperties: true node_ids: type: array items: { type: string } SelfserveStudioPathResult: type: object required: [id, result, summary, allowed, services, tasks, signals, task_count, signal_count, answers, scope, node_ids] properties: id: { type: string } result: { type: string } summary: { type: string } allowed: { type: boolean } services: type: array items: $ref: '#/components/schemas/SelfserveLaneService' tasks: type: array items: $ref: '#/components/schemas/SelfserveStudioPathTask' signals: type: array items: $ref: '#/components/schemas/SelfserveStudioPathSignal' task_count: { type: integer } signal_count: { type: integer } answers: type: array items: $ref: '#/components/schemas/SelfserveStudioPathAnswer' scope: type: object additionalProperties: true node_ids: type: array items: { type: string } path_signature: { type: string } result_signature: { type: string } confirmation_status: type: string enum: [unconfirmed, confirmed, stale] confirmed_at: type: string nullable: true confirmed_by: type: integer nullable: true stale_reason: type: string nullable: true SelfserveStudioPathConfirmationSummary: type: object properties: confirmed: { type: integer } unconfirmed: { type: integer } stale: { type: integer } removed: { type: integer } total: { type: integer } SelfserveStudioPathConfirmationRequest: type: object required: [department, path_signature] properties: department: { type: integer } action: type: string enum: [confirm, reset, delete, clear] default: confirm path_signature: { type: string } result_signature: type: string description: Required when action is confirm. scope: type: object additionalProperties: true answers: type: array items: $ref: '#/components/schemas/SelfserveStudioPathAnswer' result: type: object additionalProperties: true SelfserveStudioPathConfirmation: type: object properties: id: type: integer nullable: true department_id: { type: integer } lane_id: type: integer nullable: true vehicle_type_id: type: integer nullable: true config_version_id: type: integer nullable: true config_source: { type: string } path_signature: { type: string } result_signature: { type: string } confirmation_status: type: string enum: [unconfirmed, confirmed, stale] answers: type: array items: $ref: '#/components/schemas/SelfserveStudioPathAnswer' result: type: object additionalProperties: true scope: type: object additionalProperties: true confirmed_at: type: string nullable: true confirmed_by: type: integer nullable: true stale_reason: type: string nullable: true SelfserveStudioPathTask: type: object properties: id: { type: integer } node_id: { type: string } label: { type: string } services: type: array items: $ref: '#/components/schemas/SelfserveLaneService' buttons: type: array items: {} order_priority: { type: integer } SelfserveStudioPathSignal: type: object properties: sequence: { type: integer } runtime_stage: { type: string } signal_type: { type: string } relay_role: { type: string } relay_id: type: string nullable: true target_gateway_label: type: string nullable: true target_binding: type: string nullable: true source: { type: string } virtual: { type: boolean } predicted_status: { type: string } payload: type: object additionalProperties: true skip_block_reason: type: string nullable: true SelfserveStudioPathSampleChain: type: object properties: scope: type: object additionalProperties: true answers: type: array items: $ref: '#/components/schemas/SelfserveStudioPathAnswer' SelfserveStudioPathAnswer: type: object properties: question_id: { type: integer } question: { type: string } node_id: { type: string } answer: { type: boolean } answer_label: { type: string } SelfserveVehicleAllowedResponse: type: object properties: lane: $ref: '#/components/schemas/DepartmentLane' machine_type: allOf: - $ref: '#/components/schemas/SelfserveMachineType' nullable: true vehicle: type: object additionalProperties: true nullable: true reg: type: string customer_number: type: integer nullable: true vehicle_type_id: type: integer nullable: true questions: type: array items: $ref: '#/components/schemas/SelfserveVisibleQuestion' tasks: type: array items: $ref: '#/components/schemas/SelfserveTaskDecision' allowed_services: type: array items: $ref: '#/components/schemas/SelfserveLaneService' machine_available: type: boolean all_visible_questions_answered: type: boolean allowed: type: boolean blocked_reason: type: string nullable: true session: allOf: - $ref: '#/components/schemas/SelfserveWashSession' nullable: true config_source: type: string nullable: true evaluation_trace: type: object nullable: true additionalProperties: true SelfserveWashSummary: type: object properties: session: $ref: '#/components/schemas/SelfserveWashSession' lane: allOf: - $ref: '#/components/schemas/DepartmentLane' nullable: true machine_type: allOf: - $ref: '#/components/schemas/SelfserveMachineType' nullable: true questions: type: array items: $ref: '#/components/schemas/SelfserveWashQuestionAnswer' tasks: type: array items: $ref: '#/components/schemas/SelfserveWashTaskSnapshot' allowed_services: type: array items: $ref: '#/components/schemas/SelfserveLaneService' machine_available: type: boolean all_visible_questions_answered: type: boolean allowed: type: boolean events: type: array items: $ref: '#/components/schemas/SelfserveWashEvent' SelfserveForceStopResponse: type: object properties: lane_id: type: integer forced: type: boolean bill: type: boolean order_id: type: integer nullable: true session: allOf: - $ref: '#/components/schemas/SelfserveWashSummary' nullable: true runtime_before_reset: type: object additionalProperties: true DepartmentSelfserveVehicleConditionMutationResponse: type: object properties: condition: $ref: '#/components/schemas/DepartmentSelfserveVehicleCondition' selfserve: $ref: '#/components/schemas/SelfserveWashSummary' MachineButtonPressWebhookResponse: type: object properties: message: type: string scanner: type: string lane_id: type: integer selfserve: $ref: '#/components/schemas/SelfserveWashSummary' DepartmentSelfserveQuestion: type: object properties: id: type: integer description: Question ID department: type: integer description: Department ID lane: type: integer description: Lane ID product: type: integer description: Product ID condition_id: type: integer description: Question condition object ID nullable: true question: type: string description: Question text description: type: string description: Question description order_priority: type: integer description: Display order priority (lower numbers shown first) created_at: type: string format: date-time updated_at: type: string format: date-time DepartmentSelfserveTask: type: object properties: id: type: integer description: Task ID department: type: integer description: Department ID lane: type: integer description: Lane ID product: type: integer description: Product ID machine_type_id: type: integer description: Reusable machine type ID nullable: true condition_id: type: integer description: Condition ID (if conditional task) nullable: true task: type: string description: Task text description: type: string description: Task description order_priority: type: integer description: Display order priority (lower numbers shown first) services: type: array description: Services enabled by this task. Each item must be a valid service enum name. items: $ref: '#/components/schemas/SelfserveLaneService' default: [] buttons: type: array description: Dynamic image button IDs enabled by this task. items: type: integer default: [] dynamic_images_vehicle_type: type: integer nullable: true description: Optional vehicle type selection override for the machine UI. created_at: type: string format: date-time updated_at: type: string format: date-time DepartmentSelfserveCondition: type: object properties: id: type: integer description: Condition ID department: type: integer description: Department ID lane: type: integer description: Lane ID product: type: integer description: Product ID machine_type_id: type: integer description: Reusable machine type ID nullable: true condition_id: type: integer description: Optional condition ID nullable: true name: type: string description: Condition name description: type: string description: Condition description created_at: type: string format: date-time updated_at: type: string format: date-time DepartmentSelfserveConditionRule: type: object properties: id: type: integer description: Rule ID condition_id: type: integer description: Condition object ID type: type: string description: Condition type (e.g., IS_TRUE, IS_FALSE) object_type: type: string description: The object type to which the condition applies (e.g., question, task, etc.) object_id: type: integer description: The object id to which the condition applies name: type: string description: Condition name description: type: string description: Condition description created_at: type: string format: date-time updated_at: type: string format: date-time DepartmentSelfserveVehicleCondition: type: object properties: id: type: integer description: Vehicle condition ID department: type: integer description: Department ID lane: type: integer description: Lane ID customer_id: type: integer description: Customer ID nullable: true reg: type: string description: Vehicle registration number question: type: integer description: Question ID value: type: boolean description: Answer value created_at: type: string format: date-time updated_at: type: string format: date-time deleted_at: type: string format: date-time nullable: true OrderCreate: type: object required: - customer_id - department_id properties: customer_id: type: integer department_id: type: integer cashier_id: type: integer po: type: string lane: type: string OrderUpdate: type: object properties: id: type: integer customer_id: type: integer department_id: type: integer status: type: string po: type: string lane: type: string OrderItem: type: object properties: id: type: integer order_id: type: integer product_id: type: integer product_name: type: string quantity: type: integer unit_price: type: number format: float discount: type: number format: float total_price: type: number format: float OrderItemCreate: type: object required: - order_id - product_id - quantity properties: order_id: type: integer product_id: type: integer quantity: type: integer discount: type: number format: float OrderItemUpdate: type: object required: - id properties: id: type: integer quantity: type: integer discount: type: number format: float Department: type: object properties: id: type: integer name: type: string description: type: string economic_department_id: type: integer visible: type: boolean dimension: type: integer branding: type: integer longitude: type: number format: float latitude: type: number format: float created_at: type: string format: date-time updated_at: type: string format: date-time DepartmentGuest: type: object properties: id: type: integer name: type: string longitude: type: number format: float latitude: type: number format: float address: type: string description: Department address (same as description) self_serve_enabled: type: boolean lanes: type: array items: $ref: '#/components/schemas/DepartmentLaneGuest' DepartmentLaneGuest: type: object properties: id: type: integer name: type: string status: type: string products: type: array items: type: integer machine_available: type: boolean selfserve_enabled: type: boolean DepartmentCreate: type: object required: - name - economic_department_id properties: name: type: string description: type: string economic_department_id: type: integer visible: type: boolean longitude: type: number format: float latitude: type: number format: float DepartmentUpdate: type: object required: - id properties: id: type: integer name: type: string description: type: string visible: type: boolean longitude: type: number format: float latitude: type: number format: float DepartmentLane: type: object properties: id: type: integer department: type: integer name: type: string relay_in_id: type: string relay_out_id: type: string relay_machine_id: type: string relay_machine_program_picker_id: type: string relay_machine_cleaner_id: type: string dynamic_image_id: type: integer nullable: true minimum: 1 machine_type_id: type: integer nullable: true selfserve_enabled: type: boolean default: true status: type: string created_at: type: string format: date-time updated_at: type: string format: date-time DepartmentLaneCreate: type: object required: - department - name properties: department: type: integer name: type: string relay_in_id: type: string relay_out_id: type: string relay_machine_id: type: string relay_machine_program_picker_id: type: string relay_machine_cleaner_id: type: string dynamic_image_id: type: integer nullable: true minimum: 1 machine_type_id: type: integer nullable: true selfserve_enabled: type: boolean default: true DepartmentLaneUpdate: type: object required: - id properties: id: type: integer department: type: integer name: type: string relay_in_id: type: string relay_out_id: type: string relay_machine_id: type: string relay_machine_program_picker_id: type: string relay_machine_cleaner_id: type: string dynamic_image_id: type: integer nullable: true minimum: 1 machine_type_id: type: integer nullable: true selfserve_enabled: type: boolean DepartmentGate: type: object properties: id: type: integer department: type: integer is_entrance: type: boolean is_exit: type: boolean name: type: string config: $ref: '#/components/schemas/DepartmentGateConfig' created_at: type: string format: date-time updated_at: type: string format: date-time DepartmentGateConfig: type: object required: - type properties: type: type: string example: PHONE_CALL phone_number: type: string nullable: true example: +4512345678 call_duration_threshold: type: integer nullable: true example: 10 description: | Configuration for the department gate. If type is 'PHONE_CALL', 'phone_number' and 'call_duration_threshold' are required. DepartmentGateCreate: type: object required: - department - is_entrance - is_exit - name - config properties: department: type: integer is_entrance: type: boolean is_exit: type: boolean name: type: string config: $ref: '#/components/schemas/DepartmentGateConfig' DepartmentGateUpdate: type: object required: - id properties: id: type: integer is_entrance: type: boolean is_exit: type: boolean name: type: string config: $ref: '#/components/schemas/DepartmentGateConfig' DepartmentRelay: type: object properties: id: type: integer department: type: integer relay_id: type: string name: type: string type: type: string enum: [SWITCH, TRIGGER] config: $ref: '#/components/schemas/DepartmentRelayConfig' created_at: type: string format: date-time updated_at: type: string format: date-time DepartmentRelayConfig: type: object properties: what_happens: type: string nullable: true example: open_gate webhook_token: type: string nullable: true example: secret_token description: | Configuration for the department relay. If the relay 'type' is 'TRIGGER', 'what_happens' and 'webhook_token' are required. DepartmentRelayCreate: type: object required: - department - relay_id - name - type - config properties: department: type: integer relay_id: type: string name: type: string type: type: string enum: [SWITCH, TRIGGER] config: $ref: '#/components/schemas/DepartmentRelayConfig' DepartmentRelayUpdate: type: object required: - id properties: id: type: integer relay_id: type: string name: type: string type: type: string enum: [SWITCH, TRIGGER] config: $ref: '#/components/schemas/DepartmentRelayConfig' Product: type: object properties: id: type: integer name: type: string description: type: string price: type: number format: float category_id: type: integer category_name: type: string visible: type: boolean created_at: type: string format: date-time updated_at: type: string format: date-time ProductCreate: type: object required: - name - price - category_id properties: name: type: string description: type: string price: type: number format: float category_id: type: integer visible: type: boolean ProductUpdate: type: object required: - id properties: id: type: integer name: type: string description: type: string price: type: number format: float category_id: type: integer visible: type: boolean Category: type: object properties: id: type: integer name: type: string description: type: string created_at: type: string format: date-time updated_at: type: string format: date-time CategoryCreate: type: object required: - name properties: name: type: string description: type: string CategoryUpdate: type: object required: - id properties: id: type: integer name: type: string description: type: string ModuleActionLog: type: object properties: id: type: integer description: Log ID module: type: string description: The module name action: type: string description: The action name status_code: type: integer description: HTTP status code data: type: object description: The action data (JSON decoded) created_at: type: string format: date-time description: Log creation timestamp Booking: type: object properties: id: type: integer customer_id: type: integer department_id: type: integer booking_time: type: string format: date-time status: type: string created_at: type: string format: date-time BookingUpdate: type: object required: - id properties: id: type: integer status: type: string booking_time: type: string format: date-time Vehicle: type: object properties: id: type: integer user_id: type: integer description: Internal user ID owning the customer account reg: type: string description: Vehicle registration number customer_id: type: integer customer_name: type: string type: type: integer description: Product ID representing the vehicle wash type reference: type: string nullable: true description: Optional external reference/label wash_subscription: type: boolean barred: type: boolean description: True if the associated customer is barred addons: type: object properties: enabled: { type: integer } available: { type: integer } list: type: array items: type: object last_order_id: type: integer nullable: true xlvask: type: object nullable: true description: XL Vask vehicle data when available vehicle_types: type: array items: type: object created_at: type: string format: date-time VehicleManagementSummary: type: object required: [total, wash_subscription, self_service] properties: total: type: integer minimum: 0 wash_subscription: type: integer minimum: 0 self_service: type: integer minimum: 0 Notification: type: object properties: id: type: integer user_id: type: integer title: type: string message: type: string read: type: boolean created_at: type: string format: date-time NotificationCreate: type: object required: - user_id - title - message properties: user_id: type: integer title: type: string message: type: string Permission: type: object properties: name: type: string description: Permission identifier description: type: string description: Human-readable description GoalsCriteria: type: object description: Goal evaluation criteria properties: type: type: string description: Criteria type enum: [PRODUCT, REVENUE, VISITS, NONE] example: PRODUCT target: type: number description: Target value for the goal example: 100 target_duration: type: string nullable: true description: | Optional advanced target duration mode. Accepted values: ENTIRE_DURATION, WEEKS, MONTHS, YEARS. When omitted, legacy target behavior is preserved for backward compatibility. The canonical field name is snake_case `target_duration`. For backward-compatibility the API also accepts camelCase `targetDuration` on input. enum: [ENTIRE_DURATION, WEEKS, MONTHS, YEARS] example: WEEKS target_duration_every: type: integer nullable: true minimum: 1 description: | Optional cadence value used with `target_duration` WEEKS, MONTHS, or YEARS. Example: with `target_duration=WEEKS` and `target_duration_every=2`, the target applies every second week. Ignored when `target_duration=ENTIRE_DURATION`. The canonical field name is snake_case `target_duration_every`. For backward-compatibility the API also accepts camelCase `targetDurationEvery` on input. example: 1 label: type: string description: Optional short label/title for this goal criteria (max 255 characters) example: Q1 Revenue Goal start: type: string format: date-time description: Start of the evaluation window (ISO 8601) end: type: string format: date-time description: End of the evaluation window (ISO 8601) users: type: array description: List of user customer numbers included in the criteria items: { type: integer } departments: type: array description: List of department IDs included in the criteria items: { type: integer } products: type: array description: List of product IDs included in the criteria items: { type: integer } progress_alert_frequency: type: string description: | Frequency of progress alerts for the goal. Accepted values: DAILY, WEEKLY, MONTHLY, CHANGED, NONE. The canonical field name is snake_case `progress_alert_frequency`. For backward-compatibility the API also accepts camelCase `progressAlertFrequency` on input. enum: [DAILY, WEEKLY, MONTHLY, CHANGED, NONE] example: DAILY progress_alert_destination: type: string description: | Destination/channel where progress alerts should be delivered. Accepted values: SLACK, EMAIL, SMS, NONE. The canonical field name is snake_case `progress_alert_destination`. For backward-compatibility the API also accepts camelCase `progressAlertDestination` on input. enum: [SLACK, EMAIL, SMS, NONE] example: NONE progress_alert_progress_type: type: string description: | What part of the progress should be included in alert messages. Accepted values: ALL, PERCENTAGE_ONLY, COUNT_ONLY, COUNT_AND_TARGET, NONE. The canonical field name is snake_case `progress_alert_progress_type`. For backward-compatibility the API also accepts camelCase `progressAlertProgressType` on input. enum: [ALL, PERCENTAGE_ONLY, COUNT_ONLY, COUNT_AND_TARGET, NONE] example: ALL progress_alert_style: type: string description: | Presentation style of the alert. Accepted values: DEPARTMENT_COMPARE, COLLECTIVE, SINGLE_DEPARTMENT, NONE. The canonical field name is snake_case `progress_alert_style`. For backward-compatibility the API also accepts camelCase `progressAlertStyle` on input. enum: [DEPARTMENT_COMPARE, COLLECTIVE, SINGLE_DEPARTMENT, NONE] example: NONE progress_alert_format: type: string nullable: true description: | Optional custom template for the alert body. Supports tokens `{label}`, `{percent}`, `{count}`, `{target}`, `{timeframe}`, `{departments}`, `{prefix}`, `{body}`. Max length depends on destination: 160 characters for SMS; 1024 characters for EMAIL/SLACK/other. The canonical field name is snake_case `progress_alert_format`. For backward-compatibility the API also accepts camelCase `progressAlertFormat` on input. progress_alert_weekdays: type: array description: | Weekdays on which progress alerts should be sent. Use one or more of: MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY. The canonical field name is snake_case `progress_alert_weekdays`. For backward-compatibility the API also accepts camelCase `progressAlertWeekdays` on input. items: type: string enum: [MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY] example: [MONDAY, WEDNESDAY, FRIDAY] progress_alert_time_of_day: type: string nullable: true description: | Time of day (with timezone) when progress alerts should be sent. Format: `HH:MMZ` or `HH:MM±HH:MM` (24-hour clock with UTC offset). Examples: `14:30Z`, `09:15+02:00`, `18:45-05:00`. The canonical field name is snake_case `progress_alert_time_of_day`. For backward-compatibility the API also accepts camelCase `progressAlertTimeOfDay` on input. pattern: '^([01]\d|2[0-3]):[0-5]\d(?:Z|[+-](?:[01]\d|2[0-3]):?[0-5]\d)$' example: "14:30+02:00" department_daily_targets: type: object description: | Optional per-department custom daily targets. Keys are department IDs and values are non-negative numbers representing the target per operating day for that department. If omitted, the daily target is split evenly across selected departments. The canonical field name is snake_case `department_daily_targets`. For backward-compatibility the API also accepts camelCase `departmentDailyTargets` on input. x-additionalPropertiesName: department_id additionalProperties: type: number minimum: 0 example: "12": 3 "15": 5 GoalProgressDetails: type: object properties: count: type: number description: Current progress value target: type: number description: Target value for the period date_from: type: string nullable: true description: Inclusive period start datetime (ISO-8601), null when no lower bound applies example: "2026-01-01T00:00:00+00:00" date_end: type: string nullable: true description: Inclusive period end datetime (ISO-8601), null when no upper bound applies example: "2026-02-26T23:59:59+00:00" DepartmentGoalProgress: type: object title: Department progress details description: | Goal progress details for a single department across multiple timeframes. Timeframes are clamped to the goal timeframe (never before goal start and never after goal end). properties: all: $ref: '#/components/schemas/GoalProgressDetails' today: $ref: '#/components/schemas/GoalProgressDetails' week: $ref: '#/components/schemas/GoalProgressDetails' month: $ref: '#/components/schemas/GoalProgressDetails' year: $ref: '#/components/schemas/GoalProgressDetails' to_date: $ref: '#/components/schemas/GoalProgressDetails' DepartmentGoal: type: object properties: id: type: integer created_by: type: integer description: ID of the user who created the goal departments: type: array items: { type: integer } criteria: $ref: '#/components/schemas/GoalsCriteria' progress: type: object description: | Goal progress details for various timeframes. `year` starts at January 1 of the current year or the goal start, whichever is later. `to_date` starts at the goal start and ends at today (also clamped by goal end). properties: all: $ref: '#/components/schemas/GoalProgressDetails' today: $ref: '#/components/schemas/GoalProgressDetails' week: $ref: '#/components/schemas/GoalProgressDetails' month: $ref: '#/components/schemas/GoalProgressDetails' year: $ref: '#/components/schemas/GoalProgressDetails' to_date: $ref: '#/components/schemas/GoalProgressDetails' departmental_distribution: type: object description: | Progress details broken down by department. Keys are department IDs. Includes `all`, `today`, `week`, `month`, `year`, and `to_date` timeframes. x-additionalPropertiesName: department_id additionalProperties: $ref: '#/components/schemas/DepartmentGoalProgress' example: "12": all: count: 15 target: 100 today: count: 2 target: 5 week: count: 10 target: 35 month: count: 15 target: 100 year: count: 15 target: 100 to_date: count: 15 target: 100 created_at: type: string description: Creation timestamp updated_at: type: string description: Update timestamp DepartmentGoalCreate: type: object required: [departments, criteria] properties: departments: type: array items: { type: integer } criteria: $ref: '#/components/schemas/GoalsCriteria' DepartmentGoalUpdate: type: object required: [id] properties: id: type: integer departments: type: array items: { type: integer } criteria: $ref: '#/components/schemas/GoalsCriteria' Passkey: type: object properties: id: type: integer credential_id: type: string description: Base64URL-encoded credential ID name: type: string nullable: true algorithm: type: string example: ES256 transports: type: array items: type: string example: ["usb", "nfc", "ble", "internal"] sign_count: type: integer created_at: type: string format: date-time updated_at: type: string format: date-time PasskeyCreateRequest: type: object required: [credential_id, public_key, algorithm, transports] properties: credential_id: type: string description: Base64URL-encoded credential ID returned from WebAuthn public_key: type: string description: Base64URL-encoded public key (COSE or PEM as stored) algorithm: type: string example: ES256 transports: type: array items: type: string name: type: string nullable: true PasskeyRenameRequest: type: object required: [name] properties: name: type: string BirdVoiceCall: type: object properties: id: type: string format: uuid example: "4015cf84-8028-46a1-a0d9-9213e5bf4f09" workspaceId: type: string format: uuid example: "3d5fae4f-9c2d-41aa-9840-28b18e6a94bc" channelId: type: string format: uuid example: "a2545e48-fe8c-5741-9bdc-42a081076bc9" callFlowId: type: string format: uuid nullable: true originator: type: object additionalProperties: true receiver: type: object additionalProperties: true from: type: string example: "+4532330288" to: type: string example: "+4542331128" direction: type: string example: "outgoing" status: type: string example: "completed" type: type: string example: "pstn" duration: type: integer example: 3 hangupCauseCode: type: integer nullable: true hangupSource: type: string nullable: true sipInsights: type: object additionalProperties: true qualityInsights: type: object additionalProperties: true price: type: object additionalProperties: true createdAt: { type: string, format: date-time, nullable: true } updatedAt: { type: string, format: date-time, nullable: true } ringingAt: { type: string, format: date-time, nullable: true } answeredAt: { type: string, format: date-time, nullable: true } endedAt: { type: string, format: date-time, nullable: true } BirdVoiceCallCommandCondition: type: object properties: variable: { type: string } operator: { type: string } value: { type: string } BirdVoiceCallCommandResult: type: object properties: id: type: string format: uuid callId: type: string format: uuid callFlowId: type: string format: uuid nullable: true status: type: string command: type: string conditions: type: array items: $ref: '#/components/schemas/BirdVoiceCallCommandCondition' BirdVoiceCallBridgeResult: allOf: - $ref: '#/components/schemas/BirdVoiceCallCommandResult' - type: object properties: bridgeCallId: type: string format: uuid nullable: true BirdVoiceCallRecording: type: object properties: id: type: string format: uuid callId: type: string format: uuid status: type: string example: ongoing duration: type: integer nullable: true stereo: type: boolean nullable: true mediaUrl: type: string nullable: true createdAt: type: string format: date-time nullable: true updatedAt: type: string format: date-time nullable: true BirdVoiceCallInsights: type: object description: Voice call insights payload as returned by Bird. additionalProperties: true BirdFlashCall: type: object properties: id: type: string format: uuid workspaceId: type: string format: uuid nullable: true channelId: type: string format: uuid nullable: true from: type: string nullable: true to: type: string nullable: true receivedCli: type: string nullable: true result: type: string nullable: true status: type: string nullable: true duration: type: integer nullable: true createdAt: type: string format: date-time nullable: true updatedAt: type: string format: date-time nullable: true BirdVoiceCallListResponse: type: object properties: success: { type: boolean, example: true } data: type: object properties: nextPageToken: type: string nullable: true example: "WzE3NzIxNTY5NTI0MDUsIjk5ZDU4M2VkLTQyMzAtNDExNy1hOTQ0LTllY2JjNzhmYWJlMSJd" results: type: array items: { $ref: '#/components/schemas/BirdVoiceCall' } meta: oneOf: - type: array items: {} - type: object additionalProperties: true example: [] includes: oneOf: - type: array items: {} - type: object additionalProperties: true example: [] BirdVoiceCallSingleResponse: type: object properties: success: { type: boolean, example: true } data: { $ref: '#/components/schemas/BirdVoiceCall' } meta: oneOf: - type: array items: {} - type: object additionalProperties: true example: [] includes: oneOf: - type: array items: {} - type: object additionalProperties: true example: [] BirdVoiceCallCommandResponse: type: object properties: success: { type: boolean, example: true } data: $ref: '#/components/schemas/BirdVoiceCallCommandResult' meta: oneOf: - type: array items: {} - type: object additionalProperties: true example: [] includes: oneOf: - type: array items: {} - type: object additionalProperties: true example: [] BirdVoiceCallBridgeResponse: type: object properties: success: { type: boolean, example: true } data: $ref: '#/components/schemas/BirdVoiceCallBridgeResult' meta: oneOf: - type: array items: {} - type: object additionalProperties: true example: [] includes: oneOf: - type: array items: {} - type: object additionalProperties: true example: [] BirdVoiceCallRecordingListResponse: type: object properties: success: { type: boolean, example: true } data: type: object properties: nextPageToken: type: string nullable: true results: type: array items: $ref: '#/components/schemas/BirdVoiceCallRecording' meta: oneOf: - type: array items: {} - type: object additionalProperties: true example: [] includes: oneOf: - type: array items: {} - type: object additionalProperties: true example: [] BirdVoiceCallRecordingSingleResponse: type: object properties: success: { type: boolean, example: true } data: $ref: '#/components/schemas/BirdVoiceCallRecording' meta: oneOf: - type: array items: {} - type: object additionalProperties: true example: [] includes: oneOf: - type: array items: {} - type: object additionalProperties: true example: [] BirdVoiceCallInsightsResponse: type: object properties: success: { type: boolean, example: true } data: $ref: '#/components/schemas/BirdVoiceCallInsights' meta: oneOf: - type: array items: {} - type: object additionalProperties: true example: [] includes: oneOf: - type: array items: {} - type: object additionalProperties: true example: [] BirdVoiceCallsLogResponse: type: object properties: success: { type: boolean, example: true } data: type: object properties: nextPageToken: type: string nullable: true results: type: array items: $ref: '#/components/schemas/BirdVoiceCall' meta: oneOf: - type: array items: {} - type: object additionalProperties: true example: [] includes: oneOf: - type: array items: {} - type: object additionalProperties: true example: [] BirdTestOutboundCallRequest: type: object additionalProperties: false properties: from: type: string description: Caller E.164 number to use for the test call example: "+4599988877" to: type: string description: Target E.164 number. Defaults to configured test number if omitted. example: "+4542331128" timeout: type: integer minimum: 1 description: Backward-compatible alias mapped to ringTimeout ringTimeout: type: integer minimum: 3 maximum: 120 pollIntervalSeconds: type: integer minimum: 1 description: Poll interval while waiting for accepted status example: 2 maxPollSeconds: type: integer minimum: 5 description: Max time to wait before timing out example: 30 hangupCause: type: string enum: [rejected, busy] description: Optional hangup cause passed through to Bird BirdTestOutboundCallResponse: type: object properties: success: { type: boolean, example: true } data: type: object properties: to: { type: string, example: "+45 42 33 11 28" } to_e164: { type: string, example: "+4542331128" } call_id: { type: string, nullable: true, example: "4015cf84-8028-46a1-a0d9-9213e5bf4f09" } final_status: { type: string, nullable: true, example: "completed" } hangup_sent: { type: boolean, example: true } created_call: { $ref: '#/components/schemas/BirdVoiceCall' } last_call_snapshot: { $ref: '#/components/schemas/BirdVoiceCall' } hangup_response: { $ref: '#/components/schemas/BirdVoiceCallCommandResult' } meta: oneOf: - type: array items: {} - type: object additionalProperties: true example: [] includes: oneOf: - type: array items: {} - type: object additionalProperties: true example: [] BirdInboundCallWebhookRequest: type: object additionalProperties: true properties: callId: type: string example: "4015cf84-8028-46a1-a0d9-9213e5bf4f09" workspaceId: type: string format: uuid channelId: type: string format: uuid payload: type: object additionalProperties: true properties: endKey: type: string example: "#" retries: type: integer example: 3 timeout: type: integer example: 30 say: type: object additionalProperties: true properties: locale: type: string example: "en-US" voice: type: string example: "female" request: type: object additionalProperties: true properties: callId: type: string example: "4015cf84-8028-46a1-a0d9-9213e5bf4f09" workspaceId: type: string format: uuid channelId: type: string format: uuid waitConditions: type: object additionalProperties: true properties: timeout: type: string example: "PT10M" events: type: array items: type: object additionalProperties: true properties: action: type: string example: "continue" name: type: string example: "call_command_gather_finished" event: type: object additionalProperties: true result: type: object additionalProperties: true resumeData: type: object additionalProperties: true dtmf: type: string description: DTMF value when present, for example `1`, `1#`, or `10#` example: "10#" digit: type: string description: Alternate DTMF field, also accepts values such as `10#` example: "10#" digits: type: string description: Alternate DTMF field example: "10#" keys: type: string description: Alternate DTMF field returned by gather results example: "10#" key: type: string example: "1" input: oneOf: - type: string - type: object additionalProperties: true conditions: type: array items: type: object additionalProperties: true BirdInboundCallWebhookResponse: type: object oneOf: - $ref: '#/components/schemas/BirdInboundCallWebhookFlowGatherResponse' - $ref: '#/components/schemas/BirdInboundCallWebhookActionResultResponse' - $ref: '#/components/schemas/BirdInboundCallWebhookGatherAcceptedResponse' - $ref: '#/components/schemas/BirdInboundCallWebhookTransportErrorResponse' BirdInboundCallWebhookFlowGatherResponse: type: object properties: requestId: type: string example: "request-123" callId: type: string example: "4015cf84-8028-46a1-a0d9-9213e5bf4f09" status: type: string enum: [gather] completed: type: boolean enum: [false] stage: type: string enum: [department_select, gate_type_select] prompt: type: string gather: type: object properties: input: type: string enum: [dtmf] maxNumKeys: type: integer endKey: type: string example: "#" timeout: type: integer retries: type: integer say: type: object properties: locale: type: string example: "en-US" voice: type: string example: "female" text: type: string selection: type: object properties: departmentId: type: integer nullable: true departmentName: type: string nullable: true gateType: type: string nullable: true enum: [entrance, exit] gateId: type: integer nullable: true invalidSelectionCount: type: integer resumed: type: boolean statusCode: type: integer enum: [200] statusText: type: string enum: [OK] BirdInboundCallWebhookActionResultResponse: type: object properties: requestId: type: string example: "request-123" result: type: object properties: callId: type: string example: "4015cf84-8028-46a1-a0d9-9213e5bf4f09" status: type: string enum: [completed, failed, ignored] action: type: string enum: [gate_opened, gate_open_failed, no_action, ignored] message: type: string departmentId: type: integer nullable: true gateType: type: string nullable: true enum: [entrance, exit] gateId: type: integer nullable: true gateOpened: type: boolean resumeData: type: object additionalProperties: true properties: action: type: string example: "continue" completed: type: boolean example: true result: type: string example: "gate_opened" gateOpened: type: boolean example: true completedAt: type: string format: date-time statusCode: type: integer enum: [200] statusText: type: string enum: [OK] BirdInboundCallWebhookGatherAcceptedResponse: type: object properties: event: type: object additionalProperties: true requestId: type: string example: "request-123" result: type: object properties: callId: type: string example: "4015cf84-8028-46a1-a0d9-9213e5bf4f09" command: type: string enum: [gather] id: type: string status: type: string example: "accepted" resumeData: type: object properties: action: type: string example: "continue" resumedAt: type: string format: date-time nullable: true suspendedAt: type: string format: date-time statusCode: type: integer enum: [202] statusText: type: string enum: [Accepted] BirdInboundCallWebhookTransportErrorResponse: type: object properties: requestId: type: string statusCode: type: integer enum: [400, 500] statusText: type: string enum: [Bad Request, Internal Server Error] error: type: object properties: message: type: string BirdVoiceCallCreateRequest: type: object additionalProperties: false required: [to] properties: from: { type: string } to: { type: string } ringTimeout: { type: integer, minimum: 3, maximum: 120 } maxDuration: { type: integer, minimum: 1 } sendKeys: { type: string } record: { type: boolean } recordStart: { type: string, enum: [record-from-answer, record-from-ringing] } flowStart: { type: string, enum: [from-answer, from-ringing] } stereo: { type: boolean } callFlow: type: array items: type: object additionalProperties: true scheduledFor: { type: string, format: date-time } notification: type: object additionalProperties: false properties: url: { type: string } amdSettings: type: object additionalProperties: true tags: type: array items: { type: string } BirdVoiceCallUpdateRequest: type: object additionalProperties: false properties: status: type: string enum: [completed] callFlow: type: array items: type: object additionalProperties: true BirdVoiceCallAnswerRequest: type: object additionalProperties: false properties: {} BirdVoiceCallRingingRequest: type: object additionalProperties: false properties: {} BirdVoiceCallHangupRequest: type: object additionalProperties: false properties: cause: type: string enum: [rejected, busy] BirdVoiceCallPlaybackRequest: type: object additionalProperties: false required: [media] properties: media: type: array minItems: 1 items: { type: string } loop: { type: integer, minimum: 0 } timeout: { type: integer, minimum: 0 } pauseMilliseconds: { type: integer, minimum: 0, maximum: 30000 } BirdVoiceCallSayRequest: type: object additionalProperties: false required: [text] properties: text: { type: string } locale: { type: string } voice: { type: string } loop: { type: integer, minimum: 0 } timeout: { type: integer, minimum: 0 } hangup: { type: boolean } BirdVoiceCallGatherRequest: type: object additionalProperties: false properties: maxNumKeys: { type: integer, minimum: 1 } endKey: { type: string, enum: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '*', '#'] } timeout: { type: integer, minimum: 0 } retries: { type: integer, minimum: 0 } input: { type: string, enum: [dtmf, speech, 'dtmf speech'] } speechLocale: { type: string } playback: $ref: '#/components/schemas/BirdVoiceCallPlaybackRequest' say: $ref: '#/components/schemas/BirdVoiceCallSayRequest' BirdVoiceCallBridgeRequest: type: object additionalProperties: false required: [to] properties: from: { type: string } to: { type: string } ringTimeout: { type: integer, minimum: 3, maximum: 120 } maxDuration: { type: integer, minimum: 1 } ringTone: { type: string } hangupAfterBridge: { type: boolean } record: { type: boolean } recordStart: { type: string, enum: [record-from-answer, record-from-ringing] } recordStereo: { type: boolean } callFlow: type: array items: type: object additionalProperties: true notification: type: object additionalProperties: false properties: url: { type: string } amdSettings: type: object additionalProperties: true BirdVoiceCallRecordRequest: type: object additionalProperties: false properties: endKey: { type: string, enum: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '*', '#'] } maxLength: { type: integer, minimum: 1 } timeout: { type: integer, minimum: 0 } beep: { type: boolean } transcribe: { type: boolean } transcribeLocale: { type: string } BirdVoiceCallRecordingCreateRequest: type: object additionalProperties: false properties: maxLength: { type: integer, minimum: 1 } stereo: { type: boolean } BirdVoiceCallRecordingUpdateRequest: type: object additionalProperties: false required: [status] properties: status: type: string enum: [paused, ongoing, completed] BirdFlashCallCreateRequest: type: object additionalProperties: false required: [to] properties: from: { type: string } to: { type: string } ringTimeout: { type: integer, minimum: 3, maximum: 120 } BirdFlashCallEndRequest: type: object additionalProperties: false required: [result] properties: receivedCli: { type: string } result: type: string enum: [unknown, verified, canceled, timeout, wrong_cli] BirdFlashCallHangupRequest: oneOf: - type: object additionalProperties: false required: [result] properties: receivedCli: { type: string } result: type: string enum: [unknown, verified, canceled, timeout, wrong_cli] - type: object additionalProperties: false required: [from, to] properties: from: { type: string } to: { type: string } receivedCli: { type: string } result: type: string enum: [unknown, verified, canceled, timeout, wrong_cli] BirdFlashCallListResponse: type: object properties: success: { type: boolean, example: true } data: type: object properties: nextPageToken: type: string nullable: true results: type: array items: $ref: '#/components/schemas/BirdFlashCall' meta: oneOf: - type: array items: {} - type: object additionalProperties: true example: [] includes: oneOf: - type: array items: {} - type: object additionalProperties: true example: [] BirdFlashCallSingleResponse: type: object properties: success: { type: boolean, example: true } data: $ref: '#/components/schemas/BirdFlashCall' meta: oneOf: - type: array items: {} - type: object additionalProperties: true example: [] includes: oneOf: - type: array items: {} - type: object additionalProperties: true example: [] BirdFlashCallHangupResult: type: object properties: id: type: string format: uuid nullable: true result: type: string nullable: true receivedCli: type: string nullable: true from: type: string nullable: true to: type: string nullable: true BirdFlashCallHangupResponse: type: object properties: success: { type: boolean, example: true } data: $ref: '#/components/schemas/BirdFlashCallHangupResult' meta: oneOf: - type: array items: {} - type: object additionalProperties: true example: [] includes: oneOf: - type: array items: {} - type: object additionalProperties: true example: [] BirdNumber: type: object properties: id: type: string example: "019c73dc-60f1-76c4-98b0-c4318706b938" workspaceId: type: string example: "3d5fae4f-9c2d-41aa-9840-28b18e6a94bc" type: type: string example: "national" country: type: string example: "DK" number: type: string example: "+4532330288" status: type: string example: "active" capabilities: type: object properties: voice: { $ref: '#/components/schemas/BirdNumberCapability' } sms: { $ref: '#/components/schemas/BirdNumberCapability' } mms: { $ref: '#/components/schemas/BirdNumberCapability' } fax: { $ref: '#/components/schemas/BirdNumberCapability' } whatsapp: { $ref: '#/components/schemas/BirdNumberCapability' } monthlyRecurringPrice: type: object properties: currencyCode: { type: string, example: "EUR" } amount: { type: integer, example: 1000000 } exponent: { type: integer, example: -6 } complianceRequirements: type: array items: type: object additionalProperties: true configurations: type: object additionalProperties: true createdAt: { type: string, format: date-time, example: "2026-02-19T03:05:48.529Z" } updatedAt: { type: string, format: date-time, example: "2026-02-19T03:08:17.753Z" } activatedAt: { type: string, format: date-time, nullable: true, example: "2026-02-19T03:05:48.529Z" } deactivatedAt: { type: string, format: date-time, nullable: true } deactivatesAt: { type: string, format: date-time, nullable: true } subscription: type: object additionalProperties: true endpointSubscription: type: object additionalProperties: true requirements: type: array items: type: object additionalProperties: true whatsApp: type: object additionalProperties: true endpoint: type: object additionalProperties: true BirdNumberCapability: type: object properties: inbound: { type: boolean } outbound: { type: boolean } BirdNumberListResponse: type: object properties: success: { type: boolean, example: true } data: type: object properties: results: type: array items: { $ref: '#/components/schemas/BirdNumber' } meta: oneOf: - type: array items: {} - type: object additionalProperties: true example: [] includes: oneOf: - type: array items: {} - type: object additionalProperties: true example: [] BirdNumberSingleResponse: type: object properties: success: { type: boolean, example: true } data: { $ref: '#/components/schemas/BirdNumber' } meta: oneOf: - type: array items: {} - type: object additionalProperties: true example: [] includes: oneOf: - type: array items: {} - type: object additionalProperties: true example: [] DepartmentDailyReportOutsideHoursBreakdown: type: object properties: orders: { type: integer } xlvask: { type: integer } selfserve: { type: integer } DepartmentDailyReportOutsideHoursSummary: type: object properties: department_ids: type: array items: { type: integer } date: { type: string } date_to: { type: string } total: { type: integer } by_source: $ref: '#/components/schemas/DepartmentDailyReportOutsideHoursBreakdown' has_missing_opening_hours: { type: boolean } missing_department_ids: type: array items: { type: integer } DepartmentDailyReportMetric: type: object properties: state: { type: string } value: type: number nullable: true out_of: type: number nullable: true message: type: string nullable: true by_source: $ref: '#/components/schemas/DepartmentDailyReportOutsideHoursBreakdown' has_missing_opening_hours: { type: boolean } missing_department_ids: type: array items: { type: integer } DepartmentDailyReportComplaintCreateRequest: type: object required: [department_id, wash_date, category, description] properties: department_id: { type: integer } customer_number: type: integer nullable: true wash_date: type: string format: date category: $ref: '#/components/schemas/DepartmentDailyReportComplaintCategory' description: type: string minLength: 1 maxLength: 4000 DepartmentDailyReportComplaintUpdateRequest: type: object required: [id] properties: id: { type: integer } department_id: { type: integer } customer_number: type: integer nullable: true wash_date: type: string format: date category: $ref: '#/components/schemas/DepartmentDailyReportComplaintCategory' description: type: string minLength: 1 maxLength: 4000 DepartmentDailyReportComplaintCategory: type: string enum: - wash_quality - wash_price - damage_paint - damage_mirrors - damage_cables_electronics - damage_plastic_parts - damage_other - service - other DepartmentDailyReportComplaintCustomerSearchResult: type: object properties: customer_number: type: integer customer_name: type: string nullable: true DepartmentDailyReportComplaint: type: object properties: id: { type: integer } department_id: { type: integer } department_name: type: string nullable: true customer_number: type: integer nullable: true customer_name: type: string nullable: true wash_date: type: string format: date nullable: true category: allOf: - $ref: '#/components/schemas/DepartmentDailyReportComplaintCategory' nullable: true description: { type: string } created_by: { type: integer } created_by_name: type: string nullable: true created_at: type: string format: date-time DepartmentDailyReportComplaintResponse: type: object properties: success: { type: boolean, example: true } data: $ref: '#/components/schemas/DepartmentDailyReportComplaint' DepartmentDailyReportComplaintCollectionResponse: type: object properties: success: { type: boolean, example: true } data: oneOf: - $ref: '#/components/schemas/DepartmentDailyReportComplaint' - type: array items: $ref: '#/components/schemas/DepartmentDailyReportComplaint' DepartmentDailyReportComplaintCustomerSearchResponse: type: object properties: success: { type: boolean, example: true } data: type: array items: $ref: '#/components/schemas/DepartmentDailyReportComplaintCustomerSearchResult' DepartmentDailyReportComplaintDeleteResponse: type: object properties: success: { type: boolean, example: true } data: type: object properties: message: { type: string } DepartmentDailyReportProductTile: type: object properties: product_id: { type: integer } slug: { type: string } title: { type: string } state: { type: string } value: { type: integer } out_of: { type: integer } target_percentage: { type: number, format: float, nullable: true } target_department_id: { type: integer, nullable: true } DepartmentDailyReportProductTargetRequest: type: object required: - department_id - product_id - target_percentage properties: department_id: { type: integer } product_id: { type: integer } target_percentage: type: number format: float nullable: true DepartmentDailyReportProductTarget: type: object properties: department_id: { type: integer } product_id: { type: integer } target_percentage: { type: number, format: float, nullable: true } DepartmentDailyReportProductTargetResponse: type: object properties: success: { type: boolean, example: true } data: $ref: '#/components/schemas/DepartmentDailyReportProductTarget' DepartmentDailyReportOverviewPayload: type: object properties: department_ids: type: array items: { type: integer } date: { type: string } date_to: { type: string } metrics: type: object additionalProperties: $ref: '#/components/schemas/DepartmentDailyReportMetric' products: type: array items: $ref: '#/components/schemas/DepartmentDailyReportProductTile' DepartmentDailyReportOverviewResponse: type: object properties: success: { type: boolean, example: true } data: $ref: '#/components/schemas/DepartmentDailyReportOverviewPayload' DepartmentDailyReportTransactionCountPayload: type: object properties: quantity: { type: integer } products: { type: integer } earnings: { type: integer } washes: { type: integer } water_usage: { type: integer } date: { type: string } date_to: { type: string } department_id: { type: integer } outside_hours: $ref: '#/components/schemas/DepartmentDailyReportOutsideHoursSummary' DepartmentDailyReportTransactionCountResponse: type: object properties: success: { type: boolean, example: true } data: $ref: '#/components/schemas/DepartmentDailyReportTransactionCountPayload' DepartmentDailyReportOutsideHoursTrendPoint: type: object properties: date: { type: string } total: { type: integer } by_source: $ref: '#/components/schemas/DepartmentDailyReportOutsideHoursBreakdown' has_missing_opening_hours: { type: boolean } missing_department_ids: type: array items: { type: integer } DepartmentDailyReportOutsideHoursTrendPayload: type: object properties: department_ids: type: array items: { type: integer } date: { type: string } date_to: { type: string } points: type: array items: $ref: '#/components/schemas/DepartmentDailyReportOutsideHoursTrendPoint' has_missing_opening_hours: { type: boolean } missing_department_ids: type: array items: { type: integer } DepartmentDailyReportOutsideHoursTrendResponse: type: object properties: success: { type: boolean, example: true } data: $ref: '#/components/schemas/DepartmentDailyReportOutsideHoursTrendPayload'