{
  "openapi": "3.0.0",
  "paths": {
    "/api/audit": {
      "get": {
        "operationId": "AuditController_findAll",
        "parameters": [
          {
            "name": "action",
            "required": false,
            "in": "query",
            "schema": {
              "enum": [
                "api_key_created",
                "api_key_updated",
                "api_key_used",
                "api_key_revoked",
                "api_key_deleted",
                "api_key_auth_failed",
                "rate_limit_exceeded",
                "queue_board_mutated",
                "session_created",
                "session_started",
                "session_stopped",
                "session_force_killed",
                "session_logged_out",
                "session_deleted",
                "session_qr_generated",
                "session_connected",
                "session_disconnected",
                "session_restricted",
                "session_restriction_lifted",
                "message_sent",
                "message_failed",
                "send_pacing_blocked",
                "send_breaker_tripped",
                "webhook_created",
                "webhook_deleted",
                "webhook_triggered",
                "webhook_failed",
                "integration_instance_created",
                "integration_instance_updated",
                "integration_instance_secret_regenerated",
                "integration_instance_deleted",
                "integration_instance_redriven",
                "infra_config_saved",
                "infra_restart_requested",
                "infra_data_exported",
                "infra_data_imported",
                "infra_storage_exported",
                "infra_storage_imported"
              ],
              "type": "string"
            }
          },
          {
            "name": "severity",
            "required": false,
            "in": "query",
            "schema": {
              "enum": [
                "info",
                "warn",
                "error"
              ],
              "type": "string"
            }
          },
          {
            "name": "sessionId",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "apiKeyId",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "required": false,
            "in": "query",
            "schema": {
              "type": "number"
            }
          },
          {
            "name": "offset",
            "required": false,
            "in": "query",
            "schema": {
              "type": "number"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated list of audit logs"
          }
        },
        "summary": "List audit logs with optional filters",
        "tags": [
          "audit"
        ]
      }
    },
    "/api/auth/api-keys": {
      "post": {
        "operationId": "AuthController_create",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateApiKeyDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "API key created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiKeyCreatedResponseDto"
                }
              }
            }
          }
        },
        "summary": "Create a new API key (admin only)",
        "tags": [
          "auth"
        ]
      },
      "get": {
        "operationId": "AuthController_findAll",
        "parameters": [],
        "responses": {
          "200": {
            "description": "All API keys (the plaintext key is never returned; only the keyPrefix).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ApiKeyResponseDto"
                  }
                }
              }
            }
          }
        },
        "summary": "List all API keys (admin only)",
        "tags": [
          "auth"
        ]
      }
    },
    "/api/auth/api-keys/{id}": {
      "get": {
        "operationId": "AuthController_findOne",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The API key (plaintext never returned; only the keyPrefix).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiKeyResponseDto"
                }
              }
            }
          }
        },
        "summary": "Get API key details (admin only)",
        "tags": [
          "auth"
        ]
      },
      "put": {
        "operationId": "AuthController_update",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateApiKeyDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiKeyResponseDto"
                }
              }
            }
          },
          "409": {
            "description": "The change would remove the last usable admin key."
          }
        },
        "summary": "Update API key (admin only)",
        "tags": [
          "auth"
        ]
      },
      "delete": {
        "operationId": "AuthController_delete",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "API key deleted"
          },
          "409": {
            "description": "The key is the last usable admin key."
          }
        },
        "summary": "Delete API key (admin only)",
        "tags": [
          "auth"
        ]
      }
    },
    "/api/auth/api-keys/{id}/revoke": {
      "post": {
        "operationId": "AuthController_revoke",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The revoked API key (isActive now false).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiKeyResponseDto"
                }
              }
            }
          },
          "409": {
            "description": "The key is the last usable admin key."
          }
        },
        "summary": "Revoke API key (admin only)",
        "tags": [
          "auth"
        ]
      }
    },
    "/api/auth/validate": {
      "post": {
        "operationId": "AuthValidateController_validate",
        "parameters": [
          {
            "name": "X-API-Key",
            "in": "header",
            "description": "API key to validate",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "API key is valid"
          },
          "401": {
            "description": "Invalid or missing API key"
          }
        },
        "summary": "Validate an API key",
        "tags": [
          "auth"
        ]
      }
    },
    "/api/sessions": {
      "post": {
        "operationId": "SessionController_create",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateSessionDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Session created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SessionResponseDto"
                }
              }
            }
          },
          "409": {
            "description": "Session name already exists"
          }
        },
        "summary": "Create a new WhatsApp session",
        "tags": [
          "sessions"
        ]
      },
      "get": {
        "operationId": "SessionController_findAll",
        "parameters": [
          {
            "name": "limit",
            "required": false,
            "in": "query",
            "description": "Max sessions to return (1-1000, default 1000)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "offset",
            "required": false,
            "in": "query",
            "description": "Number of sessions to skip (for paging)",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of sessions",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/SessionResponseDto"
                  }
                }
              }
            }
          }
        },
        "summary": "List all sessions",
        "tags": [
          "sessions"
        ]
      }
    },
    "/api/sessions/{id}": {
      "get": {
        "operationId": "SessionController_findOne",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Session details",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SessionResponseDto"
                }
              }
            }
          },
          "404": {
            "description": "Session not found"
          }
        },
        "summary": "Get session by ID",
        "tags": [
          "sessions"
        ]
      },
      "delete": {
        "operationId": "SessionController_delete",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Session deleted"
          },
          "404": {
            "description": "Session not found"
          },
          "409": {
            "description": "A credential teardown for the same session name is still in flight (retryable — the body carries `code: 'SESSION_NAME_TEARDOWN_PENDING'`; wait for it to settle and retry), OR another node currently holds this session's live engine and deleting it here would strip a session the owner is running. No destructive side effect runs before either refusal."
          }
        },
        "summary": "Delete a session",
        "tags": [
          "sessions"
        ]
      }
    },
    "/api/sessions/{id}/start": {
      "post": {
        "operationId": "SessionController_start",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Session started",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SessionResponseDto"
                }
              }
            }
          },
          "400": {
            "description": "Session already started"
          },
          "404": {
            "description": "Session not found"
          },
          "409": {
            "description": "A credential teardown for the same session name is still in flight (e.g. a prior logout that owns destructive cleanup). Retryable — the body carries `code: SESSION_NAME_TEARDOWN_PENDING`; wait for it to settle and retry. No destructive side effect runs before this refusal. Also returned when another node currently holds this session's engine: only the owner may start it, and the claim is refused before any engine is launched, so no second connection to the account is opened."
          }
        },
        "summary": "Start a session and initialize WhatsApp connection",
        "tags": [
          "sessions"
        ]
      }
    },
    "/api/sessions/{id}/stop": {
      "post": {
        "operationId": "SessionController_stop",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Session stopped",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SessionResponseDto"
                }
              }
            }
          },
          "404": {
            "description": "Session not found"
          },
          "409": {
            "description": "Another node currently holds this session's live engine (multi-node deployments): stopping it here would report the session down while the owner keeps running it, so the request is refused. Retry against the owning node, or once its lease has lapsed."
          }
        },
        "summary": "Stop a session and disconnect WhatsApp",
        "tags": [
          "sessions"
        ]
      }
    },
    "/api/sessions/{id}/logout": {
      "post": {
        "description": "Attempts an engine-native unlink of this companion device, then tears the session down locally. `200` means the engine-native unlink operation completed AND the required local credential cleanup completed — for Baileys a valid companion identity, an acknowledged `remove-companion-device` IQ response, and removal of the on-disk auth dir; for whatsapp-web.js the native `Client.logout()` promise settled. `200` is NOT an independent observation that the handset UI no longer shows the linked device. Because a completed unlink wipes the stored credentials, reconnecting after a `200` always requires a fresh QR scan or pairing code.",
        "operationId": "SessionController_logout",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Unlink operation and required local cleanup completed; session is stopped and `phone` is cleared. Recorded in the audit log as `session_logged_out`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SessionResponseDto"
                },
                "example": {
                  "id": "8f3c2b1a-9d4e-4c7a-8b2f-1e6d5a4c3b2a",
                  "name": "my-bot",
                  "status": "disconnected",
                  "phone": null,
                  "pushName": null,
                  "connectedAt": null,
                  "lastActive": "2026-06-25T09:01:55.000Z",
                  "createdAt": "2026-06-20T11:30:00.000Z",
                  "updatedAt": "2026-06-25T09:11:00.000Z",
                  "lastError": null
                }
              }
            }
          },
          "400": {
            "description": "Session is not started (no engine to send through); the row is left untouched"
          },
          "404": {
            "description": "Session not found"
          },
          "502": {
            "description": "Session was stopped locally, but the logout operation is incomplete (no send, no acknowledgement, timeout/transport error, or local-cleanup failure). Retryable — the body carries `code: 'SESSION_LOGOUT_INCOMPLETE'`; `phone` is cleared and no success audit is written. Start the session again and retry the logout."
          }
        },
        "summary": "Log out of WhatsApp (unlinks this device) and stop the session",
        "tags": [
          "sessions"
        ]
      }
    },
    "/api/sessions/{id}/force-kill": {
      "post": {
        "operationId": "SessionController_forceKill",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Session force-killed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SessionResponseDto"
                }
              }
            }
          },
          "400": {
            "description": "Session is not started"
          },
          "404": {
            "description": "Session not found"
          }
        },
        "summary": "Force-kill a stuck session (SIGKILL its wedged engine, then tear it down)",
        "tags": [
          "sessions"
        ]
      }
    },
    "/api/sessions/{id}/qr": {
      "get": {
        "operationId": "SessionController_getQRCode",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "QR code data",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QRCodeResponseDto"
                }
              }
            }
          },
          "400": {
            "description": "QR code not ready or session already authenticated"
          },
          "404": {
            "description": "Session not found"
          }
        },
        "summary": "Get QR code for session authentication",
        "tags": [
          "sessions"
        ]
      }
    },
    "/api/sessions/{id}/pairing-code": {
      "post": {
        "operationId": "SessionController_requestPairingCode",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RequestPairingCodeDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Pairing code generated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PairingCodeResponseDto"
                }
              }
            }
          },
          "400": {
            "description": "Session not started or already authenticated"
          },
          "404": {
            "description": "Session not found"
          }
        },
        "summary": "Request an 8-char pairing code to link via phone number (alternative to QR)",
        "tags": [
          "sessions"
        ]
      }
    },
    "/api/sessions/{id}/groups": {
      "get": {
        "operationId": "SessionController_getGroups",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "required": false,
            "in": "query",
            "description": "Max groups to return (1–1000, default 1000)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "offset",
            "required": false,
            "in": "query",
            "description": "Number of groups to skip (for paging)",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of groups the session is a member of"
          },
          "400": {
            "description": "Session not ready"
          },
          "404": {
            "description": "Session not found"
          }
        },
        "summary": "Get all groups for a session",
        "tags": [
          "sessions"
        ]
      }
    },
    "/api/sessions/{id}/chats": {
      "get": {
        "operationId": "SessionController_getChats",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "required": false,
            "in": "query",
            "description": "Max chats to return (1–1000, default 1000)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "offset",
            "required": false,
            "in": "query",
            "description": "Number of chats to skip (for paging)",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of active chats (most recent first)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ChatSummaryDto"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Session not ready"
          },
          "404": {
            "description": "Session not found"
          }
        },
        "summary": "Get active chats for a session",
        "tags": [
          "sessions"
        ]
      }
    },
    "/api/sessions/{id}/chats/read": {
      "post": {
        "operationId": "SessionController_markChatRead",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MarkChatReadDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Chat marked as read successfully"
          },
          "400": {
            "description": "Session not ready"
          },
          "404": {
            "description": "Session not found"
          }
        },
        "summary": "Mark a chat as read/seen",
        "tags": [
          "sessions"
        ]
      }
    },
    "/api/sessions/{id}/presence/subscribe": {
      "post": {
        "description": "Asks WhatsApp to start reporting who is online or typing in this chat. Updates arrive as the `presence.update` webhook and socket event — there is no synchronous answer, because presence cannot be queried from either engine, only received.\n\nThe subscription belongs to the connection: it does **not** survive a restart or an automatic reconnect, and must be re-issued. Subscribe per chat rather than to everything — WhatsApp emits an update on every transition, so a broad subscription is a firehose.\n\nwhatsapp-web.js cannot do this at all (it exposes no presence subscribe and emits no presence event) and answers `501`.",
        "operationId": "SessionController_subscribeToPresence",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SubscribePresenceDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Subscribed; updates now arrive as presence.update events"
          },
          "400": {
            "description": "Session not started"
          },
          "404": {
            "description": "Session not found"
          },
          "501": {
            "description": "The active engine cannot observe presence (whatsapp-web.js)"
          }
        },
        "summary": "Subscribe to a chat's presence",
        "tags": [
          "sessions"
        ]
      }
    },
    "/api/sessions/{id}/presence/{chatId}": {
      "get": {
        "description": "Serves the most recent report received since the chat was subscribed. Returns `null` when nothing has been reported — either the chat was never subscribed, or nothing has changed since. That is a normal state, not a missing resource, so it is `200` with a null body rather than a `404`.\n\nHeld in memory and never persisted: presence is short-lived, and answering \"typing\" from before a restart would be worse than answering nothing.",
        "operationId": "SessionController_getPresence",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "chatId",
            "required": true,
            "in": "path",
            "description": "Chat ID as subscribed",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Last reported presence, or null",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChatPresenceResponseDto"
                }
              }
            }
          },
          "404": {
            "description": "Session not found"
          }
        },
        "summary": "Read a chat's last reported presence",
        "tags": [
          "sessions"
        ]
      }
    },
    "/api/sessions/{id}/chats/unread": {
      "post": {
        "operationId": "SessionController_markChatUnread",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MarkChatReadDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Chat marked as unread successfully"
          },
          "400": {
            "description": "Session not ready"
          },
          "404": {
            "description": "Session not found"
          }
        },
        "summary": "Mark a chat as unread",
        "tags": [
          "sessions"
        ]
      }
    },
    "/api/sessions/{id}/chats/{chatId}/messages": {
      "delete": {
        "operationId": "SessionController_clearChatMessages",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "chatId",
            "required": true,
            "in": "path",
            "description": "Chat JID, e.g. 1234567890-123@g.us (URL-encode the '@')",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Returns `{ success }`. `false` means the engine declined to act — an unknown chat, or on the Baileys engine a chat with no known history, since the change is keyed to its last message."
          },
          "400": {
            "description": "Session not ready"
          },
          "404": {
            "description": "Session not found"
          }
        },
        "summary": "Delete every message in a chat, keeping the chat itself",
        "tags": [
          "sessions"
        ]
      }
    },
    "/api/sessions/{id}/chats/archive": {
      "post": {
        "operationId": "SessionController_archiveChat",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ArchiveChatDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Returns `{ success }`. `false` means the engine declined to act — on the Baileys engine a chat with no known history cannot be archived, since the change is keyed to its last message."
          },
          "400": {
            "description": "Session not ready"
          },
          "404": {
            "description": "Session not found"
          }
        },
        "summary": "Archive or unarchive a chat",
        "tags": [
          "sessions"
        ]
      }
    },
    "/api/sessions/{id}/chats/delete": {
      "post": {
        "operationId": "SessionController_deleteChat",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DeleteChatDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Chat deleted successfully"
          },
          "400": {
            "description": "Session not ready"
          },
          "404": {
            "description": "Session not found"
          }
        },
        "summary": "Delete a chat from the chat list (e.g. a group you have left)",
        "tags": [
          "sessions"
        ]
      }
    },
    "/api/sessions/{id}/chats/typing": {
      "post": {
        "operationId": "SessionController_sendChatState",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SendChatStateDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Presence sent"
          },
          "404": {
            "description": "Session not found"
          }
        },
        "summary": "Send a typing/recording presence indicator to a chat (or clear it with 'paused')",
        "tags": [
          "sessions"
        ]
      }
    },
    "/api/sessions/stats/overview": {
      "get": {
        "operationId": "SessionController_getStats",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Session statistics including counts and memory usage"
          }
        },
        "summary": "Get session statistics for multi-session monitoring",
        "tags": [
          "sessions"
        ]
      }
    },
    "/api/sessions/{sessionId}/webhooks": {
      "post": {
        "operationId": "WebhookController_create",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateWebhookDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Webhook created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookResponseDto"
                }
              }
            }
          }
        },
        "summary": "Create a webhook for the session",
        "tags": [
          "webhooks"
        ]
      },
      "get": {
        "operationId": "WebhookController_findBySession",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of webhooks",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/WebhookResponseDto"
                  }
                }
              }
            }
          }
        },
        "summary": "List all webhooks for a session",
        "tags": [
          "webhooks"
        ]
      }
    },
    "/api/sessions/{sessionId}/webhooks/{id}": {
      "get": {
        "operationId": "WebhookController_findOne",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "Webhook ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Webhook details",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookResponseDto"
                }
              }
            }
          },
          "404": {
            "description": "Webhook not found"
          }
        },
        "summary": "Get a webhook by ID",
        "tags": [
          "webhooks"
        ]
      },
      "put": {
        "operationId": "WebhookController_update",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "Webhook ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateWebhookDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Webhook updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookResponseDto"
                }
              }
            }
          },
          "404": {
            "description": "Webhook not found"
          }
        },
        "summary": "Update a webhook",
        "tags": [
          "webhooks"
        ]
      },
      "delete": {
        "operationId": "WebhookController_delete",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "Webhook ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Webhook deleted"
          },
          "404": {
            "description": "Webhook not found"
          }
        },
        "summary": "Delete a webhook",
        "tags": [
          "webhooks"
        ]
      }
    },
    "/api/sessions/{sessionId}/webhooks/{id}/test": {
      "post": {
        "operationId": "WebhookController_test",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "Webhook ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Test result"
          },
          "404": {
            "description": "Webhook not found"
          }
        },
        "summary": "Test a webhook by sending a test payload",
        "tags": [
          "webhooks"
        ]
      }
    },
    "/api/webhooks/delivery-failures": {
      "get": {
        "operationId": "WebhooksListController_deliveryFailures",
        "parameters": [
          {
            "name": "sessionId",
            "required": false,
            "in": "query",
            "description": "Filter to a single session",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "required": false,
            "in": "query",
            "description": "Max records to return (1-1000, default 1000)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "offset",
            "required": false,
            "in": "query",
            "description": "Number of records to skip (for paging)",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Permanently-failed webhook deliveries, most recent first"
          }
        },
        "summary": "List recently-failed webhook deliveries (all retries exhausted)",
        "tags": [
          "webhooks"
        ]
      }
    },
    "/api/webhooks": {
      "get": {
        "operationId": "WebhooksListController_findAll",
        "parameters": [
          {
            "name": "limit",
            "required": false,
            "in": "query",
            "description": "Max webhooks to return (1-1000, default 1000)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "offset",
            "required": false,
            "in": "query",
            "description": "Number of webhooks to skip (for paging)",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of webhooks",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/WebhookResponseDto"
                  }
                }
              }
            }
          }
        },
        "summary": "List webhooks visible to the calling key (scoped to its allowed sessions)",
        "tags": [
          "webhooks"
        ]
      }
    },
    "/api/sessions/{sessionId}/automation-rules": {
      "post": {
        "operationId": "AutomationRuleController_create",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateAutomationRuleDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Rule created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AutomationRuleResponseDto"
                }
              }
            }
          },
          "400": {
            "description": "Invalid rule (bad conditions, over-limit text)."
          }
        },
        "summary": "Create an autoreply rule",
        "tags": [
          "automation"
        ]
      },
      "get": {
        "operationId": "AutomationRuleController_findAll",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Rules in evaluation order: creation time, id as the same-second tiebreak."
          }
        },
        "summary": "List the session’s autoreply rules",
        "tags": [
          "automation"
        ]
      }
    },
    "/api/sessions/{sessionId}/automation-rules/{ruleId}": {
      "get": {
        "operationId": "AutomationRuleController_findOne",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "ruleId",
            "required": true,
            "in": "path",
            "description": "Rule ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The rule.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AutomationRuleResponseDto"
                }
              }
            }
          },
          "404": {
            "description": "No such rule in this session."
          }
        },
        "summary": "Get one autoreply rule",
        "tags": [
          "automation"
        ]
      },
      "put": {
        "operationId": "AutomationRuleController_update",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "ruleId",
            "required": true,
            "in": "path",
            "description": "Rule ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateAutomationRuleDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated rule.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AutomationRuleResponseDto"
                }
              }
            }
          },
          "404": {
            "description": "No such rule in this session."
          }
        },
        "summary": "Update an autoreply rule",
        "tags": [
          "automation"
        ]
      },
      "delete": {
        "operationId": "AutomationRuleController_remove",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "ruleId",
            "required": true,
            "in": "path",
            "description": "Rule ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Rule deleted."
          },
          "404": {
            "description": "No such rule in this session."
          }
        },
        "summary": "Delete an autoreply rule",
        "tags": [
          "automation"
        ]
      }
    },
    "/api/sessions/{sessionId}/messages": {
      "get": {
        "operationId": "MessageController_getMessages",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "chatId",
            "required": false,
            "in": "query",
            "description": "Filter by chat ID",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "from",
            "required": false,
            "in": "query",
            "description": "Filter by sender. A phone also matches group messages via the author field and any lid that resolves to it.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "required": false,
            "in": "query",
            "description": "Max messages to return (default 50)",
            "schema": {
              "type": "number"
            }
          },
          {
            "name": "offset",
            "required": false,
            "in": "query",
            "description": "Offset for pagination",
            "schema": {
              "type": "number"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Message history"
          }
        },
        "summary": "Get message history for a session",
        "tags": [
          "messages"
        ]
      }
    },
    "/api/sessions/{sessionId}/messages/send-text": {
      "post": {
        "operationId": "MessageController_sendText",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SendTextMessageDto"
              },
              "examples": {
                "minimal": {
                  "summary": "Plain text message",
                  "value": {
                    "chatId": "628123456789@c.us",
                    "text": "Hello from OpenWA!"
                  }
                },
                "withMentions": {
                  "summary": "Group message with an @mention (the text must carry the @<number> token)",
                  "value": {
                    "chatId": "120363000000000000@g.us",
                    "text": "Hello @62811",
                    "mentions": [
                      "62811@c.us"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Message sent",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MessageResponseDto"
                }
              }
            }
          },
          "400": {
            "description": "Session not active or invalid request"
          },
          "404": {
            "description": "Session not found"
          }
        },
        "summary": "Send a text message",
        "tags": [
          "messages"
        ]
      }
    },
    "/api/sessions/{sessionId}/messages/send-template": {
      "post": {
        "operationId": "MessageController_sendTemplate",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SendTemplateMessageDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Template rendered and sent",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MessageResponseDto"
                }
              }
            }
          },
          "400": {
            "description": "Session not active or invalid request"
          },
          "404": {
            "description": "Session or template not found"
          }
        },
        "summary": "Render a stored text template and send it as a text message",
        "tags": [
          "messages"
        ]
      }
    },
    "/api/sessions/{sessionId}/messages/send-image": {
      "post": {
        "operationId": "MessageController_sendImage",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SendMediaMessageDto"
              },
              "examples": {
                "fromUrl": {
                  "summary": "Fetch the media from a URL",
                  "value": {
                    "chatId": "628123456789@c.us",
                    "url": "https://example.com/image.jpg",
                    "caption": "Check out this image!"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Image sent",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MessageResponseDto"
                }
              }
            }
          },
          "400": {
            "description": "Session not active or invalid request"
          }
        },
        "summary": "Send an image message",
        "tags": [
          "messages"
        ]
      }
    },
    "/api/sessions/{sessionId}/messages/send-video": {
      "post": {
        "operationId": "MessageController_sendVideo",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SendMediaMessageDto"
              },
              "examples": {
                "fromUrl": {
                  "summary": "Fetch the media from a URL",
                  "value": {
                    "chatId": "628123456789@c.us",
                    "url": "https://example.com/video.mp4"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Video sent",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MessageResponseDto"
                }
              }
            }
          },
          "400": {
            "description": "Session not active or invalid request"
          }
        },
        "summary": "Send a video message",
        "tags": [
          "messages"
        ]
      }
    },
    "/api/sessions/{sessionId}/messages/send-audio": {
      "post": {
        "operationId": "MessageController_sendAudio",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SendAudioMessageDto"
              },
              "examples": {
                "fromUrl": {
                  "summary": "Fetch the media from a URL",
                  "value": {
                    "chatId": "628123456789@c.us",
                    "url": "https://example.com/audio.ogg",
                    "ptt": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Audio sent",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MessageResponseDto"
                }
              }
            }
          },
          "400": {
            "description": "Session not active or invalid request"
          }
        },
        "summary": "Send an audio/voice message",
        "tags": [
          "messages"
        ]
      }
    },
    "/api/sessions/{sessionId}/messages/send-document": {
      "post": {
        "operationId": "MessageController_sendDocument",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SendMediaMessageDto"
              },
              "examples": {
                "fromUrl": {
                  "summary": "Fetch the media from a URL",
                  "value": {
                    "chatId": "628123456789@c.us",
                    "url": "https://example.com/report.pdf",
                    "filename": "report.pdf"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Document sent",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MessageResponseDto"
                }
              }
            }
          },
          "400": {
            "description": "Session not active or invalid request"
          }
        },
        "summary": "Send a document/file",
        "tags": [
          "messages"
        ]
      }
    },
    "/api/sessions/{sessionId}/messages/send-location": {
      "post": {
        "operationId": "MessageController_sendLocation",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SendLocationDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Location sent",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MessageResponseDto"
                }
              }
            }
          }
        },
        "summary": "Send a location message",
        "tags": [
          "messages"
        ]
      }
    },
    "/api/sessions/{sessionId}/messages/send-contact": {
      "post": {
        "operationId": "MessageController_sendContact",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SendContactDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Contact sent",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MessageResponseDto"
                }
              }
            }
          }
        },
        "summary": "Send a contact card message",
        "tags": [
          "messages"
        ]
      }
    },
    "/api/sessions/{sessionId}/messages/send-sticker": {
      "post": {
        "operationId": "MessageController_sendSticker",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SendMediaMessageDto"
              },
              "examples": {
                "fromUrl": {
                  "summary": "Fetch the media from a URL",
                  "value": {
                    "chatId": "628123456789@c.us",
                    "url": "https://example.com/sticker.png"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Sticker sent",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MessageResponseDto"
                }
              }
            }
          }
        },
        "summary": "Send a sticker message",
        "tags": [
          "messages"
        ]
      }
    },
    "/api/sessions/{sessionId}/messages/send-poll": {
      "post": {
        "operationId": "MessageController_sendPoll",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SendPollDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Poll sent",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MessageResponseDto"
                }
              }
            }
          }
        },
        "summary": "Send a native WhatsApp poll",
        "tags": [
          "messages"
        ]
      }
    },
    "/api/sessions/{sessionId}/messages/reply": {
      "post": {
        "operationId": "MessageController_reply",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ReplyMessageDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Reply sent",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MessageResponseDto"
                }
              }
            }
          }
        },
        "summary": "Reply to a message",
        "tags": [
          "messages"
        ]
      }
    },
    "/api/sessions/{sessionId}/messages/forward": {
      "post": {
        "operationId": "MessageController_forward",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ForwardMessageDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Message forwarded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MessageResponseDto"
                }
              }
            }
          }
        },
        "summary": "Forward a message to another chat",
        "tags": [
          "messages"
        ]
      }
    },
    "/api/sessions/{sessionId}/messages/react": {
      "post": {
        "operationId": "MessageController_react",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ReactMessageDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Reaction added or removed. Send empty emoji to remove reaction."
          },
          "400": {
            "description": "Session not active or message not found"
          }
        },
        "summary": "Add or remove a reaction to a message",
        "tags": [
          "messages"
        ]
      }
    },
    "/api/sessions/{sessionId}/messages/{chatId}/history": {
      "get": {
        "description": "Reads messages directly from the WhatsApp client for the given chat, bypassing the local DB. Useful for retrieving messages that arrived before the gateway was started.",
        "operationId": "MessageController_getChatHistory",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "chatId",
            "required": true,
            "in": "path",
            "description": "Chat ID (e.g. 1234567890@c.us or groupId@g.us)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "required": false,
            "in": "query",
            "description": "Max messages to return (default 50)",
            "schema": {
              "type": "number"
            }
          },
          {
            "name": "includeMedia",
            "required": false,
            "in": "query",
            "description": "When true, downloads media (base64) for messages that have it. Slower; default false.",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "deep",
            "required": false,
            "in": "query",
            "description": "When true, raises the limit ceiling from 100 to 2000 for reaching further back in history (whatsapp-web.js only; loads earlier messages on demand). Forces metadata-only (includeMedia is ignored). Large/slow requests may increase WhatsApp rate-limiting risk; default false.",
            "schema": {
              "type": "boolean"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Chat history (most recent messages)"
          }
        },
        "summary": "Fetch chat history live from WhatsApp",
        "tags": [
          "messages"
        ]
      }
    },
    "/api/sessions/{sessionId}/messages/{chatId}/{messageId}/reactions": {
      "get": {
        "operationId": "MessageController_getReactions",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "chatId",
            "required": true,
            "in": "path",
            "description": "Chat ID containing the message",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "messageId",
            "required": true,
            "in": "path",
            "description": "Message ID to get reactions for",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of reactions with senders"
          }
        },
        "summary": "Get reactions for a specific message",
        "tags": [
          "messages"
        ]
      }
    },
    "/api/sessions/{sessionId}/messages/{chatId}/{messageId}/media": {
      "get": {
        "operationId": "MessageController_getChatMedia",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "chatId",
            "required": true,
            "in": "path",
            "description": "Chat ID containing the message",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "messageId",
            "required": true,
            "in": "path",
            "description": "WhatsApp message ID whose media to download",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The archived media bytes, served as an attachment."
          },
          "404": {
            "description": "Nothing archived for this message — archiving was off when it arrived, the message carries no media, the media was above the archive cap, retention has since cleared it, or the message was sent BY this account (only inbound media is archived)."
          }
        },
        "summary": "Download a message’s archived media",
        "tags": [
          "messages"
        ]
      }
    },
    "/api/sessions/{sessionId}/messages/delete": {
      "post": {
        "operationId": "MessageController_deleteMessage",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DeleteMessageDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Message deleted"
          },
          "400": {
            "description": "Session not active or message not found"
          }
        },
        "summary": "Delete a message",
        "tags": [
          "messages"
        ]
      }
    },
    "/api/sessions/{sessionId}/messages/vote-poll": {
      "post": {
        "operationId": "MessageController_votePoll",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/VotePollDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Vote cast"
          },
          "400": {
            "description": "Session not active, or the target message is not a poll"
          },
          "404": {
            "description": "Poll not found in the chat’s recent history"
          },
          "501": {
            "description": "Not supported on the Baileys engine"
          }
        },
        "summary": "Cast a vote on a poll",
        "tags": [
          "messages"
        ]
      }
    },
    "/api/sessions/{sessionId}/messages/pin": {
      "post": {
        "operationId": "MessageController_pinMessage",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PinMessageDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Message pinned"
          },
          "400": {
            "description": "Session not active, or durationSeconds is not one of 86400 / 604800 / 2592000"
          },
          "403": {
            "description": "The engine refused the pin — in a group only admins may pin"
          },
          "404": {
            "description": "Message not found in the chat"
          }
        },
        "summary": "Pin a message in its chat",
        "tags": [
          "messages"
        ]
      }
    },
    "/api/sessions/{sessionId}/messages/unpin": {
      "post": {
        "operationId": "MessageController_unpinMessage",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UnpinMessageDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Message unpinned"
          },
          "400": {
            "description": "Session not active"
          },
          "403": {
            "description": "The engine refused the unpin — in a group only admins may unpin"
          },
          "404": {
            "description": "Message not found in the chat"
          }
        },
        "summary": "Remove a message’s pin",
        "tags": [
          "messages"
        ]
      }
    },
    "/api/sessions/{sessionId}/messages/star": {
      "post": {
        "operationId": "MessageController_starMessage",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/StarMessageDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Instruction delivered. On whatsapp-web.js the engine silently ignores a message it will not star, so this does not guarantee the star is set."
          },
          "400": {
            "description": "Session not active"
          },
          "404": {
            "description": "Message not found in the chat"
          }
        },
        "summary": "Star or unstar a message",
        "tags": [
          "messages"
        ]
      }
    },
    "/api/sessions/{sessionId}/messages/edit": {
      "post": {
        "operationId": "MessageController_edit",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EditMessageDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Message edited",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MessageResponseDto"
                }
              }
            }
          },
          "400": {
            "description": "Session not active, invalid request, or the send was blocked by a plugin"
          },
          "403": {
            "description": "The message was not sent by this account, or the engine refused the edit"
          },
          "404": {
            "description": "Message not found"
          }
        },
        "summary": "Edit the text of a message sent by this account",
        "tags": [
          "messages"
        ]
      }
    },
    "/api/sessions/{sessionId}/messages/send-bulk": {
      "post": {
        "operationId": "MessageController_sendBulk",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SendBulkMessageDto"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Batch created and processing started",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BulkMessageResponseDto"
                }
              }
            }
          },
          "400": {
            "description": "Session not active or invalid request"
          }
        },
        "summary": "Send messages to multiple recipients (async batch processing)",
        "tags": [
          "messages"
        ]
      }
    },
    "/api/sessions/{sessionId}/messages/batch/{batchId}": {
      "get": {
        "operationId": "MessageController_getBatchStatus",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "batchId",
            "required": true,
            "in": "path",
            "description": "Batch ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Batch status and progress"
          },
          "404": {
            "description": "Batch not found"
          }
        },
        "summary": "Get batch processing status",
        "tags": [
          "messages"
        ]
      }
    },
    "/api/sessions/{sessionId}/messages/batch/{batchId}/cancel": {
      "post": {
        "operationId": "MessageController_cancelBatch",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "batchId",
            "required": true,
            "in": "path",
            "description": "Batch ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Batch cancelled"
          },
          "400": {
            "description": "Batch already completed, cancelled, or failed (terminal statuses are exclusive)"
          },
          "404": {
            "description": "Batch not found"
          }
        },
        "summary": "Cancel a running batch",
        "tags": [
          "messages"
        ]
      }
    },
    "/api/sessions/{sessionId}/templates": {
      "post": {
        "operationId": "TemplateController_create",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateTemplateDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Template created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TemplateResponseDto"
                }
              }
            }
          }
        },
        "summary": "Create a message template for the session",
        "tags": [
          "templates"
        ]
      },
      "get": {
        "operationId": "TemplateController_findBySession",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of templates",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/TemplateResponseDto"
                  }
                }
              }
            }
          }
        },
        "summary": "List all templates for a session",
        "tags": [
          "templates"
        ]
      }
    },
    "/api/sessions/{sessionId}/templates/{id}": {
      "get": {
        "operationId": "TemplateController_findOne",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "Template ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Template details",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TemplateResponseDto"
                }
              }
            }
          },
          "404": {
            "description": "Template not found"
          }
        },
        "summary": "Get a template by ID",
        "tags": [
          "templates"
        ]
      },
      "put": {
        "operationId": "TemplateController_update",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "Template ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateTemplateDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Template updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TemplateResponseDto"
                }
              }
            }
          },
          "404": {
            "description": "Template not found"
          }
        },
        "summary": "Update a template",
        "tags": [
          "templates"
        ]
      },
      "delete": {
        "operationId": "TemplateController_delete",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "Template ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Template deleted"
          },
          "404": {
            "description": "Template not found"
          }
        },
        "summary": "Delete a template",
        "tags": [
          "templates"
        ]
      }
    },
    "/api/health": {
      "get": {
        "operationId": "HealthController_check",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Application is healthy"
          }
        },
        "summary": "Basic health check",
        "tags": [
          "health"
        ],
        "security": []
      }
    },
    "/api/health/live": {
      "get": {
        "operationId": "HealthController_liveness",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Application is alive"
          }
        },
        "summary": "Liveness probe for Kubernetes",
        "tags": [
          "health"
        ],
        "security": []
      }
    },
    "/api/health/ready": {
      "get": {
        "operationId": "HealthController_readiness",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Application is ready to accept traffic"
          },
          "503": {
            "description": "A required dependency is down"
          }
        },
        "summary": "Readiness probe — verifies the auth/audit + data databases respond",
        "tags": [
          "health"
        ],
        "security": []
      }
    },
    "/api/settings": {
      "get": {
        "operationId": "SettingsController_get",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Current settings"
          }
        },
        "summary": "Get application settings",
        "tags": [
          "settings"
        ]
      },
      "put": {
        "operationId": "SettingsController_update",
        "parameters": [],
        "responses": {
          "501": {
            "description": "Settings are derived from environment configuration and cannot be changed at runtime"
          }
        },
        "summary": "Settings are read-only at runtime (environment-derived)",
        "tags": [
          "settings"
        ]
      }
    },
    "/api/infra/status": {
      "get": {
        "operationId": "InfraStatusController_getStatus",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Infrastructure status"
          }
        },
        "summary": "Get infrastructure status",
        "tags": [
          "infrastructure"
        ]
      }
    },
    "/api/infra/engines": {
      "get": {
        "operationId": "InfraStatusController_getEngines",
        "parameters": [],
        "responses": {
          "200": {
            "description": "List of available engines"
          }
        },
        "summary": "Get available WhatsApp engines",
        "tags": [
          "infrastructure"
        ]
      }
    },
    "/api/infra/engines/current": {
      "get": {
        "operationId": "InfraStatusController_getCurrentEngine",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Current engine info"
          }
        },
        "summary": "Get current active engine",
        "tags": [
          "infrastructure"
        ]
      }
    },
    "/api/infra/health": {
      "get": {
        "operationId": "InfraStatusController_healthCheck",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Server is healthy"
          }
        },
        "summary": "Health check endpoint",
        "tags": [
          "infrastructure"
        ],
        "security": []
      }
    },
    "/api/infra/config": {
      "get": {
        "operationId": "InfraConfigController_getConfig",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Saved configuration (secrets omitted)"
          }
        },
        "summary": "Read the saved infrastructure configuration for the dashboard form",
        "tags": [
          "infrastructure"
        ]
      },
      "put": {
        "operationId": "InfraConfigController_saveConfig",
        "parameters": [],
        "requestBody": {
          "required": true,
          "description": "Configuration to save",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SaveConfigDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Configuration saved"
          }
        },
        "summary": "Save infrastructure configuration to .env file",
        "tags": [
          "infrastructure"
        ]
      }
    },
    "/api/infra/restart": {
      "post": {
        "operationId": "InfraConfigController_requestRestart",
        "parameters": [],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RestartDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Server will restart with new profiles"
          }
        },
        "summary": "Request server restart with Docker orchestration",
        "tags": [
          "infrastructure"
        ]
      }
    },
    "/api/infra/export-data": {
      "get": {
        "operationId": "InfraDataController_exportData",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Exported data as JSON"
          }
        },
        "summary": "Export all data from Data DB for migration",
        "tags": [
          "infrastructure"
        ]
      }
    },
    "/api/infra/import-data": {
      "post": {
        "operationId": "InfraDataController_importData",
        "parameters": [],
        "requestBody": {
          "required": true,
          "description": "Exported data from export-data endpoint",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "force": {
                    "type": "boolean",
                    "description": "Allow the replace to proceed even while engines are running for sessions the backup does not contain (they keep running until restart; see restartRequired). Prefer stopOrphans, which closes that window inside this request instead."
                  },
                  "stopOrphans": {
                    "type": "boolean",
                    "description": "Stop the running engines for sessions the backup does not contain, inside this request and before the replace runs. Supersedes force for the orphan case: with stopOrphans the engines no longer need a process restart to reconcile, so restartRequired stays false on the success path."
                  },
                  "tables": {
                    "type": "object",
                    "description": "Every one of the 14 migration tables is emptied before the restore runs, so a key omitted here is restored EMPTY rather than left untouched. Post the whole GET /api/infra/export-data payload, not a hand-built subset.",
                    "properties": {
                      "sessions": {
                        "type": "array"
                      },
                      "webhooks": {
                        "type": "array"
                      },
                      "messages": {
                        "type": "array"
                      },
                      "messageBatches": {
                        "type": "array"
                      },
                      "templates": {
                        "type": "array"
                      },
                      "baileysStoredMessages": {
                        "type": "array"
                      },
                      "lidMappings": {
                        "type": "array"
                      },
                      "pluginInstances": {
                        "type": "array"
                      },
                      "conversationMappings": {
                        "type": "array"
                      },
                      "ingressEvents": {
                        "type": "array"
                      },
                      "webhookDeliveryFailures": {
                        "type": "array"
                      },
                      "integrationDeliveryFailures": {
                        "type": "array"
                      },
                      "statusUpdates": {
                        "type": "array"
                      },
                      "automationRules": {
                        "type": "array"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Data imported successfully"
          },
          "409": {
            "description": "Refused, with the reason in `code`. IMPORT_ALREADY_RUNNING: another import is running — wait for it. IMPORT_NESTED_TRANSACTION: another database transaction holds this connection, so a restore could not be made durable — retry with nothing else in flight. IMPORT_WOULD_ORPHAN_ENGINES: live engines exist for sessions the backup would remove — retry with stopOrphans=true to stop them in-request, or force=true to proceed and restart after. Only the last of these is retryable with stopOrphans; the others leave nothing to decide"
          }
        },
        "summary": "Import data to Data DB (replaces existing data)",
        "tags": [
          "infrastructure"
        ]
      }
    },
    "/api/infra/storage/files/count": {
      "get": {
        "operationId": "InfraStorageController_getStorageFileCount",
        "parameters": [],
        "responses": {
          "200": {
            "description": "File count and size"
          }
        },
        "summary": "Get file count in current storage",
        "tags": [
          "infrastructure"
        ]
      }
    },
    "/api/infra/storage/export": {
      "get": {
        "operationId": "InfraStorageController_exportStorage",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Tar.gz archive stream"
          }
        },
        "summary": "Export all storage files as tar.gz",
        "tags": [
          "infrastructure"
        ]
      }
    },
    "/api/infra/storage/import": {
      "post": {
        "operationId": "InfraStorageController_importStorage",
        "parameters": [],
        "requestBody": {
          "required": true,
          "description": "Path to tar.gz file to import",
          "content": {
            "application/json": {
              "schema": {
                "type": "string"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Import result"
          }
        },
        "summary": "Import storage files from tar.gz",
        "tags": [
          "infrastructure"
        ]
      }
    },
    "/api/sessions/{sessionId}/contacts": {
      "get": {
        "operationId": "ContactController_findAll",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "required": false,
            "in": "query",
            "description": "Max contacts to return (1–1000, default 1000)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "offset",
            "required": false,
            "in": "query",
            "description": "Number of contacts to skip (for paging)",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of contacts"
          },
          "400": {
            "description": "Session not ready"
          },
          "404": {
            "description": "Session not found"
          }
        },
        "summary": "Get all contacts for a session",
        "tags": [
          "contacts"
        ]
      }
    },
    "/api/sessions/{sessionId}/contacts/profile-pictures": {
      "get": {
        "description": "One request for a whole chat sidebar — avoids the burst of parallel single fetches that would exhaust the per-IP throttle. Engine lookups run 3 at a time; per-id failures return null.",
        "operationId": "ContactController_getProfilePictures",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "ids",
            "required": true,
            "in": "query",
            "description": "Comma-separated contact ids (max 50 used)",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          }
        },
        "summary": "Batch-resolve profile picture URLs for up to 50 contacts",
        "tags": [
          "contacts"
        ]
      }
    },
    "/api/sessions/{sessionId}/contacts/{contactId}": {
      "get": {
        "operationId": "ContactController_findOne",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "contactId",
            "required": true,
            "in": "path",
            "description": "Contact ID (e.g., 628xxx@c.us)",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Contact details"
          },
          "404": {
            "description": "Contact not found"
          }
        },
        "summary": "Get a specific contact by ID",
        "tags": [
          "contacts"
        ]
      },
      "put": {
        "operationId": "ContactController_upsertContact",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "contactId",
            "required": true,
            "in": "path",
            "description": "Contact ID (e.g., 628xxx@c.us)",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpsertContactDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Contact saved"
          },
          "400": {
            "description": "Session not active or invalid request"
          }
        },
        "summary": "Save a contact to the account's addressbook, or edit an existing entry",
        "tags": [
          "contacts"
        ]
      },
      "delete": {
        "operationId": "ContactController_deleteContact",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "contactId",
            "required": true,
            "in": "path",
            "description": "Contact ID (e.g., 628xxx@c.us)",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Contact deleted"
          },
          "400": {
            "description": "Session not active"
          }
        },
        "summary": "Remove a contact from the account's addressbook",
        "tags": [
          "contacts"
        ]
      }
    },
    "/api/sessions/{sessionId}/contacts/check/{number}": {
      "get": {
        "description": "Returns whether the number is a registered WhatsApp account and its canonical id. Use this to pre-validate a recipient before sending: the send endpoints return 201 on accepting a message even for numbers that are not on WhatsApp, so this is the only way to confirm a new number is reachable before you send to it.",
        "operationId": "ContactController_checkNumber",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "number",
            "required": true,
            "in": "path",
            "description": "Phone number to check (e.g., 628123456789)",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Number existence check result"
          }
        },
        "summary": "Check if a phone number exists on WhatsApp",
        "tags": [
          "contacts"
        ]
      }
    },
    "/api/sessions/{sessionId}/contacts/{contactId}/profile-picture": {
      "get": {
        "operationId": "ContactController_getProfilePicture",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "contactId",
            "required": true,
            "in": "path",
            "description": "Contact ID (e.g., 628xxx@c.us)",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Profile picture URL"
          }
        },
        "summary": "Get profile picture URL for a contact",
        "tags": [
          "contacts"
        ]
      }
    },
    "/api/sessions/{sessionId}/contacts/{contactId}/phone": {
      "get": {
        "operationId": "ContactController_resolvePhone",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "contactId",
            "required": true,
            "in": "path",
            "description": "Contact ID / JID to resolve (e.g., an @lid)",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Resolved phone number (MSISDN digits), or null when the engine cannot map it"
          }
        },
        "summary": "Resolve a contact id (e.g. an @lid) to a phone number — best-effort",
        "tags": [
          "contacts"
        ]
      }
    },
    "/api/sessions/{sessionId}/contacts/{contactId}/block": {
      "post": {
        "operationId": "ContactController_blockContact",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "contactId",
            "required": true,
            "in": "path",
            "description": "Contact ID (e.g., 628xxx@c.us)",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Contact blocked"
          }
        },
        "summary": "Block a contact",
        "tags": [
          "contacts"
        ]
      },
      "delete": {
        "operationId": "ContactController_unblockContact",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "contactId",
            "required": true,
            "in": "path",
            "description": "Contact ID (e.g., 628xxx@c.us)",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Contact unblocked"
          }
        },
        "summary": "Unblock a contact",
        "tags": [
          "contacts"
        ]
      }
    },
    "/api/sessions/{sessionId}/groups/join-info": {
      "get": {
        "description": "Read-only: nothing about the account changes, which is what makes it safe to call on a code from an untrusted source. Supported on both engines.\n\nThere is no participant LIST — the account is not a member — only a count, and only when WhatsApp discloses one. Fields the engine does not report are omitted rather than defaulted, because whatsapp-web.js returns an untyped object with no guaranteed shape.",
        "operationId": "GroupController_joinInfo",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "code",
            "required": true,
            "in": "query",
            "description": "Group invite code (the part after the invite link)",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "What the invite discloses about the group"
          },
          "400": {
            "description": "Session not started, or no code supplied"
          },
          "404": {
            "description": "No such invite — invalid, expired or revoked"
          }
        },
        "summary": "Preview a group from its invite code, without joining",
        "tags": [
          "groups"
        ]
      }
    },
    "/api/sessions/{sessionId}/groups/{groupId}": {
      "get": {
        "operationId": "GroupController_findOne",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "groupId",
            "required": true,
            "in": "path",
            "description": "Group ID (e.g., 120363xxx@g.us)",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Group details with participants"
          },
          "404": {
            "description": "Group not found"
          }
        },
        "summary": "Get detailed group info",
        "tags": [
          "groups"
        ]
      }
    },
    "/api/sessions/{sessionId}/groups/join": {
      "post": {
        "operationId": "GroupController_join",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/JoinGroupDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Joined the group"
          },
          "400": {
            "description": "Invalid or expired invite code, or session is not started"
          }
        },
        "summary": "Join a group via invite code",
        "tags": [
          "groups"
        ]
      }
    },
    "/api/sessions/{sessionId}/groups/{groupId}/settings": {
      "get": {
        "operationId": "GroupController_getSettings",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "groupId",
            "required": true,
            "in": "path",
            "description": "Group ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Group settings"
          },
          "404": {
            "description": "Group not found"
          }
        },
        "summary": "Get group settings (announce / locked / ephemeral timer)",
        "tags": [
          "groups"
        ]
      },
      "put": {
        "operationId": "GroupController_updateSettings",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "groupId",
            "required": true,
            "in": "path",
            "description": "Group ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/GroupSettingsDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Group settings updated"
          },
          "400": {
            "description": "No setting provided, or a value is not a boolean"
          },
          "403": {
            "description": "The engine refused the change (the account is not a group admin)"
          },
          "404": {
            "description": "Group not found"
          },
          "501": {
            "description": "The active engine does not support a requested setting"
          }
        },
        "summary": "Update group settings (announce / locked / ephemeral timer)",
        "tags": [
          "groups"
        ]
      }
    },
    "/api/sessions/{sessionId}/groups": {
      "post": {
        "operationId": "GroupController_create",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateGroupDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Group created"
          }
        },
        "summary": "Create a new group",
        "tags": [
          "groups"
        ]
      }
    },
    "/api/sessions/{sessionId}/groups/{groupId}/participants": {
      "post": {
        "operationId": "GroupController_addParticipants",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "groupId",
            "required": true,
            "in": "path",
            "description": "Group ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ParticipantsDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Participants processed — `results` carries the per-participant outcome (a partial refusal does not fail the batch; a total refusal is an error)"
          }
        },
        "summary": "Add participants to a group",
        "tags": [
          "groups"
        ]
      },
      "delete": {
        "operationId": "GroupController_removeParticipants",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "groupId",
            "required": true,
            "in": "path",
            "description": "Group ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ParticipantsDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Participants processed — `results` carries the per-participant outcome (a partial refusal does not fail the batch; a total refusal is an error)"
          }
        },
        "summary": "Remove participants from a group",
        "tags": [
          "groups"
        ]
      }
    },
    "/api/sessions/{sessionId}/groups/{groupId}/participants/promote": {
      "post": {
        "operationId": "GroupController_promoteParticipants",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "groupId",
            "required": true,
            "in": "path",
            "description": "Group ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ParticipantsDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Participants processed — `results` carries the per-participant outcome (a partial refusal does not fail the batch; a total refusal is an error)"
          }
        },
        "summary": "Promote participants to admin",
        "tags": [
          "groups"
        ]
      }
    },
    "/api/sessions/{sessionId}/groups/{groupId}/participants/demote": {
      "post": {
        "operationId": "GroupController_demoteParticipants",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "groupId",
            "required": true,
            "in": "path",
            "description": "Group ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ParticipantsDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Participants processed — `results` carries the per-participant outcome (a partial refusal does not fail the batch; a total refusal is an error)"
          }
        },
        "summary": "Demote participants from admin",
        "tags": [
          "groups"
        ]
      }
    },
    "/api/sessions/{sessionId}/groups/{groupId}/subject": {
      "put": {
        "operationId": "GroupController_setSubject",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "groupId",
            "required": true,
            "in": "path",
            "description": "Group ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/GroupSubjectDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Subject updated"
          },
          "403": {
            "description": "The engine refused the change — admin rights are required"
          }
        },
        "summary": "Change group name/subject",
        "tags": [
          "groups"
        ]
      }
    },
    "/api/sessions/{sessionId}/groups/{groupId}/description": {
      "put": {
        "operationId": "GroupController_setDescription",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "groupId",
            "required": true,
            "in": "path",
            "description": "Group ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/GroupDescriptionDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Description updated"
          },
          "403": {
            "description": "The engine refused the change — admin rights are required"
          }
        },
        "summary": "Change group description",
        "tags": [
          "groups"
        ]
      }
    },
    "/api/sessions/{sessionId}/groups/{groupId}/leave": {
      "post": {
        "operationId": "GroupController_leave",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "groupId",
            "required": true,
            "in": "path",
            "description": "Group ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Left the group"
          }
        },
        "summary": "Leave a group",
        "tags": [
          "groups"
        ]
      }
    },
    "/api/sessions/{sessionId}/groups/{groupId}/picture": {
      "get": {
        "operationId": "GroupController_getPicture",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "groupId",
            "required": true,
            "in": "path",
            "description": "Group ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Picture URL, or null when the group has none"
          }
        },
        "summary": "Get the group's picture URL",
        "tags": [
          "groups"
        ]
      },
      "put": {
        "operationId": "GroupController_setPicture",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "groupId",
            "required": true,
            "in": "path",
            "description": "Group ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SetGroupPictureDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Group picture updated"
          },
          "400": {
            "description": "Session not active, or neither url nor base64 supplied"
          },
          "403": {
            "description": "The engine refused the change — admin rights required"
          }
        },
        "summary": "Set the group's picture",
        "tags": [
          "groups"
        ]
      },
      "delete": {
        "operationId": "GroupController_deletePicture",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "groupId",
            "required": true,
            "in": "path",
            "description": "Group ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Group picture removed"
          },
          "403": {
            "description": "The engine refused the change — admin rights required"
          }
        },
        "summary": "Remove the group's picture",
        "tags": [
          "groups"
        ]
      }
    },
    "/api/sessions/{sessionId}/groups/{groupId}/invite-code": {
      "get": {
        "operationId": "GroupController_getInviteCode",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "groupId",
            "required": true,
            "in": "path",
            "description": "Group ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Group invite code"
          }
        },
        "summary": "Get group invite code/link",
        "tags": [
          "groups"
        ]
      }
    },
    "/api/sessions/{sessionId}/groups/{groupId}/invite-code/revoke": {
      "post": {
        "operationId": "GroupController_revokeInviteCode",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "groupId",
            "required": true,
            "in": "path",
            "description": "Group ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "New invite code generated"
          }
        },
        "summary": "Revoke group invite code and generate new one",
        "tags": [
          "groups"
        ]
      }
    },
    "/api/sessions/{sessionId}/profile/name": {
      "put": {
        "operationId": "ProfileController_setName",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SetProfileNameDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Profile name updated"
          },
          "400": {
            "description": "Session is not started"
          },
          "403": {
            "description": "The engine refused the name change"
          }
        },
        "summary": "Set the account display name",
        "tags": [
          "profile"
        ]
      }
    },
    "/api/sessions/{sessionId}/profile/status": {
      "put": {
        "operationId": "ProfileController_setStatus",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SetProfileStatusDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Profile status updated"
          },
          "400": {
            "description": "Session is not started"
          }
        },
        "summary": "Set the account about/status text",
        "tags": [
          "profile"
        ]
      }
    },
    "/api/sessions/{sessionId}/profile/picture": {
      "put": {
        "operationId": "ProfileController_setPicture",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SetProfilePictureDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Profile picture updated"
          },
          "400": {
            "description": "Neither url nor base64 provided, base64 without mimetype, or session is not started"
          },
          "403": {
            "description": "The engine refused the picture change"
          },
          "413": {
            "description": "Decoded base64 image exceeds the configured media cap"
          }
        },
        "summary": "Set the account profile picture (URL or base64 image)",
        "tags": [
          "profile"
        ]
      }
    },
    "/api/sessions/{sessionId}/calls/{callId}/reject": {
      "post": {
        "operationId": "CallController_reject",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "callId",
            "required": true,
            "in": "path",
            "description": "Call ID from the call.received event",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Call rejected"
          },
          "400": {
            "description": "Session is not started"
          },
          "404": {
            "description": "Call not found or no longer ringing"
          }
        },
        "summary": "Reject a ringing incoming call",
        "tags": [
          "calls"
        ]
      }
    },
    "/api/sessions/{sessionId}/labels": {
      "get": {
        "operationId": "LabelController_findAll",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of labels"
          },
          "400": {
            "description": "Session not ready or not a business account"
          },
          "404": {
            "description": "Session not found"
          }
        },
        "summary": "Get all labels (WhatsApp Business only)",
        "tags": [
          "labels"
        ]
      }
    },
    "/api/sessions/{sessionId}/labels/{labelId}": {
      "get": {
        "operationId": "LabelController_findOne",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "labelId",
            "required": true,
            "in": "path",
            "description": "Label ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Label details"
          },
          "404": {
            "description": "Label not found"
          }
        },
        "summary": "Get a specific label by ID",
        "tags": [
          "labels"
        ]
      },
      "put": {
        "description": "Baileys only. `PUT` rather than `POST` because the label id is chosen by the caller: WhatsApp carries one `label_edit` write keyed on that id, so whether this creates or updates depends purely on whether the id already exists, and there is no server-assigned id to return.\n\n**Choose an unused id to create.** Reusing one silently rewrites that label rather than failing, because the protocol has no create-only form. Fields left out are left alone.\n\nwhatsapp-web.js can read and assign labels but cannot edit one, and answers `501`.",
        "operationId": "LabelController_upsertLabel",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "labelId",
            "required": true,
            "in": "path",
            "description": "Label ID — caller-chosen",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpsertLabelDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Label created or updated"
          },
          "400": {
            "description": "Session not started, or validation failed"
          },
          "501": {
            "description": "The active engine cannot edit labels (whatsapp-web.js)"
          }
        },
        "summary": "Create or update a label",
        "tags": [
          "labels"
        ]
      },
      "delete": {
        "description": "Baileys only. The label disappears from every chat it was on. whatsapp-web.js cannot edit labels and answers `501`.",
        "operationId": "LabelController_deleteLabel",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "labelId",
            "required": true,
            "in": "path",
            "description": "Label ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Label deleted"
          },
          "400": {
            "description": "Session not started"
          },
          "501": {
            "description": "The active engine cannot edit labels (whatsapp-web.js)"
          }
        },
        "summary": "Delete a label",
        "tags": [
          "labels"
        ]
      }
    },
    "/api/sessions/{sessionId}/labels/{labelId}/chats": {
      "get": {
        "description": "whatsapp-web.js only. Baileys exposes label writes but no label query of any kind, so it answers `501`.",
        "operationId": "LabelController_getChatsByLabel",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "labelId",
            "required": true,
            "in": "path",
            "description": "Label ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Chats carrying the label"
          },
          "400": {
            "description": "Session not started"
          },
          "501": {
            "description": "The active engine cannot list chats by label (Baileys)"
          }
        },
        "summary": "Get every chat carrying a label",
        "tags": [
          "labels"
        ]
      }
    },
    "/api/sessions/{sessionId}/labels/chat/{chatId}": {
      "get": {
        "operationId": "LabelController_getChatLabels",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "chatId",
            "required": true,
            "in": "path",
            "description": "Chat ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of labels for the chat"
          }
        },
        "summary": "Get labels for a specific chat",
        "tags": [
          "labels"
        ]
      },
      "post": {
        "operationId": "LabelController_addLabelToChat",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "chatId",
            "required": true,
            "in": "path",
            "description": "Chat ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "labelId": {
                    "type": "string",
                    "description": "Label ID to add"
                  }
                },
                "required": [
                  "labelId"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Label added to chat"
          },
          "422": {
            "description": "Labels require a WhatsApp Business account, or the chat type has no labels"
          }
        },
        "summary": "Add a label to a chat",
        "tags": [
          "labels"
        ]
      }
    },
    "/api/sessions/{sessionId}/labels/chat/{chatId}/{labelId}": {
      "delete": {
        "operationId": "LabelController_removeLabelFromChat",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "chatId",
            "required": true,
            "in": "path",
            "description": "Chat ID",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "labelId",
            "required": true,
            "in": "path",
            "description": "Label ID to remove",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Label removed from chat"
          },
          "422": {
            "description": "Labels require a WhatsApp Business account, or the chat type has no labels"
          }
        },
        "summary": "Remove a label from a chat",
        "tags": [
          "labels"
        ]
      }
    },
    "/api/sessions/{sessionId}/channels": {
      "get": {
        "operationId": "ChannelController_findAll",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of subscribed channels"
          },
          "400": {
            "description": "Session not ready"
          }
        },
        "summary": "Get all subscribed channels/newsletters",
        "tags": [
          "channels"
        ]
      },
      "post": {
        "description": "The account becomes the channel owner, which is what makes deleting it possible later — neither engine can delete a channel it does not own.",
        "operationId": "ChannelController_create",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateChannelDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The created channel"
          },
          "400": {
            "description": "Session not started, or validation failed"
          },
          "403": {
            "description": "The engine refused — channel creation may be disabled for this account"
          }
        },
        "summary": "Create a channel",
        "tags": [
          "channels"
        ]
      }
    },
    "/api/sessions/{sessionId}/channels/{channelId}": {
      "get": {
        "operationId": "ChannelController_findOne",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "channelId",
            "required": true,
            "in": "path",
            "description": "Channel ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Channel details"
          },
          "404": {
            "description": "Channel not found"
          }
        },
        "summary": "Get a specific channel by ID",
        "tags": [
          "channels"
        ]
      },
      "delete": {
        "operationId": "ChannelController_unsubscribe",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "channelId",
            "required": true,
            "in": "path",
            "description": "Channel ID to unsubscribe from",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successfully unsubscribed from channel"
          }
        },
        "summary": "Unsubscribe from a channel",
        "tags": [
          "channels"
        ]
      }
    },
    "/api/sessions/{sessionId}/channels/{channelId}/messages": {
      "get": {
        "operationId": "ChannelController_getMessages",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "channelId",
            "required": true,
            "in": "path",
            "description": "Channel ID",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "required": false,
            "in": "query",
            "description": "Max messages to return (default 50, max 100)",
            "schema": {
              "type": "number"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of channel messages"
          }
        },
        "summary": "Get messages from a channel",
        "tags": [
          "channels"
        ]
      }
    },
    "/api/sessions/{sessionId}/channels/{channelId}/delete": {
      "post": {
        "description": "Irreversible, and every subscriber loses the channel. Deliberately NOT `DELETE /channels/:channelId` — that route unsubscribes, and the two must not be reachable by the same request with a slip of the wrist. Only the owner can delete.",
        "operationId": "ChannelController_remove",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "channelId",
            "required": true,
            "in": "path",
            "description": "Channel ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Channel deleted"
          },
          "400": {
            "description": "Session not started"
          },
          "403": {
            "description": "The engine refused — not found, or this account does not own it"
          }
        },
        "summary": "Delete a channel this account owns",
        "tags": [
          "channels"
        ]
      }
    },
    "/api/sessions/{sessionId}/channels/{channelId}/mute": {
      "post": {
        "description": "Silences the channel's notifications for this account. The subscription is untouched.",
        "operationId": "ChannelController_mute",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "channelId",
            "required": true,
            "in": "path",
            "description": "Channel ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MuteChannelDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Channel muted or unmuted"
          },
          "400": {
            "description": "Session not started, or validation failed"
          },
          "403": {
            "description": "The engine refused"
          }
        },
        "summary": "Mute or unmute a channel",
        "tags": [
          "channels"
        ]
      }
    },
    "/api/sessions/{sessionId}/channels/subscribe": {
      "post": {
        "operationId": "ChannelController_subscribe",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "inviteCode": {
                    "type": "string",
                    "description": "Channel invite code (from channel link)",
                    "example": "ABC123xyz"
                  }
                },
                "required": [
                  "inviteCode"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Successfully subscribed to channel"
          }
        },
        "summary": "Subscribe to a channel using invite code",
        "tags": [
          "channels"
        ]
      }
    },
    "/api/stats/overview": {
      "get": {
        "operationId": "StatsController_getOverview",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Cross-session aggregate statistics (sessions, messages, etc.)."
          }
        },
        "summary": "Get overall statistics",
        "tags": [
          "statistics"
        ]
      }
    },
    "/api/stats/messages": {
      "get": {
        "operationId": "StatsController_getMessageStats",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Message statistics with a time series for the requested period."
          }
        },
        "summary": "Get message statistics with time series",
        "tags": [
          "statistics"
        ]
      }
    },
    "/api/stats/sessions/{sessionId}": {
      "get": {
        "operationId": "StatsController_getSessionStats",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Per-session statistics for the requested session."
          }
        },
        "summary": "Get statistics for a specific session",
        "tags": [
          "statistics"
        ]
      }
    },
    "/api/metrics": {
      "get": {
        "operationId": "MetricsController_scrape",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Prometheus exposition text"
          },
          "401": {
            "description": "METRICS_TOKEN is configured but the bearer is missing or wrong"
          },
          "404": {
            "description": "Metrics endpoint is disabled (METRICS_TOKEN unset)"
          }
        },
        "security": [
          {
            "metrics-bearer": []
          }
        ],
        "summary": "Prometheus metrics (requires METRICS_TOKEN bearer)",
        "tags": [
          "metrics"
        ]
      }
    },
    "/api/sessions/{sessionId}/status": {
      "get": {
        "operationId": "StatusController_getStatuses",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Status updates visible to the session, grouped by contact."
          }
        },
        "summary": "Get all contact status updates",
        "tags": [
          "status"
        ]
      }
    },
    "/api/sessions/{sessionId}/status/{contactId}": {
      "get": {
        "operationId": "StatusController_getContactStatus",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "contactId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Status updates from the requested contact."
          }
        },
        "summary": "Get status updates from a specific contact",
        "tags": [
          "status"
        ]
      }
    },
    "/api/sessions/{sessionId}/status/{statusId}/media": {
      "get": {
        "operationId": "StatusController_getStatusMedia",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "statusId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The status image/video bytes."
          },
          "404": {
            "description": "No stored media (text status, omitted, or expired)."
          }
        },
        "summary": "Stream a stored status media file",
        "tags": [
          "status"
        ]
      }
    },
    "/api/sessions/{sessionId}/status/send-text": {
      "post": {
        "operationId": "StatusController_sendTextStatus",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SendTextStatusDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Text status posted. The recipients allow-list is honored on Baileys only; whatsapp-web.js broadcasts to the account's status-privacy audience."
          },
          "400": {
            "description": "Invalid request, or the post was blocked by a plugin."
          }
        },
        "summary": "Post a text status",
        "tags": [
          "status"
        ]
      }
    },
    "/api/sessions/{sessionId}/status/send-image": {
      "post": {
        "operationId": "StatusController_sendImageStatus",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SendImageStatusDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Image status posted. The recipients allow-list is honored on Baileys only; whatsapp-web.js broadcasts to the account's status-privacy audience."
          },
          "400": {
            "description": "Neither url nor base64 provided, or the post was blocked by a plugin."
          },
          "413": {
            "description": "Base64 media exceeds MEDIA_DOWNLOAD_MAX_BYTES."
          }
        },
        "summary": "Post an image status",
        "tags": [
          "status"
        ]
      }
    },
    "/api/sessions/{sessionId}/status/send-video": {
      "post": {
        "operationId": "StatusController_sendVideoStatus",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SendVideoStatusDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Video status posted. The recipients allow-list is honored on Baileys only; whatsapp-web.js broadcasts to the account's status-privacy audience."
          },
          "400": {
            "description": "Neither url nor base64 provided, or the post was blocked by a plugin."
          },
          "413": {
            "description": "Base64 media exceeds MEDIA_DOWNLOAD_MAX_BYTES."
          }
        },
        "summary": "Post a video status",
        "tags": [
          "status"
        ]
      }
    },
    "/api/sessions/{sessionId}/status/send-voice": {
      "post": {
        "operationId": "StatusController_sendVoiceStatus",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SendVoiceStatusDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Voice status posted. WhatsApp plays a status voice note only as Ogg/Opus and neither engine transcodes, so convert first via POST /media/convert/voice. The recipients allow-list is honored on Baileys only; whatsapp-web.js broadcasts to the account's status-privacy audience."
          },
          "400": {
            "description": "Neither url nor base64 provided, or the post was blocked by a plugin."
          },
          "413": {
            "description": "Base64 media exceeds MEDIA_DOWNLOAD_MAX_BYTES."
          }
        },
        "summary": "Post an audio status as a voice note",
        "tags": [
          "status"
        ]
      }
    },
    "/api/sessions/{sessionId}/status/{statusId}": {
      "delete": {
        "operationId": "StatusController_deleteStatus",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "statusId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Status deleted."
          }
        },
        "summary": "Delete own status",
        "tags": [
          "status"
        ]
      }
    },
    "/api/sessions/{sessionId}/media/convert": {
      "get": {
        "operationId": "MediaController_conversionStatus",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Reports whether conversion is switched on AND the ffmpeg binary can be run, so a client can decide between converting here and converting before it sends."
          }
        },
        "summary": "Whether server-side media conversion is available",
        "tags": [
          "media"
        ]
      }
    },
    "/api/sessions/{sessionId}/media/convert/voice": {
      "post": {
        "operationId": "MediaController_convertVoice",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ConvertMediaDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Converted bytes, ready to post to send-audio with ptt=true. WhatsApp only renders a playable mic bubble for Ogg/Opus; other formats arrive as an audio file that will not play."
          },
          "400": {
            "description": "Neither url nor base64 given, or ffmpeg refused the input."
          },
          "413": {
            "description": "The supplied media is above the media size cap."
          },
          "503": {
            "description": "Conversion is disabled, the ffmpeg binary is not runnable, or the conversion queue is saturated — retry shortly."
          }
        },
        "summary": "Convert audio into a WhatsApp voice note (Ogg/Opus)",
        "tags": [
          "media"
        ]
      }
    },
    "/api/sessions/{sessionId}/media/convert/video": {
      "post": {
        "operationId": "MediaController_convertVideo",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ConvertMediaDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Converted bytes: baseline H.264 with AAC audio, long edge bounded at 1280, index moved to the front so the recipient can start playing before the whole file arrives."
          },
          "400": {
            "description": "Neither url nor base64 given, or ffmpeg refused the input."
          },
          "413": {
            "description": "The supplied media is above the media size cap."
          },
          "503": {
            "description": "Conversion is disabled, the ffmpeg binary is not runnable, or the conversion queue is saturated — retry shortly."
          }
        },
        "summary": "Convert video into a WhatsApp-compatible MP4",
        "tags": [
          "media"
        ]
      }
    },
    "/api/sessions/{sessionId}/catalog": {
      "get": {
        "operationId": "CatalogController_getCatalog",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "501": {
            "description": "Not supported by the active engine: whatsapp-web.js has no catalog API."
          }
        },
        "summary": "Get business catalog info (Baileys engine only)",
        "tags": [
          "catalog"
        ]
      }
    },
    "/api/sessions/{sessionId}/catalog/products": {
      "get": {
        "operationId": "CatalogController_getProducts",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "page",
            "required": false,
            "in": "query",
            "description": "Result page (1-based).",
            "schema": {
              "minimum": 1,
              "example": 1,
              "type": "number"
            }
          },
          {
            "name": "limit",
            "required": false,
            "in": "query",
            "description": "Page size.",
            "schema": {
              "minimum": 1,
              "example": 20,
              "type": "number"
            }
          }
        ],
        "responses": {
          "501": {
            "description": "Not supported by the active engine: whatsapp-web.js has no catalog API."
          }
        },
        "summary": "List catalog products (Baileys engine only)",
        "tags": [
          "catalog"
        ]
      }
    },
    "/api/sessions/{sessionId}/catalog/products/{productId}": {
      "get": {
        "operationId": "CatalogController_getProduct",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "productId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "501": {
            "description": "Not supported by the active engine: whatsapp-web.js has no catalog API."
          }
        },
        "summary": "Get a specific product (Baileys engine only)",
        "tags": [
          "catalog"
        ]
      }
    },
    "/api/sessions/{sessionId}/messages/send-product": {
      "post": {
        "operationId": "CatalogController_sendProduct",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SendProductDto"
              }
            }
          }
        },
        "responses": {
          "400": {
            "description": "Product has no image — a product card requires one."
          },
          "404": {
            "description": "Product id not found in the session catalog."
          },
          "501": {
            "description": "Not supported by the active engine: whatsapp-web.js cannot send product messages."
          }
        },
        "summary": "Send a product message (Baileys engine only)",
        "tags": [
          "catalog"
        ]
      }
    },
    "/api/sessions/{sessionId}/messages/send-catalog": {
      "post": {
        "operationId": "CatalogController_sendCatalog",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SendCatalogDto"
              }
            }
          }
        },
        "responses": {
          "501": {
            "description": "Not supported by the active engine: no engine can send catalog links."
          }
        },
        "summary": "Send catalog link (not supported by any engine)",
        "tags": [
          "catalog"
        ]
      }
    },
    "/api/plugins": {
      "get": {
        "operationId": "PluginsController_findAll",
        "parameters": [],
        "responses": {
          "200": {
            "description": "List of all plugins"
          }
        },
        "summary": "List all plugins",
        "tags": [
          "plugins"
        ]
      }
    },
    "/api/plugins/install": {
      "post": {
        "operationId": "PluginsController_install",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Plugin installed"
          },
          "400": {
            "description": "Invalid package"
          },
          "409": {
            "description": "Plugin already installed"
          }
        },
        "summary": "Install a plugin from an uploaded .zip package",
        "tags": [
          "plugins"
        ]
      }
    },
    "/api/plugins/install-url": {
      "post": {
        "operationId": "PluginsController_installFromUrl",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InstallFromUrlDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Plugin installed"
          },
          "400": {
            "description": "Invalid URL, download failed, or invalid package"
          },
          "409": {
            "description": "Plugin already installed"
          }
        },
        "summary": "Install a plugin by downloading its .zip from a URL (SSRF-guarded)",
        "tags": [
          "plugins"
        ]
      }
    },
    "/api/plugins/catalog": {
      "get": {
        "operationId": "PluginsController_catalog",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Catalog entries"
          },
          "400": {
            "description": "Catalog could not be fetched or parsed"
          }
        },
        "summary": "List the remote plugin catalog, annotated with install state",
        "tags": [
          "plugins"
        ]
      }
    },
    "/api/plugins/{id}": {
      "get": {
        "operationId": "PluginsController_findOne",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Plugin details"
          },
          "404": {
            "description": "Plugin not found"
          }
        },
        "summary": "Get plugin by ID",
        "tags": [
          "plugins"
        ]
      },
      "delete": {
        "operationId": "PluginsController_uninstall",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Plugin uninstalled"
          },
          "400": {
            "description": "Cannot uninstall (e.g. built-in)"
          },
          "404": {
            "description": "Plugin not found"
          }
        },
        "summary": "Uninstall a plugin (removes its files; built-ins are protected)",
        "tags": [
          "plugins"
        ]
      }
    },
    "/api/plugins/{id}/enable": {
      "post": {
        "operationId": "PluginsController_enable",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Plugin enabled successfully"
          }
        },
        "summary": "Enable a plugin",
        "tags": [
          "plugins"
        ]
      }
    },
    "/api/plugins/{id}/disable": {
      "post": {
        "operationId": "PluginsController_disable",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Plugin disabled successfully"
          }
        },
        "summary": "Disable a plugin",
        "tags": [
          "plugins"
        ]
      }
    },
    "/api/plugins/{id}/config": {
      "put": {
        "operationId": "PluginsController_updateConfig",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PluginConfigDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Plugin configuration updated"
          }
        },
        "summary": "Update plugin configuration",
        "tags": [
          "plugins"
        ]
      }
    },
    "/api/plugins/{id}/config-ui": {
      "get": {
        "operationId": "PluginsController_getConfigUi",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Config UI HTML"
          },
          "404": {
            "description": "Plugin not found or has no config UI"
          }
        },
        "summary": "Serve a plugin's sandboxed config-UI entry HTML (for an iframe srcdoc)",
        "tags": [
          "plugins"
        ]
      }
    },
    "/api/plugins/{id}/config/{sessionId}": {
      "put": {
        "operationId": "PluginsController_updateSessionConfig",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PluginConfigDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Per-session plugin configuration updated"
          },
          "400": {
            "description": "Plugin is global (not session-scoped)"
          },
          "404": {
            "description": "Plugin not found"
          }
        },
        "summary": "Set a plugin config override for a specific session (empty = clear it)",
        "tags": [
          "plugins"
        ]
      }
    },
    "/api/plugins/{id}/sessions": {
      "put": {
        "operationId": "PluginsController_updateSessions",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PluginSessionsDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Plugin session activation updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PluginDto"
                }
              }
            }
          },
          "400": {
            "description": "Plugin is global (not session-scoped)"
          },
          "403": {
            "description": "A session-restricted key may not replace the full active set — full activation replacement requires an unrestricted key"
          },
          "404": {
            "description": "Plugin not found"
          }
        },
        "summary": "Set which sessions a session-scoped plugin is activated for (['*'] = all)",
        "tags": [
          "plugins"
        ]
      }
    },
    "/api/plugins/{id}/update": {
      "post": {
        "operationId": "PluginsController_update",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InstallFromUrlDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Plugin updated"
          },
          "400": {
            "description": "Invalid URL/package, id mismatch, or built-in"
          },
          "404": {
            "description": "Plugin not found"
          }
        },
        "summary": "Update an installed plugin in place from a URL (preserves config + enabled state)",
        "tags": [
          "plugins"
        ]
      }
    },
    "/api/plugins/{id}/health": {
      "get": {
        "operationId": "PluginsController_healthCheck",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Plugin health status"
          }
        },
        "summary": "Check plugin health",
        "tags": [
          "plugins"
        ]
      }
    },
    "/api/ingress/{pluginId}/{instanceId}/{path}": {
      "get": {
        "operationId": "IngressController_receive_get",
        "parameters": [
          {
            "name": "pluginId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "instanceId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "GET verification challenge echo, or a duplicate delivery already persisted (idempotent re-delivery). Not the primary success path — see 202."
          },
          "202": {
            "description": "Webhook accepted and queued for async plugin processing (the primary success path)."
          },
          "401": {
            "description": "Signature verification failed (missing, stale, or wrong secret)."
          },
          "403": {
            "description": "GET verification challenge failed (verifyToken mismatch)."
          },
          "404": {
            "description": "Unknown pluginId/instanceId, or no route claimed by the plugin."
          },
          "413": {
            "description": "Request body exceeds the route maxBodyBytes limit."
          },
          "429": {
            "description": "Per-instance rate limit exceeded (INGRESS_INSTANCE_LIMIT)."
          }
        },
        "tags": [
          "integration"
        ],
        "security": []
      },
      "post": {
        "operationId": "IngressController_receive_post",
        "parameters": [
          {
            "name": "pluginId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "instanceId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "GET verification challenge echo, or a duplicate delivery already persisted (idempotent re-delivery). Not the primary success path — see 202."
          },
          "202": {
            "description": "Webhook accepted and queued for async plugin processing (the primary success path)."
          },
          "401": {
            "description": "Signature verification failed (missing, stale, or wrong secret)."
          },
          "403": {
            "description": "GET verification challenge failed (verifyToken mismatch)."
          },
          "404": {
            "description": "Unknown pluginId/instanceId, or no route claimed by the plugin."
          },
          "413": {
            "description": "Request body exceeds the route maxBodyBytes limit."
          },
          "429": {
            "description": "Per-instance rate limit exceeded (INGRESS_INSTANCE_LIMIT)."
          }
        },
        "tags": [
          "integration"
        ],
        "security": []
      },
      "put": {
        "operationId": "IngressController_receive_put",
        "parameters": [
          {
            "name": "pluginId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "instanceId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "GET verification challenge echo, or a duplicate delivery already persisted (idempotent re-delivery). Not the primary success path — see 202."
          },
          "202": {
            "description": "Webhook accepted and queued for async plugin processing (the primary success path)."
          },
          "401": {
            "description": "Signature verification failed (missing, stale, or wrong secret)."
          },
          "403": {
            "description": "GET verification challenge failed (verifyToken mismatch)."
          },
          "404": {
            "description": "Unknown pluginId/instanceId, or no route claimed by the plugin."
          },
          "413": {
            "description": "Request body exceeds the route maxBodyBytes limit."
          },
          "429": {
            "description": "Per-instance rate limit exceeded (INGRESS_INSTANCE_LIMIT)."
          }
        },
        "tags": [
          "integration"
        ],
        "security": []
      },
      "delete": {
        "operationId": "IngressController_receive_delete",
        "parameters": [
          {
            "name": "pluginId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "instanceId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "GET verification challenge echo, or a duplicate delivery already persisted (idempotent re-delivery). Not the primary success path — see 202."
          },
          "202": {
            "description": "Webhook accepted and queued for async plugin processing (the primary success path)."
          },
          "401": {
            "description": "Signature verification failed (missing, stale, or wrong secret)."
          },
          "403": {
            "description": "GET verification challenge failed (verifyToken mismatch)."
          },
          "404": {
            "description": "Unknown pluginId/instanceId, or no route claimed by the plugin."
          },
          "413": {
            "description": "Request body exceeds the route maxBodyBytes limit."
          },
          "429": {
            "description": "Per-instance rate limit exceeded (INGRESS_INSTANCE_LIMIT)."
          }
        },
        "tags": [
          "integration"
        ],
        "security": []
      },
      "patch": {
        "operationId": "IngressController_receive_patch",
        "parameters": [
          {
            "name": "pluginId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "instanceId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "GET verification challenge echo, or a duplicate delivery already persisted (idempotent re-delivery). Not the primary success path — see 202."
          },
          "202": {
            "description": "Webhook accepted and queued for async plugin processing (the primary success path)."
          },
          "401": {
            "description": "Signature verification failed (missing, stale, or wrong secret)."
          },
          "403": {
            "description": "GET verification challenge failed (verifyToken mismatch)."
          },
          "404": {
            "description": "Unknown pluginId/instanceId, or no route claimed by the plugin."
          },
          "413": {
            "description": "Request body exceeds the route maxBodyBytes limit."
          },
          "429": {
            "description": "Per-instance rate limit exceeded (INGRESS_INSTANCE_LIMIT)."
          }
        },
        "tags": [
          "integration"
        ],
        "security": []
      },
      "options": {
        "operationId": "IngressController_receive_options",
        "parameters": [
          {
            "name": "pluginId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "instanceId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "GET verification challenge echo, or a duplicate delivery already persisted (idempotent re-delivery). Not the primary success path — see 202."
          },
          "202": {
            "description": "Webhook accepted and queued for async plugin processing (the primary success path)."
          },
          "401": {
            "description": "Signature verification failed (missing, stale, or wrong secret)."
          },
          "403": {
            "description": "GET verification challenge failed (verifyToken mismatch)."
          },
          "404": {
            "description": "Unknown pluginId/instanceId, or no route claimed by the plugin."
          },
          "413": {
            "description": "Request body exceeds the route maxBodyBytes limit."
          },
          "429": {
            "description": "Per-instance rate limit exceeded (INGRESS_INSTANCE_LIMIT)."
          }
        },
        "tags": [
          "integration"
        ],
        "security": []
      },
      "head": {
        "operationId": "IngressController_receive_head",
        "parameters": [
          {
            "name": "pluginId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "instanceId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "GET verification challenge echo, or a duplicate delivery already persisted (idempotent re-delivery). Not the primary success path — see 202."
          },
          "202": {
            "description": "Webhook accepted and queued for async plugin processing (the primary success path)."
          },
          "401": {
            "description": "Signature verification failed (missing, stale, or wrong secret)."
          },
          "403": {
            "description": "GET verification challenge failed (verifyToken mismatch)."
          },
          "404": {
            "description": "Unknown pluginId/instanceId, or no route claimed by the plugin."
          },
          "413": {
            "description": "Request body exceeds the route maxBodyBytes limit."
          },
          "429": {
            "description": "Per-instance rate limit exceeded (INGRESS_INSTANCE_LIMIT)."
          }
        },
        "tags": [
          "integration"
        ],
        "security": []
      },
      "search": {
        "operationId": "IngressController_receive_search",
        "parameters": [
          {
            "name": "pluginId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "instanceId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "GET verification challenge echo, or a duplicate delivery already persisted (idempotent re-delivery). Not the primary success path — see 202."
          },
          "202": {
            "description": "Webhook accepted and queued for async plugin processing (the primary success path)."
          },
          "401": {
            "description": "Signature verification failed (missing, stale, or wrong secret)."
          },
          "403": {
            "description": "GET verification challenge failed (verifyToken mismatch)."
          },
          "404": {
            "description": "Unknown pluginId/instanceId, or no route claimed by the plugin."
          },
          "413": {
            "description": "Request body exceeds the route maxBodyBytes limit."
          },
          "429": {
            "description": "Per-instance rate limit exceeded (INGRESS_INSTANCE_LIMIT)."
          }
        },
        "tags": [
          "integration"
        ],
        "security": []
      }
    },
    "/api/integration/instances/{pluginId}/{instanceId}/redrive": {
      "post": {
        "operationId": "RedriveController_redriveInstance",
        "parameters": [
          {
            "name": "pluginId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "instanceId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "One bounded batch of dead-lettered ingress deliveries re-dispatched, with remaining depth."
          }
        },
        "tags": [
          "integration"
        ]
      }
    },
    "/api/integration/plugins/{pluginId}/instances": {
      "post": {
        "operationId": "IntegrationInstanceController_create",
        "parameters": [
          {
            "name": "pluginId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateInstanceDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Instance created. The plaintext ingress secret and verifyToken are revealed once in this response — store them immediately (both masked on every later read).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InstanceView"
                }
              }
            }
          }
        },
        "tags": [
          "integration"
        ]
      },
      "get": {
        "operationId": "IntegrationInstanceController_list",
        "parameters": [
          {
            "name": "pluginId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Instances for the plugin (secrets masked).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/InstanceView"
                  }
                }
              }
            }
          }
        },
        "tags": [
          "integration"
        ]
      }
    },
    "/api/integration/plugins/{pluginId}/instances/{instanceId}": {
      "get": {
        "operationId": "IntegrationInstanceController_getOne",
        "parameters": [
          {
            "name": "pluginId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "instanceId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The instance (secret masked).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InstanceView"
                }
              }
            }
          }
        },
        "tags": [
          "integration"
        ]
      },
      "patch": {
        "operationId": "IntegrationInstanceController_patch",
        "parameters": [
          {
            "name": "pluginId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "instanceId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateInstanceDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Instance updated (secret masked).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InstanceView"
                }
              }
            }
          }
        },
        "tags": [
          "integration"
        ]
      },
      "delete": {
        "operationId": "IntegrationInstanceController_remove",
        "parameters": [
          {
            "name": "pluginId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "instanceId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Instance deleted and its session scope torn down."
          }
        },
        "tags": [
          "integration"
        ]
      }
    },
    "/api/integration/plugins/{pluginId}/instances/{instanceId}/regenerate-secret": {
      "post": {
        "operationId": "IntegrationInstanceController_regenerate",
        "parameters": [
          {
            "name": "pluginId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "instanceId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Secret regenerated. The new plaintext secret is revealed once in this response; the verifyToken is also shown (unchanged).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InstanceView"
                }
              }
            }
          }
        },
        "tags": [
          "integration"
        ]
      }
    },
    "/api/search": {
      "get": {
        "operationId": "SearchController_search",
        "parameters": [
          {
            "name": "q",
            "required": true,
            "in": "query",
            "description": "Search term (required, non-empty)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sessionId",
            "required": false,
            "in": "query",
            "description": "Restrict to a single session",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "chatId",
            "required": false,
            "in": "query",
            "description": "Restrict to a single chat id",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "from",
            "required": false,
            "in": "query",
            "description": "Sender filter",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "direction",
            "required": false,
            "in": "query",
            "description": "incoming | outgoing",
            "schema": {
              "type": "string",
              "enum": [
                "incoming",
                "outgoing"
              ]
            }
          },
          {
            "name": "type",
            "required": false,
            "in": "query",
            "description": "Message type filter",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "dateFrom",
            "required": false,
            "in": "query",
            "description": "Epoch-ms lower bound (inclusive)",
            "schema": {
              "type": "number"
            }
          },
          {
            "name": "dateTo",
            "required": false,
            "in": "query",
            "description": "Epoch-ms upper bound (inclusive)",
            "schema": {
              "type": "number"
            }
          },
          {
            "name": "limit",
            "required": false,
            "in": "query",
            "description": "Max hits to return",
            "schema": {
              "type": "number"
            }
          },
          {
            "name": "offset",
            "required": false,
            "in": "query",
            "description": "Pagination offset",
            "schema": {
              "type": "number"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Search results from the active provider"
          },
          "400": {
            "description": "Empty or whitespace-only \"q\""
          },
          "501": {
            "description": "No search provider configured"
          }
        },
        "summary": "Search messages across sessions (active search provider)",
        "tags": [
          "search"
        ]
      }
    }
  },
  "info": {
    "title": "OpenWA API",
    "description": "Open Source WhatsApp API Gateway - Free, Self-Hosted HTTP API",
    "version": "0.14.4",
    "contact": {
      "name": "OpenWA",
      "url": "https://github.com/rmyndharis/OpenWA",
      "email": "yudhi@rmyndharis.com"
    }
  },
  "tags": [
    {
      "name": "sessions",
      "description": "WhatsApp session management"
    },
    {
      "name": "messages",
      "description": "Send and manage messages"
    },
    {
      "name": "webhooks",
      "description": "Webhook configuration"
    },
    {
      "name": "contacts",
      "description": "Contact management"
    },
    {
      "name": "groups",
      "description": "Group management"
    },
    {
      "name": "labels",
      "description": "Label management (WhatsApp Business)"
    },
    {
      "name": "channels",
      "description": "Channel/Newsletter management"
    },
    {
      "name": "catalog",
      "description": "Product catalog (WhatsApp Business)"
    },
    {
      "name": "status",
      "description": "Status/Stories"
    },
    {
      "name": "calls",
      "description": "Call handling"
    },
    {
      "name": "profile",
      "description": "Own profile management"
    },
    {
      "name": "search",
      "description": "Global message search"
    },
    {
      "name": "statistics",
      "description": "Usage statistics"
    },
    {
      "name": "templates",
      "description": "Message templates"
    },
    {
      "name": "plugins",
      "description": "Plugin management"
    },
    {
      "name": "settings",
      "description": "Application settings"
    },
    {
      "name": "infrastructure",
      "description": "Infrastructure & datastore management"
    },
    {
      "name": "integration",
      "description": "Integration Fabric (provider webhooks & instances)"
    },
    {
      "name": "auth",
      "description": "API key management"
    },
    {
      "name": "audit",
      "description": "Audit log"
    },
    {
      "name": "metrics",
      "description": "Prometheus metrics"
    },
    {
      "name": "health",
      "description": "Health check endpoints"
    }
  ],
  "servers": [
    {
      "url": "/",
      "description": "This instance (the origin serving these docs)"
    },
    {
      "url": "http://{host}:{port}",
      "description": "Another instance (set host and port)",
      "variables": {
        "host": {
          "default": "localhost"
        },
        "port": {
          "default": "2785",
          "description": "PORT env var"
        }
      }
    }
  ],
  "components": {
    "securitySchemes": {
      "X-API-Key": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key"
      },
      "metrics-bearer": {
        "scheme": "bearer",
        "bearerFormat": "opaque",
        "type": "http",
        "description": "METRICS_TOKEN for the GET /api/metrics scrape endpoint"
      }
    },
    "schemas": {
      "CreateApiKeyDto": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Friendly name for the API key",
            "example": "Production Bot"
          },
          "role": {
            "type": "string",
            "description": "Role/permission level",
            "enum": [
              "admin",
              "operator",
              "viewer"
            ],
            "default": "operator"
          },
          "allowedIps": {
            "description": "Allowed IP addresses (whitelist)",
            "example": [
              "192.168.1.1",
              "10.0.0.0/8"
            ],
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "allowedSessions": {
            "description": "Allowed session IDs this key can access",
            "example": [
              "session-uuid-1",
              "session-uuid-2"
            ],
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "expiresAt": {
            "type": "string",
            "description": "Expiration date (ISO 8601)",
            "example": "2027-12-31T23:59:59Z"
          }
        },
        "required": [
          "name"
        ]
      },
      "ApiKeyCreatedResponseDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "keyPrefix": {
            "type": "string",
            "description": "First 8 characters of the key (for identification)"
          },
          "role": {
            "type": "string",
            "enum": [
              "admin",
              "operator",
              "viewer"
            ]
          },
          "allowedIps": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "allowedSessions": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "isActive": {
            "type": "boolean"
          },
          "expiresAt": {
            "format": "date-time",
            "type": "string"
          },
          "lastUsedAt": {
            "format": "date-time",
            "type": "string"
          },
          "usageCount": {
            "type": "number"
          },
          "createdAt": {
            "format": "date-time",
            "type": "string"
          },
          "apiKey": {
            "type": "string",
            "description": "Full API key (only shown once at creation)",
            "example": "owa_k1_abc123..."
          }
        },
        "required": [
          "id",
          "name",
          "keyPrefix",
          "role",
          "isActive",
          "usageCount",
          "createdAt",
          "apiKey"
        ]
      },
      "ApiKeyResponseDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "keyPrefix": {
            "type": "string",
            "description": "First 8 characters of the key (for identification)"
          },
          "role": {
            "type": "string",
            "enum": [
              "admin",
              "operator",
              "viewer"
            ]
          },
          "allowedIps": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "allowedSessions": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "isActive": {
            "type": "boolean"
          },
          "expiresAt": {
            "format": "date-time",
            "type": "string"
          },
          "lastUsedAt": {
            "format": "date-time",
            "type": "string"
          },
          "usageCount": {
            "type": "number"
          },
          "createdAt": {
            "format": "date-time",
            "type": "string"
          }
        },
        "required": [
          "id",
          "name",
          "keyPrefix",
          "role",
          "isActive",
          "usageCount",
          "createdAt"
        ]
      },
      "UpdateApiKeyDto": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "role": {
            "type": "string",
            "enum": [
              "admin",
              "operator",
              "viewer"
            ]
          },
          "allowedIps": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "allowedSessions": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "expiresAt": {
            "type": "string"
          }
        }
      },
      "CreateSessionDto": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Unique name for the session (alphanumeric and hyphens only)",
            "example": "my-bot",
            "minLength": 3,
            "maxLength": 50
          },
          "config": {
            "type": "object",
            "description": "Session configuration options. Set autoRejectCalls (boolean, default false) to automatically reject incoming calls — the call.received event is still emitted.",
            "example": {
              "autoReconnect": true
            }
          },
          "proxyUrl": {
            "type": "string",
            "description": "Optional per-session egress proxy URL (http/https/socks4/socks5; credentialed form \"http://user:pass@host\" allowed). Must be a REAL, REACHABLE proxy — an unreachable value silently blocks the WhatsApp WebSocket (no QR is ever delivered) and the session start times out (~30s → 504 Gateway Timeout). Leave unset unless your network cannot reach WhatsApp directly."
          },
          "proxyType": {
            "type": "string",
            "description": "Proxy type",
            "enum": [
              "http",
              "https",
              "socks4",
              "socks5"
            ],
            "example": "http"
          }
        },
        "required": [
          "name"
        ]
      },
      "AccountRestrictionDto": {
        "type": "object",
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "reachout_timelock",
              "tos_block",
              "proxy_block"
            ],
            "description": "What WhatsApp is restricting. `reachout_timelock` leaves the session connected and existing chats working, blocking only the start of new conversations. `tos_block` and `proxy_block` are connection-level refusals — the session cannot stay linked while one is in force, so seeing either alongside a `ready` status is not possible.",
            "example": "reachout_timelock"
          },
          "code": {
            "type": "string",
            "description": "The engine's own token for the cause, passed through verbatim so it can be searched for and so a value newer than this gateway is still surfaced rather than flattened.",
            "example": "BIZ_QUALITY"
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time",
            "description": "When enforcement ends, if the engine states it. Only reachout timelocks carry an expiry; absent means the engine gave no end time, not that the restriction is permanent.",
            "example": "2026-08-04T09:00:00Z",
            "nullable": true
          }
        },
        "required": [
          "kind",
          "code"
        ]
      },
      "SessionResponseDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "sess_123e4567-e89b-12d3-a456-426614174000"
          },
          "name": {
            "type": "string",
            "example": "my-bot"
          },
          "status": {
            "type": "string",
            "enum": [
              "created",
              "initializing",
              "qr_ready",
              "authenticating",
              "ready",
              "disconnected",
              "action_required",
              "failed"
            ],
            "example": "ready"
          },
          "phone": {
            "type": "string",
            "example": "628123456789",
            "nullable": true
          },
          "pushName": {
            "type": "string",
            "example": "John Doe",
            "nullable": true
          },
          "connectedAt": {
            "type": "string",
            "format": "date-time",
            "example": "2025-02-02T10:00:00Z",
            "nullable": true
          },
          "lastActive": {
            "type": "string",
            "format": "date-time",
            "example": "2025-02-02T10:30:00Z",
            "nullable": true
          },
          "createdAt": {
            "format": "date-time",
            "type": "string",
            "example": "2025-02-02T09:00:00Z"
          },
          "updatedAt": {
            "format": "date-time",
            "type": "string",
            "example": "2025-02-02T10:00:00Z"
          },
          "lastError": {
            "type": "string",
            "description": "Human-readable reason carried while the status is FAILED (a terminal engine failure) or ACTION_REQUIRED (the engine is running but something needs a human). Cleared on any other status.",
            "example": "Failed to launch the browser process: spawn /usr/bin/chromium ENOENT",
            "nullable": true
          },
          "restriction": {
            "description": "A restriction WhatsApp itself has placed on this session's account, or null when there is none. Distinct from `lastError`, which describes a fault on our side of the link. Derived from live engine state, so it is never persisted: it is re-established on the next connect.",
            "nullable": true,
            "type": "object",
            "allOf": [
              {
                "$ref": "#/components/schemas/AccountRestrictionDto"
              }
            ]
          },
          "engineLoaded": {
            "type": "boolean",
            "description": "Whether the gateway currently holds a live engine for this session. This is the precondition the lifecycle routes actually enforce, and `status` alone does not imply it: a `disconnected` session keeps its engine for the duration of an automatic reconnect backoff, while a session stopped through `POST /sessions/:id/stop` carries the same status with no engine. When `true`, `stop`, `logout` and `force-kill` can act and `start` answers 400; when `false`, the reverse. Derived per request from live process state, so it is never persisted and never historical.",
            "example": true
          }
        },
        "required": [
          "id",
          "name",
          "status",
          "createdAt",
          "updatedAt",
          "engineLoaded"
        ]
      },
      "QRCodeResponseDto": {
        "type": "object",
        "properties": {
          "qrCode": {
            "type": "string",
            "description": "QR code as data URL",
            "example": "data:image/png;base64,..."
          },
          "status": {
            "type": "string",
            "enum": [
              "created",
              "initializing",
              "qr_ready",
              "authenticating",
              "ready",
              "disconnected",
              "action_required",
              "failed"
            ],
            "example": "qr_ready"
          }
        },
        "required": [
          "qrCode",
          "status"
        ]
      },
      "RequestPairingCodeDto": {
        "type": "object",
        "properties": {
          "phoneNumber": {
            "type": "string",
            "description": "Phone number to link, digits only in international format (country code + number).",
            "example": "628123456789"
          }
        },
        "required": [
          "phoneNumber"
        ]
      },
      "PairingCodeResponseDto": {
        "type": "object",
        "properties": {
          "pairingCode": {
            "type": "string",
            "description": "The 8-character pairing code to enter in WhatsApp.",
            "example": "ABCD1234"
          },
          "status": {
            "type": "string",
            "description": "Current session status.",
            "example": "qr_ready"
          }
        },
        "required": [
          "pairingCode",
          "status"
        ]
      },
      "ChatSummaryDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "628111@c.us"
          },
          "name": {
            "type": "string",
            "example": "Alice"
          },
          "isGroup": {
            "type": "boolean",
            "description": "Retained for back-compat; true for @g.us chats.",
            "example": false
          },
          "kind": {
            "type": "string",
            "enum": [
              "individual",
              "group",
              "channel",
              "status",
              "broadcast",
              "unknown"
            ],
            "description": "User-facing chat kind.",
            "example": "individual"
          },
          "unreadCount": {
            "type": "number",
            "example": 1
          },
          "timestamp": {
            "type": "number",
            "description": "Unix seconds of the last activity.",
            "example": 1700000010
          },
          "lastMessage": {
            "type": "string",
            "example": "hi"
          }
        },
        "required": [
          "id",
          "name",
          "isGroup",
          "kind",
          "unreadCount",
          "timestamp"
        ]
      },
      "MarkChatReadDto": {
        "type": "object",
        "properties": {
          "chatId": {
            "type": "string",
            "description": "Chat ID in the active engine's native format (e.g. 1234567890@c.us on whatsapp-web.js)",
            "example": "1234567890@c.us"
          }
        },
        "required": [
          "chatId"
        ]
      },
      "SubscribePresenceDto": {
        "type": "object",
        "properties": {
          "chatId": {
            "type": "string",
            "description": "Chat ID in the active engine's native format (e.g. 1234567890@c.us on whatsapp-web.js)",
            "example": "1234567890@c.us"
          }
        },
        "required": [
          "chatId"
        ]
      },
      "ParticipantPresenceDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Participant id. In a 1:1 chat this is the chat itself.",
            "example": "1234567890@c.us"
          },
          "state": {
            "type": "string",
            "enum": [
              "available",
              "unavailable",
              "composing",
              "recording",
              "paused"
            ],
            "description": "`composing` and `recording` mean actively typing or recording in this chat; `paused` means they stopped without sending. `available`/`unavailable` describe reachability.",
            "example": "composing"
          },
          "lastSeen": {
            "type": "number",
            "description": "Unix SECONDS the contact was last seen. Absent whenever the contact's privacy settings hide last-seen — the common case, and not an error.",
            "example": 1786000000
          }
        },
        "required": [
          "id",
          "state"
        ]
      },
      "ChatPresenceResponseDto": {
        "type": "object",
        "properties": {
          "chatId": {
            "type": "string",
            "example": "1234567890@c.us"
          },
          "participants": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ParticipantPresenceDto"
            }
          },
          "groupOnlineCount": {
            "type": "number",
            "description": "Online member count, groups only.",
            "example": 3
          },
          "observedAt": {
            "type": "string",
            "format": "date-time",
            "description": "When this gateway received the report — NOT a WhatsApp timestamp. Presence is short-lived, so an old `observedAt` means the state is stale rather than steady.",
            "example": "2026-08-03T12:00:00Z"
          }
        },
        "required": [
          "chatId",
          "participants",
          "observedAt"
        ]
      },
      "ArchiveChatDto": {
        "type": "object",
        "properties": {
          "chatId": {
            "type": "string",
            "description": "Chat ID in the active engine's native format (e.g. 1234567890-123@g.us on whatsapp-web.js)",
            "example": "1234567890-123@g.us"
          },
          "archive": {
            "type": "boolean",
            "description": "true to archive, false to unarchive."
          }
        },
        "required": [
          "chatId",
          "archive"
        ]
      },
      "DeleteChatDto": {
        "type": "object",
        "properties": {
          "chatId": {
            "type": "string",
            "description": "Chat ID in the active engine's native format (e.g. 1234567890-123@g.us on whatsapp-web.js)",
            "example": "1234567890-123@g.us"
          }
        },
        "required": [
          "chatId"
        ]
      },
      "SendChatStateDto": {
        "type": "object",
        "properties": {
          "chatId": {
            "type": "string",
            "description": "Chat ID, in the active engine's native format (e.g. 1234567890@c.us)",
            "example": "1234567890@c.us"
          },
          "state": {
            "type": "string",
            "description": "Presence to send: 'typing' or 'recording' shows the indicator; 'paused' clears it",
            "enum": [
              "typing",
              "recording",
              "paused"
            ],
            "example": "typing"
          }
        },
        "required": [
          "chatId",
          "state"
        ]
      },
      "CreateWebhookDto": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "description": "Webhook URL to receive events",
            "example": "https://your-server.com/webhook"
          },
          "events": {
            "type": "array",
            "description": "Event types to subscribe to. '*' subscribes to all events.",
            "example": [
              "message.received",
              "session.status"
            ],
            "minItems": 1,
            "items": {
              "type": "string",
              "enum": [
                "message.received",
                "message.sent",
                "message.ack",
                "message.failed",
                "message.revoked",
                "message.reaction",
                "message.edited",
                "status.received",
                "session.status",
                "session.qr",
                "session.authenticated",
                "session.disconnected",
                "session.reconnect_loop",
                "session.restriction",
                "presence.update",
                "group.join",
                "group.leave",
                "group.update",
                "call.received",
                "call.accepted",
                "call.rejected",
                "call.missed",
                "*"
              ]
            }
          },
          "secret": {
            "type": "string",
            "description": "Secret key for HMAC signature verification",
            "example": "your-secret-key"
          },
          "headers": {
            "type": "object",
            "description": "Custom headers to include in webhook requests",
            "example": {
              "X-Custom-Header": "value"
            }
          },
          "filters": {
            "type": "object",
            "description": "Optional smart pre-filter. When set, every condition must match (AND) for the webhook to fire. Omit or null to fire on every subscribed event.",
            "example": {
              "conditions": [
                {
                  "field": "sender",
                  "operator": "is",
                  "value": [
                    "1234567890@c.us"
                  ]
                },
                {
                  "field": "body",
                  "operator": "contains",
                  "value": "invoice"
                }
              ]
            }
          },
          "retryCount": {
            "type": "number",
            "description": "Number of retry attempts on failure",
            "example": 3,
            "minimum": 0,
            "maximum": 5
          }
        },
        "required": [
          "url"
        ]
      },
      "WebhookResponseDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "sessionId": {
            "type": "string"
          },
          "url": {
            "type": "string"
          },
          "events": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "filters": {
            "type": "object",
            "description": "Optional smart pre-filter. When set, every condition must match (AND) for the webhook to fire. Omit or null to fire on every subscribed event.",
            "example": {
              "conditions": [
                {
                  "field": "sender",
                  "operator": "is",
                  "value": [
                    "1234567890@c.us"
                  ]
                },
                {
                  "field": "body",
                  "operator": "contains",
                  "value": "invoice"
                }
              ]
            }
          },
          "active": {
            "type": "boolean"
          },
          "retryCount": {
            "type": "number"
          },
          "lastTriggeredAt": {
            "type": "object"
          },
          "createdAt": {
            "format": "date-time",
            "type": "string"
          },
          "updatedAt": {
            "format": "date-time",
            "type": "string"
          }
        },
        "required": [
          "id",
          "sessionId",
          "url",
          "events",
          "active",
          "retryCount",
          "createdAt",
          "updatedAt"
        ]
      },
      "UpdateWebhookDto": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "description": "Webhook URL"
          },
          "events": {
            "type": "array",
            "description": "Event types to subscribe to. '*' subscribes to all events.",
            "minItems": 1,
            "items": {
              "type": "string",
              "enum": [
                "message.received",
                "message.sent",
                "message.ack",
                "message.failed",
                "message.revoked",
                "message.reaction",
                "message.edited",
                "status.received",
                "session.status",
                "session.qr",
                "session.authenticated",
                "session.disconnected",
                "session.reconnect_loop",
                "session.restriction",
                "presence.update",
                "group.join",
                "group.leave",
                "group.update",
                "call.received",
                "call.accepted",
                "call.rejected",
                "call.missed",
                "*"
              ]
            }
          },
          "secret": {
            "type": "string",
            "description": "Secret key for HMAC signature"
          },
          "headers": {
            "type": "object",
            "description": "Custom headers"
          },
          "filters": {
            "type": "object",
            "description": "Optional smart pre-filter. When set, every condition must match (AND) for the webhook to fire. Omit or null to fire on every subscribed event.",
            "example": {
              "conditions": [
                {
                  "field": "sender",
                  "operator": "is",
                  "value": [
                    "1234567890@c.us"
                  ]
                },
                {
                  "field": "body",
                  "operator": "contains",
                  "value": "invoice"
                }
              ]
            }
          },
          "active": {
            "type": "boolean",
            "description": "Enable/disable webhook"
          },
          "retryCount": {
            "type": "number",
            "description": "Retry count"
          }
        }
      },
      "CreateAutomationRuleDto": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Display name for the rule",
            "example": "Greet new enquiries",
            "maxLength": 100
          },
          "replyText": {
            "type": "string",
            "description": "Text sent back into the chat when the rule matches",
            "example": "Thanks for reaching out — we reply within the hour.",
            "maxLength": 4096
          },
          "conditions": {
            "type": "object",
            "description": "Match conditions in the webhook filter format (message family: sender, recipient, body, type, isGroup, fromMe, hasMedia, mentions). All conditions must match (AND). Omitted or empty means the rule matches every inbound message."
          },
          "cooldownSeconds": {
            "type": "number",
            "description": "Quiet period per chat, in seconds: after the rule replies in a chat it stays silent there for this long (default 60, 0 disables). This is the guard against two auto-repliers answering each other forever, so disable it knowingly.",
            "default": 60,
            "minimum": 0,
            "maximum": 86400
          },
          "enabled": {
            "type": "boolean",
            "description": "Whether the rule is active",
            "default": true
          }
        },
        "required": [
          "name",
          "replyText"
        ]
      },
      "AutomationRuleResponseDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "sessionId": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "enabled": {
            "type": "boolean"
          },
          "conditions": {
            "type": "object",
            "description": "Match conditions in the webhook filter format (message family: sender, recipient, body, type, isGroup, fromMe, hasMedia, mentions). All conditions must match (AND). Omitted or empty means the rule matches every inbound message.",
            "nullable": true
          },
          "replyText": {
            "type": "string"
          },
          "cooldownSeconds": {
            "type": "number"
          },
          "createdAt": {
            "format": "date-time",
            "type": "string"
          },
          "updatedAt": {
            "format": "date-time",
            "type": "string"
          }
        },
        "required": [
          "id",
          "sessionId",
          "name",
          "enabled",
          "replyText",
          "cooldownSeconds",
          "createdAt",
          "updatedAt"
        ]
      },
      "UpdateAutomationRuleDto": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Display name for the rule",
            "maxLength": 100
          },
          "replyText": {
            "type": "string",
            "description": "Text sent back into the chat when the rule matches",
            "maxLength": 4096
          },
          "conditions": {
            "type": "object",
            "description": "Match conditions in the webhook filter format (message family: sender, recipient, body, type, isGroup, fromMe, hasMedia, mentions). All conditions must match (AND). Omitted or empty means the rule matches every inbound message."
          },
          "cooldownSeconds": {
            "type": "number",
            "description": "Quiet period per chat, in seconds: after the rule replies in a chat it stays silent there for this long (default 60, 0 disables). This is the guard against two auto-repliers answering each other forever, so disable it knowingly.",
            "minimum": 0,
            "maximum": 86400
          },
          "enabled": {
            "type": "boolean",
            "description": "Whether the rule is active"
          }
        }
      },
      "CustomLinkPreviewDto": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "description": "The URL as it appears in the message text — WhatsApp anchors the preview to it.",
            "example": "https://example.com/launch"
          },
          "title": {
            "type": "string",
            "description": "Required: WhatsApp will not render a preview without a title.",
            "example": "We just launched",
            "maxLength": 256
          },
          "description": {
            "type": "string",
            "description": "Preview description",
            "example": "Read the announcement.",
            "maxLength": 1024
          }
        },
        "required": [
          "url",
          "title"
        ]
      },
      "SendTextMessageDto": {
        "type": "object",
        "properties": {
          "chatId": {
            "type": "string",
            "description": "WhatsApp chat ID (phone@c.us for individual, groupId@g.us for groups)",
            "example": "628123456789@c.us"
          },
          "text": {
            "type": "string",
            "description": "Text message content",
            "example": "Hello from OpenWA!",
            "maxLength": 4096
          },
          "mentions": {
            "description": "WIDs to @mention (e.g. [\"62811@c.us\"]). The text/caption must also contain the @<number> token.",
            "example": [
              "628123456789@c.us"
            ],
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "linkPreview": {
            "type": "boolean",
            "description": "Controls the URL preview, and the engines differ. On whatsapp-web.js WhatsApp Web builds one by default and `false` suppresses it. On Baileys previews are OPT-IN: `true` asks the gateway to fetch the page and attach one, while unset or `false` sends none — generating a preview is a blocking outbound fetch per URL in the text, so it is never done unless asked for.",
            "example": false
          },
          "customLinkPreview": {
            "description": "Attach a preview you supply yourself, instead of one fetched from the URL. Nothing is fetched for these, so a preview can be attached even for a URL this server cannot reach. **Baileys only** — whatsapp-web.js takes a boolean and answers `501`. Cannot be combined with `linkPreview: false`, which asks for the opposite.",
            "allOf": [
              {
                "$ref": "#/components/schemas/CustomLinkPreviewDto"
              }
            ]
          }
        },
        "required": [
          "chatId",
          "text"
        ]
      },
      "MessageResponseDto": {
        "type": "object",
        "properties": {
          "messageId": {
            "type": "string",
            "description": "The message id, assigned when the gateway accepts the message for sending. A 201 here means the message was handed to the WhatsApp client — it does NOT confirm delivery. WhatsApp does not reject an unregistered recipient synchronously, so a message to a number that is not on WhatsApp still returns 201 with a valid messageId; whether it later delivers, stalls, or is reported as an error reaches you asynchronously, if at all. To confirm a number is on WhatsApp before sending, use GET /api/sessions/{sessionId}/contacts/check/{number}; track real delivery via the message `status` field (sent → delivered → read, or failed if WhatsApp reports an error for it). A message resting at `sent` is not diagnostic on its own: a registered recipient whose device has not come online since the send stays at `sent` too.",
            "example": "true_628123456789@c.us_3EB0123456789"
          },
          "timestamp": {
            "type": "number",
            "description": "Unix timestamp (seconds) at which the gateway accepted the message for sending.",
            "example": 1706868000
          }
        },
        "required": [
          "messageId",
          "timestamp"
        ]
      },
      "SendTemplateMessageDto": {
        "type": "object",
        "properties": {
          "chatId": {
            "type": "string",
            "description": "WhatsApp chat ID (phone@c.us for individual, groupId@g.us for groups)",
            "example": "628123456789@c.us"
          },
          "templateId": {
            "type": "string",
            "description": "Template ID to render. Provide either templateId or templateName.",
            "example": "b1c2d3e4-f5a6-7890-bcde-f01234567890"
          },
          "templateName": {
            "type": "string",
            "description": "Template name to render. Provide either templateId or templateName.",
            "example": "order-confirmation"
          },
          "vars": {
            "type": "object",
            "description": "Variables substituted into {{placeholder}} tokens in the template",
            "example": {
              "customer": "Alice",
              "orderId": "1234"
            }
          }
        },
        "required": [
          "chatId"
        ]
      },
      "SendMediaMessageDto": {
        "type": "object",
        "properties": {
          "chatId": {
            "type": "string",
            "description": "WhatsApp chat ID",
            "example": "628123456789@c.us"
          },
          "url": {
            "type": "string",
            "description": "Media URL (http/https)",
            "example": "https://example.com/image.jpg"
          },
          "base64": {
            "type": "string",
            "description": "Base64 encoded media data"
          },
          "mimetype": {
            "type": "string",
            "description": "Media MIME type (required when using base64)",
            "example": "image/jpeg"
          },
          "filename": {
            "type": "string",
            "description": "Filename for the media. Only rendered on document sends — defaults to 'file' when omitted (a URL-based document send on whatsapp-web.js first derives the URL basename)",
            "example": "image.jpg"
          },
          "caption": {
            "type": "string",
            "description": "Caption for the media",
            "example": "Check out this image!",
            "maxLength": 1024
          },
          "mentions": {
            "description": "WIDs to @mention (e.g. [\"62811@c.us\"]). The text/caption must also contain the @<number> token.",
            "example": [
              "628123456789@c.us"
            ],
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "chatId"
        ]
      },
      "SendAudioMessageDto": {
        "type": "object",
        "properties": {
          "chatId": {
            "type": "string",
            "description": "WhatsApp chat ID",
            "example": "628123456789@c.us"
          },
          "url": {
            "type": "string",
            "description": "Media URL (http/https)",
            "example": "https://example.com/image.jpg"
          },
          "base64": {
            "type": "string",
            "description": "Base64 encoded media data"
          },
          "mimetype": {
            "type": "string",
            "description": "Media MIME type (required when using base64)",
            "example": "image/jpeg"
          },
          "filename": {
            "type": "string",
            "description": "Filename for the media. Only rendered on document sends — defaults to 'file' when omitted (a URL-based document send on whatsapp-web.js first derives the URL basename)",
            "example": "image.jpg"
          },
          "caption": {
            "type": "string",
            "description": "Caption for the media",
            "example": "Check out this image!",
            "maxLength": 1024
          },
          "mentions": {
            "description": "WIDs to @mention (e.g. [\"62811@c.us\"]). The text/caption must also contain the @<number> token.",
            "example": [
              "628123456789@c.us"
            ],
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "ptt": {
            "type": "boolean",
            "description": "Send as a WhatsApp voice note (PTT — mic bubble + waveform). Provide audio/ogg; codecs=opus bytes for reliable playback; when the mimetype is omitted it defaults to that for voice notes. Expects a JSON boolean. Default false = plain audio file. Only valid on send-audio."
          }
        },
        "required": [
          "chatId"
        ]
      },
      "SendLocationDto": {
        "type": "object",
        "properties": {
          "chatId": {
            "type": "string",
            "description": "Chat ID (e.g. 628123456789@c.us)"
          },
          "latitude": {
            "type": "number",
            "example": -6.2088
          },
          "longitude": {
            "type": "number",
            "example": 106.8456
          },
          "description": {
            "type": "string",
            "maxLength": 1024
          },
          "address": {
            "type": "string",
            "maxLength": 1024
          }
        },
        "required": [
          "chatId",
          "latitude",
          "longitude"
        ]
      },
      "SendContactDto": {
        "type": "object",
        "properties": {
          "chatId": {
            "type": "string"
          },
          "contactName": {
            "type": "string",
            "maxLength": 255
          },
          "contactNumber": {
            "type": "string",
            "maxLength": 30
          }
        },
        "required": [
          "chatId",
          "contactName",
          "contactNumber"
        ]
      },
      "SendPollDto": {
        "type": "object",
        "properties": {
          "chatId": {
            "type": "string",
            "description": "Chat ID (e.g. 628123456789@c.us or 1203630000@g.us)"
          },
          "name": {
            "type": "string",
            "description": "Poll question / title",
            "maxLength": 255,
            "example": "Where should we meet?"
          },
          "options": {
            "description": "Options to vote on (WhatsApp allows between 2 and 12)",
            "example": [
              "Park",
              "Beach",
              "Downtown"
            ],
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "allowMultipleAnswers": {
            "type": "boolean",
            "description": "Allow voters to pick several options (default single choice)"
          }
        },
        "required": [
          "chatId",
          "name",
          "options"
        ]
      },
      "ReplyMessageDto": {
        "type": "object",
        "properties": {
          "chatId": {
            "type": "string"
          },
          "quotedMessageId": {
            "type": "string"
          },
          "text": {
            "type": "string",
            "maxLength": 4096
          }
        },
        "required": [
          "chatId",
          "quotedMessageId",
          "text"
        ]
      },
      "ForwardMessageDto": {
        "type": "object",
        "properties": {
          "fromChatId": {
            "type": "string"
          },
          "toChatId": {
            "type": "string"
          },
          "messageId": {
            "type": "string"
          }
        },
        "required": [
          "fromChatId",
          "toChatId",
          "messageId"
        ]
      },
      "ReactMessageDto": {
        "type": "object",
        "properties": {
          "chatId": {
            "type": "string"
          },
          "messageId": {
            "type": "string"
          },
          "emoji": {
            "type": "string",
            "description": "Emoji to react with. Send an empty string to remove the reaction.",
            "maxLength": 32
          }
        },
        "required": [
          "chatId",
          "messageId",
          "emoji"
        ]
      },
      "DeleteMessageDto": {
        "type": "object",
        "properties": {
          "chatId": {
            "type": "string"
          },
          "messageId": {
            "type": "string"
          },
          "forEveryone": {
            "type": "boolean",
            "description": "Delete for everyone (default true)"
          }
        },
        "required": [
          "chatId",
          "messageId"
        ]
      },
      "VotePollDto": {
        "type": "object",
        "properties": {
          "chatId": {
            "type": "string"
          },
          "pollMessageId": {
            "type": "string",
            "description": "The poll creation message to vote on."
          },
          "options": {
            "description": "The option TEXTS to select, exactly as they appear on the poll. Replaces the current selection; an empty array clears the vote.",
            "maxItems": 12,
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "chatId",
          "pollMessageId",
          "options"
        ]
      },
      "PinMessageDto": {
        "type": "object",
        "properties": {
          "chatId": {
            "type": "string"
          },
          "messageId": {
            "type": "string"
          },
          "durationSeconds": {
            "type": "number",
            "description": "Pin duration in seconds: 86400 (24h), 604800 (7d) or 2592000 (30d). Defaults to 24h.",
            "enum": [
              86400,
              604800,
              2592000
            ],
            "default": 86400
          }
        },
        "required": [
          "chatId",
          "messageId"
        ]
      },
      "UnpinMessageDto": {
        "type": "object",
        "properties": {
          "chatId": {
            "type": "string"
          },
          "messageId": {
            "type": "string"
          }
        },
        "required": [
          "chatId",
          "messageId"
        ]
      },
      "StarMessageDto": {
        "type": "object",
        "properties": {
          "chatId": {
            "type": "string"
          },
          "messageId": {
            "type": "string"
          },
          "star": {
            "type": "boolean",
            "description": "true to star, false to remove the star."
          }
        },
        "required": [
          "chatId",
          "messageId",
          "star"
        ]
      },
      "EditMessageDto": {
        "type": "object",
        "properties": {
          "chatId": {
            "type": "string"
          },
          "messageId": {
            "type": "string"
          },
          "body": {
            "type": "string",
            "description": "New text body for the message",
            "maxLength": 4096
          }
        },
        "required": [
          "chatId",
          "messageId",
          "body"
        ]
      },
      "BulkMediaDto": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "description": "Media URL (http/https)"
          },
          "base64": {
            "type": "string",
            "description": "Base64-encoded media data"
          },
          "mimetype": {
            "type": "string",
            "description": "Media MIME type"
          },
          "filename": {
            "type": "string",
            "description": "Filename (documents only)"
          },
          "ptt": {
            "type": "boolean",
            "description": "Audio only: send as a WhatsApp voice note (PTT)"
          }
        }
      },
      "BulkMessageContentDto": {
        "type": "object",
        "properties": {
          "text": {
            "type": "string",
            "description": "Text content for text messages",
            "maxLength": 4096
          },
          "image": {
            "description": "Image URL or base64",
            "allOf": [
              {
                "$ref": "#/components/schemas/BulkMediaDto"
              }
            ]
          },
          "video": {
            "description": "Video URL or base64",
            "allOf": [
              {
                "$ref": "#/components/schemas/BulkMediaDto"
              }
            ]
          },
          "audio": {
            "description": "Audio URL or base64",
            "allOf": [
              {
                "$ref": "#/components/schemas/BulkMediaDto"
              }
            ]
          },
          "document": {
            "description": "Document URL or base64",
            "allOf": [
              {
                "$ref": "#/components/schemas/BulkMediaDto"
              }
            ]
          },
          "caption": {
            "type": "string",
            "description": "Caption for media messages",
            "maxLength": 1024
          }
        }
      },
      "BulkMessageItemDto": {
        "type": "object",
        "properties": {
          "chatId": {
            "type": "string",
            "description": "Recipient chat ID",
            "example": "628123456789@c.us"
          },
          "type": {
            "type": "string",
            "description": "Message type",
            "enum": [
              "text",
              "image",
              "video",
              "audio",
              "document"
            ]
          },
          "content": {
            "description": "Message content based on type",
            "allOf": [
              {
                "$ref": "#/components/schemas/BulkMessageContentDto"
              }
            ]
          },
          "variables": {
            "type": "object",
            "description": "Variables for template substitution"
          }
        },
        "required": [
          "chatId",
          "type",
          "content"
        ]
      },
      "BulkMessageOptionsDto": {
        "type": "object",
        "properties": {
          "delayBetweenMessages": {
            "type": "number",
            "description": "Delay between messages in ms (min: 1000, default: 3000)",
            "default": 3000
          },
          "randomizeDelay": {
            "type": "boolean",
            "description": "Add random 0-2s to delay",
            "default": true
          },
          "stopOnError": {
            "type": "boolean",
            "description": "Stop batch on first error",
            "default": false
          }
        }
      },
      "SendBulkMessageDto": {
        "type": "object",
        "properties": {
          "batchId": {
            "type": "string",
            "description": "Custom batch ID (auto-generated if not provided)"
          },
          "messages": {
            "description": "Array of messages (max 100 per request; exact duplicate entries are collapsed — first occurrence wins)",
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BulkMessageItemDto"
            }
          },
          "options": {
            "description": "Batch processing options",
            "allOf": [
              {
                "$ref": "#/components/schemas/BulkMessageOptionsDto"
              }
            ]
          }
        },
        "required": [
          "messages"
        ]
      },
      "BulkMessageResponseDto": {
        "type": "object",
        "properties": {
          "batchId": {
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "totalMessages": {
            "type": "number"
          },
          "estimatedCompletionTime": {
            "type": "string"
          },
          "statusUrl": {
            "type": "string"
          }
        },
        "required": [
          "batchId",
          "status",
          "totalMessages",
          "statusUrl"
        ]
      },
      "CreateTemplateDto": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Unique template name within the session",
            "example": "order-confirmation",
            "maxLength": 100
          },
          "body": {
            "type": "string",
            "description": "Template body with {{variable}} placeholders",
            "example": "Hi {{customer}}, your order {{orderId}} has shipped.",
            "maxLength": 4096
          },
          "header": {
            "type": "string",
            "description": "Optional header text, prepended to the rendered body",
            "example": "OpenWA Store",
            "maxLength": 1024
          },
          "footer": {
            "type": "string",
            "description": "Optional footer text, appended to the rendered body",
            "example": "Reply STOP to unsubscribe.",
            "maxLength": 1024
          }
        },
        "required": [
          "name",
          "body"
        ]
      },
      "TemplateResponseDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "sessionId": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "body": {
            "type": "string"
          },
          "header": {
            "type": "string",
            "nullable": true
          },
          "footer": {
            "type": "string",
            "nullable": true
          },
          "createdAt": {
            "format": "date-time",
            "type": "string"
          },
          "updatedAt": {
            "format": "date-time",
            "type": "string"
          }
        },
        "required": [
          "id",
          "sessionId",
          "name",
          "body",
          "createdAt",
          "updatedAt"
        ]
      },
      "UpdateTemplateDto": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Template name",
            "maxLength": 100
          },
          "body": {
            "type": "string",
            "description": "Template body with {{variable}} placeholders",
            "maxLength": 4096
          },
          "header": {
            "type": "string",
            "description": "Optional header text",
            "maxLength": 1024
          },
          "footer": {
            "type": "string",
            "description": "Optional footer text",
            "maxLength": 1024
          }
        }
      },
      "DatabaseConfigDto": {
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "sqlite",
              "postgres"
            ]
          },
          "builtIn": {
            "type": "boolean"
          },
          "host": {
            "type": "string"
          },
          "port": {
            "type": "string"
          },
          "username": {
            "type": "string"
          },
          "password": {
            "type": "string"
          },
          "database": {
            "type": "string"
          },
          "schema": {
            "type": "string"
          },
          "poolSize": {
            "type": "number"
          },
          "sslEnabled": {
            "type": "boolean"
          },
          "sslRejectUnauthorized": {
            "type": "boolean"
          }
        },
        "required": [
          "type"
        ]
      },
      "RedisConfigDto": {
        "type": "object",
        "properties": {
          "enabled": {
            "type": "boolean"
          },
          "builtIn": {
            "type": "boolean"
          },
          "host": {
            "type": "string"
          },
          "port": {
            "type": "string"
          },
          "password": {
            "type": "string"
          }
        }
      },
      "QueueConfigDto": {
        "type": "object",
        "properties": {
          "enabled": {
            "type": "boolean"
          }
        }
      },
      "StorageConfigDto": {
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "local",
              "s3"
            ]
          },
          "builtIn": {
            "type": "boolean"
          },
          "localPath": {
            "type": "string"
          },
          "s3Bucket": {
            "type": "string"
          },
          "s3Region": {
            "type": "string"
          },
          "s3AccessKey": {
            "type": "string"
          },
          "s3SecretKey": {
            "type": "string"
          },
          "s3Endpoint": {
            "type": "string"
          }
        },
        "required": [
          "type"
        ]
      },
      "EngineConfigDto": {
        "type": "object",
        "properties": {
          "type": {
            "type": "string"
          },
          "headless": {
            "type": "boolean"
          },
          "sessionDataPath": {
            "type": "string"
          },
          "browserArgs": {
            "type": "string"
          }
        }
      },
      "SaveConfigDto": {
        "type": "object",
        "properties": {
          "database": {
            "$ref": "#/components/schemas/DatabaseConfigDto"
          },
          "redis": {
            "$ref": "#/components/schemas/RedisConfigDto"
          },
          "queue": {
            "$ref": "#/components/schemas/QueueConfigDto"
          },
          "storage": {
            "$ref": "#/components/schemas/StorageConfigDto"
          },
          "engine": {
            "$ref": "#/components/schemas/EngineConfigDto"
          }
        }
      },
      "RestartDto": {
        "type": "object",
        "properties": {
          "profiles": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "profilesToRemove": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "UpsertContactDto": {
        "type": "object",
        "properties": {
          "firstName": {
            "type": "string",
            "description": "The contact's first name.",
            "maxLength": 100
          },
          "lastName": {
            "type": "string",
            "description": "The contact's last name. Omit for a single-name contact — WhatsApp allows those.",
            "maxLength": 100
          }
        },
        "required": [
          "firstName"
        ]
      },
      "JoinGroupDto": {
        "type": "object",
        "properties": {
          "inviteCode": {
            "type": "string",
            "description": "Group invite code (the token from a https://chat.whatsapp.com/<code> link)",
            "maxLength": 128
          }
        },
        "required": [
          "inviteCode"
        ]
      },
      "GroupSettingsDto": {
        "type": "object",
        "properties": {
          "announce": {
            "type": "boolean",
            "description": "Only admins can send messages (announce group)"
          },
          "locked": {
            "type": "boolean",
            "description": "Only admins can edit group info (locked group)"
          },
          "ephemeralSeconds": {
            "type": "number",
            "description": "Disappearing-messages timer in seconds; 0 disables. Known values: 86400 (24h), 604800 (7d), 7776000 (90d)",
            "minimum": 0
          },
          "memberAddMode": {
            "type": "string",
            "description": "Who may add participants: 'all' (any member) or 'admins' (admins only)",
            "enum": [
              "all",
              "admins"
            ]
          }
        }
      },
      "CreateGroupDto": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Group subject/name",
            "maxLength": 100
          },
          "participants": {
            "description": "Participant WhatsApp IDs (e.g. 628123456789@c.us)",
            "maxItems": 256,
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "name",
          "participants"
        ]
      },
      "ParticipantsDto": {
        "type": "object",
        "properties": {
          "participants": {
            "description": "Participant WhatsApp IDs (e.g. 628123456789@c.us)",
            "maxItems": 256,
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "participants"
        ]
      },
      "GroupSubjectDto": {
        "type": "object",
        "properties": {
          "subject": {
            "type": "string",
            "description": "New group subject/name",
            "maxLength": 100
          }
        },
        "required": [
          "subject"
        ]
      },
      "GroupDescriptionDto": {
        "type": "object",
        "properties": {
          "description": {
            "type": "string",
            "description": "New group description (may be empty to clear it)",
            "maxLength": 1024
          }
        },
        "required": [
          "description"
        ]
      },
      "SetGroupPictureDto": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "description": "Image URL (http/https)",
            "example": "https://example.com/group.jpg"
          },
          "base64": {
            "type": "string",
            "description": "Base64 encoded image data"
          },
          "mimetype": {
            "type": "string",
            "description": "Image MIME type (required when using base64)",
            "example": "image/jpeg"
          }
        }
      },
      "SetProfileNameDto": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "New display name (WhatsApp limit: 25 characters)",
            "maxLength": 25
          }
        },
        "required": [
          "name"
        ]
      },
      "SetProfileStatusDto": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "description": "New about/status text (may be empty to clear it; WhatsApp limit: 139 characters)",
            "maxLength": 139
          }
        },
        "required": [
          "status"
        ]
      },
      "SetProfilePictureDto": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "description": "Image URL (http/https)",
            "example": "https://example.com/avatar.jpg"
          },
          "base64": {
            "type": "string",
            "description": "Base64 encoded image data"
          },
          "mimetype": {
            "type": "string",
            "description": "Image MIME type (required when using base64)",
            "example": "image/jpeg"
          }
        }
      },
      "UpsertLabelDto": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Label name. Omit to leave the current name untouched.",
            "example": "VIP customer",
            "minLength": 1,
            "maxLength": 100
          },
          "color": {
            "type": "number",
            "description": "WhatsApp's colour INDEX (0-19), not a hex value — the read path's `hexColor` cannot be translated back, because neither engine exposes the mapping. Omit to leave the colour alone.",
            "example": 3,
            "minimum": 0,
            "maximum": 19
          }
        }
      },
      "CreateChannelDto": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Channel name",
            "example": "Product updates",
            "maxLength": 100
          },
          "description": {
            "type": "string",
            "description": "Channel description",
            "example": "Release notes and downtime notices"
          }
        },
        "required": [
          "name"
        ]
      },
      "MuteChannelDto": {
        "type": "object",
        "properties": {
          "mute": {
            "type": "boolean",
            "description": "True to mute the channel, false to unmute it. Subscription is unaffected either way.",
            "example": true
          }
        },
        "required": [
          "mute"
        ]
      },
      "SendTextStatusDto": {
        "type": "object",
        "properties": {
          "text": {
            "type": "string",
            "description": "Status text body.",
            "example": "Out for delivery 📦",
            "maxLength": 4096
          },
          "backgroundColor": {
            "type": "string",
            "description": "Background color (hex).",
            "example": "#25D366"
          },
          "font": {
            "type": "number",
            "description": "Font family index from the WhatsApp status font enum: 0 (default), 1, 2, 6 (bold), 7, 8, 9, or 10. whatsapp-web.js honors only 0–7 and clamps anything above to the default.",
            "example": 0,
            "enum": [
              0,
              1,
              2,
              6,
              7,
              8,
              9,
              10
            ]
          },
          "recipients": {
            "description": "Recipient JIDs (0–256). WhatsApp Status is not posted to a group — use @c.us or @lid individuals. Required on the Baileys engine (it posts to exactly this allow-list); ignored by whatsapp-web.js, which broadcasts to the account's status-privacy audience — omit it there.",
            "example": [
              "628123456789@c.us"
            ],
            "maxItems": 256,
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "text"
        ]
      },
      "StatusMediaInput": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "description": "Public http(s) URL of the media (server-fetched, SSRF-guarded).",
            "example": "https://example.com/banner.jpg"
          },
          "base64": {
            "type": "string",
            "description": "Base64-encoded media. Requires mimetype.",
            "example": "/9j/4AAQSkZJRg..."
          },
          "mimetype": {
            "type": "string",
            "description": "MIME type. Required when sending base64.",
            "example": "image/jpeg"
          }
        }
      },
      "SendImageStatusDto": {
        "type": "object",
        "properties": {
          "image": {
            "description": "Image source (URL or base64).",
            "allOf": [
              {
                "$ref": "#/components/schemas/StatusMediaInput"
              }
            ]
          },
          "caption": {
            "type": "string",
            "description": "Optional caption.",
            "example": "New drop!",
            "maxLength": 1024
          },
          "recipients": {
            "description": "Recipient JIDs (0–256), @c.us or @lid. Required on the Baileys engine (it posts to exactly this allow-list); ignored by whatsapp-web.js, which broadcasts to the account's status-privacy audience — omit it there.",
            "example": [
              "628123456789@c.us"
            ],
            "maxItems": 256,
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "image"
        ]
      },
      "SendVideoStatusDto": {
        "type": "object",
        "properties": {
          "video": {
            "description": "Video source (URL or base64).",
            "allOf": [
              {
                "$ref": "#/components/schemas/StatusMediaInput"
              }
            ]
          },
          "caption": {
            "type": "string",
            "description": "Optional caption.",
            "example": "Demo",
            "maxLength": 1024
          },
          "recipients": {
            "description": "Recipient JIDs (0–256), @c.us or @lid. Required on the Baileys engine (it posts to exactly this allow-list); ignored by whatsapp-web.js, which broadcasts to the account's status-privacy audience — omit it there.",
            "example": [
              "628123456789@c.us"
            ],
            "maxItems": 256,
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "video"
        ]
      },
      "SendVoiceStatusDto": {
        "type": "object",
        "properties": {
          "audio": {
            "description": "Audio source (URL or base64). WhatsApp plays a status voice note only as Ogg/Opus, and neither engine transcodes — use the media conversion endpoint to produce it. The mimetype defaults to 'audio/ogg; codecs=opus'.",
            "allOf": [
              {
                "$ref": "#/components/schemas/StatusMediaInput"
              }
            ]
          },
          "backgroundColor": {
            "type": "string",
            "description": "Background colour as `#RRGGBB`, which WhatsApp renders behind the voice-note bubble. Baileys only — whatsapp-web.js exposes no styling for a status and ignores it.",
            "example": "#25D366"
          },
          "recipients": {
            "description": "Recipient JIDs (0–256), @c.us or @lid. Required on the Baileys engine (it posts to exactly this allow-list); ignored by whatsapp-web.js, which broadcasts to the account's status-privacy audience — omit it there.",
            "example": [
              "628123456789@c.us"
            ],
            "maxItems": 256,
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "audio"
        ]
      },
      "ConvertMediaDto": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "description": "Public http(s) URL of the media to convert (server-fetched, SSRF-guarded).",
            "example": "https://example.com/note.m4a"
          },
          "base64": {
            "type": "string",
            "description": "Base64-encoded media to convert. Takes precedence when both are supplied.",
            "example": "SUQzBAAAAAAA..."
          }
        }
      },
      "SendProductDto": {
        "type": "object",
        "properties": {
          "chatId": {
            "type": "string",
            "description": "Chat to send the product card to (@c.us or @g.us).",
            "example": "628123456789@c.us"
          },
          "productId": {
            "type": "string",
            "description": "Catalog product id to send.",
            "example": "product-42"
          },
          "body": {
            "type": "string",
            "description": "Optional body text accompanying the product card.",
            "example": "Back in stock!"
          }
        },
        "required": [
          "chatId",
          "productId"
        ]
      },
      "SendCatalogDto": {
        "type": "object",
        "properties": {
          "chatId": {
            "type": "string",
            "description": "Chat to send the catalog to (@c.us or @g.us).",
            "example": "628123456789@c.us"
          },
          "body": {
            "type": "string",
            "description": "Optional body text accompanying the catalog.",
            "example": "Browse our full catalog"
          }
        },
        "required": [
          "chatId"
        ]
      },
      "InstallFromUrlDto": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "description": "HTTPS URL of the plugin .zip to download and install. Plain http:// is rejected: the package is executable code, so it must be integrity-protected in transit (hosts on private networks remain subject to the SSRF guard). Optional content pinning: append `#sha256=<64 hex>` (fragment — never sent to the server; query params are ignored) to require the downloaded archive to match that digest; a mismatch fails the install."
          }
        },
        "required": [
          "url"
        ]
      },
      "PluginConfigDto": {
        "type": "object",
        "properties": {
          "config": {
            "type": "object",
            "description": "Plugin configuration object"
          }
        },
        "required": [
          "config"
        ]
      },
      "PluginSessionsDto": {
        "type": "object",
        "properties": {
          "sessions": {
            "description": "Sessions to activate the plugin for; ['*'] = all numbers, [] = none",
            "example": [
              "*"
            ],
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "sessions"
        ]
      },
      "PluginDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Plugin ID"
          },
          "name": {
            "type": "string",
            "description": "Plugin name"
          },
          "version": {
            "type": "string",
            "description": "Plugin version"
          },
          "type": {
            "type": "string",
            "enum": [
              "engine",
              "storage",
              "queue",
              "auth",
              "extension"
            ],
            "description": "Plugin type"
          },
          "description": {
            "type": "string",
            "description": "Plugin description"
          },
          "author": {
            "type": "string",
            "description": "Plugin author"
          },
          "status": {
            "type": "string",
            "enum": [
              "installed",
              "enabled",
              "disabled",
              "error"
            ],
            "description": "Plugin status"
          },
          "config": {
            "type": "object",
            "description": "Plugin configuration"
          },
          "builtIn": {
            "type": "boolean",
            "description": "Whether this is a built-in plugin"
          },
          "provides": {
            "description": "Features provided by this plugin",
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "ingressCapable": {
            "type": "boolean",
            "description": "Whether this plugin can host provisioned ingress instances"
          },
          "sessionScoped": {
            "type": "boolean",
            "description": "Whether the plugin is scoped to specific sessions (false = global)"
          },
          "activeSessions": {
            "description": "Sessions this plugin is activated for; ['*'] = all numbers",
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "configSchema": {
            "type": "object",
            "description": "Configuration schema"
          },
          "configUi": {
            "type": "object",
            "description": "Sandboxed-iframe config editor (entry HTML + optional height)"
          },
          "i18n": {
            "type": "object",
            "description": "Localized dashboard text (name/description/config titles) per locale code"
          },
          "sessionConfig": {
            "type": "object",
            "description": "Per-session config overrides, keyed by sessionId (secrets redacted)"
          },
          "loadedAt": {
            "type": "string",
            "description": "When the plugin was loaded"
          },
          "enabledAt": {
            "type": "string",
            "description": "When the plugin was enabled"
          },
          "error": {
            "type": "string",
            "description": "Error message if plugin is in error state"
          }
        },
        "required": [
          "id",
          "name",
          "version",
          "type",
          "status",
          "config",
          "builtIn",
          "provides",
          "ingressCapable",
          "sessionScoped",
          "activeSessions"
        ]
      },
      "CreateInstanceDto": {
        "type": "object",
        "properties": {
          "instanceId": {
            "type": "string",
            "description": "Operator-chosen instance id (unique within the plugin). Namespaces the ingress URL and the instance secret.",
            "example": "chatwoot-prod-1"
          },
          "sessionScope": {
            "type": "string",
            "description": "Session id the instance is scoped to. Omit for all sessions.",
            "example": "8f3c2b1a-9d4e-4c7a-8b2f-1e6d5a4c3b2a"
          },
          "verifyToken": {
            "type": "string",
            "description": "Token echoed back for the provider webhook verification handshake. Auto-generated when omitted.",
            "example": "a1b2c3d4e5f6"
          },
          "secret": {
            "type": "string",
            "description": "Ingress HMAC secret shared with the provider. Omit to auto-generate a random 64-hex secret. Masked (****) on every read.",
            "writeOnly": true,
            "example": "super-secret-provider-webhook-key"
          },
          "config": {
            "type": "object",
            "description": "Per-instance config slice passed to the adapter (shape defined by the plugin).",
            "example": {
              "apiKey": "chatwoot-key",
              "inboxId": 42
            }
          }
        },
        "required": [
          "instanceId"
        ]
      },
      "IngressUrl": {
        "type": "object",
        "properties": {
          "route": {
            "type": "string",
            "description": "Plugin-declared route segment the provider posts to."
          },
          "url": {
            "type": "string",
            "description": "Full ingress URL (BASE_URL + plugin/instance/route), or a relative path when BASE_URL is unset."
          }
        },
        "required": [
          "route",
          "url"
        ]
      },
      "InstanceView": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Instance row id."
          },
          "pluginId": {
            "type": "string",
            "description": "Plugin id this instance belongs to."
          },
          "instanceId": {
            "type": "string",
            "description": "Operator-chosen instance id (unique within the plugin)."
          },
          "sessionScope": {
            "type": "object",
            "description": "Session id the instance is scoped to, or null for all sessions.",
            "nullable": true
          },
          "secret": {
            "type": "string",
            "description": "Ingress HMAC secret. Masked ('***') on every read; plaintext returned only once on create/regenerate-secret."
          },
          "verifyToken": {
            "type": "object",
            "description": "Provider verify-token. Masked ('***') on reads when set; plaintext on create/regenerate-secret.",
            "nullable": true
          },
          "config": {
            "type": "object",
            "description": "Per-instance config slice passed to the adapter, or null. Fields flagged `secret` in the plugin's config schema are masked ('***') on EVERY response — including create/regenerate-secret; only the ingress secret and verifyToken are ever revealed once.",
            "nullable": true
          },
          "enabled": {
            "type": "boolean",
            "description": "Whether ingress is accepted and dispatch is active."
          },
          "createdAt": {
            "format": "date-time",
            "type": "string",
            "description": "Creation timestamp."
          },
          "updatedAt": {
            "format": "date-time",
            "type": "string",
            "description": "Last update timestamp."
          },
          "ingressUrls": {
            "description": "Ingress URLs the provider posts webhook deliveries to.",
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/IngressUrl"
            }
          }
        },
        "required": [
          "id",
          "pluginId",
          "instanceId",
          "secret",
          "enabled",
          "createdAt",
          "updatedAt",
          "ingressUrls"
        ]
      },
      "UpdateInstanceDto": {
        "type": "object",
        "properties": {
          "enabled": {
            "type": "boolean",
            "description": "Whether the instance is enabled (ingress accepted, dispatch active).",
            "example": true
          },
          "sessionScope": {
            "type": "string",
            "description": "Session id the instance is scoped to. Omit for all sessions."
          },
          "config": {
            "type": "object",
            "description": "Per-instance config slice passed to the adapter."
          }
        }
      }
    }
  },
  "security": [
    {
      "X-API-Key": []
    }
  ]
}
