GitHub Raw JSON API

rmyndharis / OpenWA

12,602 TypeScript

Free, Open Source, Self-Hosted WhatsApp API Gateway

OpenWA API Specification

Located in openapi.json on branch HEAD

3.0.0 JSON 158 Endpoints 498.8 KB

Operations & Route Endpoints (100)

GET /api/audit
List audit logs with optional filters
POST /api/auth/api-keys
Create a new API key (admin only)
GET /api/auth/api-keys
List all API keys (admin only)
GET /api/auth/api-keys/{id}
Get API key details (admin only)
PUT /api/auth/api-keys/{id}
Update API key (admin only)
DELETE /api/auth/api-keys/{id}
Delete API key (admin only)
POST /api/auth/api-keys/{id}/revoke
Revoke API key (admin only)
POST /api/auth/validate
Validate an API key
POST /api/sessions
Create a new WhatsApp session
GET /api/sessions
List all sessions
GET /api/sessions/{id}
Get session by ID
DELETE /api/sessions/{id}
Delete a session
GET /api/sessions/{id}/config
Get the tunable configuration for a session
PATCH /api/sessions/{id}/config
Update the tunable configuration for a session
POST /api/sessions/{id}/start
Start a session and initialize WhatsApp connection
POST /api/sessions/{id}/stop
Stop a session and disconnect WhatsApp
POST /api/sessions/{id}/logout
Log out of WhatsApp (unlinks this device) and stop the session
POST /api/sessions/{id}/force-kill
Force-kill a stuck session (SIGKILL its wedged engine, then tear it down)
GET /api/sessions/{id}/qr
Get QR code for session authentication
POST /api/sessions/{id}/pairing-code
Request an 8-char pairing code to link via phone number (alternative to QR)
GET /api/sessions/{sessionId}/groups
Get all groups for a session
POST /api/sessions/{sessionId}/groups
Create a new group
GET /api/sessions/{id}/chats
Get active chats for a session
POST /api/sessions/{id}/chats/read
Mark a chat as read/seen
POST /api/sessions/{id}/presence/subscribe
Subscribe to a chat's presence
PUT /api/sessions/{id}/presence
Set the account's own global presence (appear online or offline)
GET /api/sessions/{id}/presence/{chatId}
Read a chat's last reported presence
POST /api/sessions/{id}/chats/unread
Mark a chat as unread
DELETE /api/sessions/{id}/chats/{chatId}/messages
Delete every message in a chat, keeping the chat itself
POST /api/sessions/{id}/chats/archive
Archive or unarchive a chat
POST /api/sessions/{id}/chats/mute
Mute or unmute a chat
POST /api/sessions/{id}/chats/pin
Pin or unpin a chat at the top of the chat list
POST /api/sessions/{id}/chats/delete
Delete a chat from the chat list (e.g. a group you have left)
POST /api/sessions/{id}/chats/typing
Send a typing/recording presence indicator to a chat (or clear it with 'paused')
GET /api/sessions/stats/overview
Get session statistics for multi-session monitoring
POST /api/sessions/{sessionId}/webhooks
Create a webhook for the session
GET /api/sessions/{sessionId}/webhooks
List all webhooks for a session
GET /api/sessions/{sessionId}/webhooks/{id}
Get a webhook by ID
PUT /api/sessions/{sessionId}/webhooks/{id}
Update a webhook
DELETE /api/sessions/{sessionId}/webhooks/{id}
Delete a webhook
POST /api/sessions/{sessionId}/webhooks/{id}/test
Test a webhook by sending a test payload
GET /api/webhooks/delivery-failures
List recently-failed webhook deliveries (all retries exhausted)
GET /api/webhooks
List webhooks visible to the calling key (scoped to its allowed sessions)
POST /api/sessions/{sessionId}/automation-rules
Create an autoreply rule
GET /api/sessions/{sessionId}/automation-rules
List the session’s autoreply rules
GET /api/sessions/{sessionId}/automation-rules/{ruleId}
Get one autoreply rule
PUT /api/sessions/{sessionId}/automation-rules/{ruleId}
Update an autoreply rule
DELETE /api/sessions/{sessionId}/automation-rules/{ruleId}
Delete an autoreply rule
GET /api/sessions/{sessionId}/messages
Get message history for a session
POST /api/sessions/{sessionId}/messages/send-text
Send a text message
POST /api/sessions/{sessionId}/messages/send-template
Render a stored text template and send it as a text message
POST /api/sessions/{sessionId}/messages/send-image
Send an image message
POST /api/sessions/{sessionId}/messages/send-video
Send a video message
POST /api/sessions/{sessionId}/messages/send-audio
Send an audio/voice message
POST /api/sessions/{sessionId}/messages/send-document
Send a document/file
POST /api/sessions/{sessionId}/messages/send-location
Send a location message
POST /api/sessions/{sessionId}/messages/send-contact
Send a contact card message
POST /api/sessions/{sessionId}/messages/send-sticker
Send a sticker message
POST /api/sessions/{sessionId}/messages/send-poll
Send a native WhatsApp poll
POST /api/sessions/{sessionId}/messages/reply
Reply to a message
POST /api/sessions/{sessionId}/messages/forward
Forward a message to another chat
POST /api/sessions/{sessionId}/messages/react
Add or remove a reaction to a message
GET /api/sessions/{sessionId}/messages/{chatId}/history
Fetch chat history live from WhatsApp
GET /api/sessions/{sessionId}/messages/{chatId}/{messageId}/reactions
Get reactions for a specific message
GET /api/sessions/{sessionId}/messages/{chatId}/{messageId}/media
Download a message’s stored media
POST /api/sessions/{sessionId}/messages/delete
Delete a message
POST /api/sessions/{sessionId}/messages/vote-poll
Cast a vote on a poll
POST /api/sessions/{sessionId}/messages/pin
Pin a message in its chat
POST /api/sessions/{sessionId}/messages/unpin
Remove a message’s pin
POST /api/sessions/{sessionId}/messages/star
Star or unstar a message
POST /api/sessions/{sessionId}/messages/edit
Edit the text of a message sent by this account
POST /api/sessions/{sessionId}/messages/send-bulk
Send messages to multiple recipients (async batch processing)
GET /api/sessions/{sessionId}/messages/batch/{batchId}
Get batch processing status
POST /api/sessions/{sessionId}/messages/batch/{batchId}/cancel
Cancel a running batch
POST /api/sessions/{sessionId}/templates
Create a message template for the session
GET /api/sessions/{sessionId}/templates
List all templates for a session
GET /api/sessions/{sessionId}/templates/{id}
Get a template by ID
PUT /api/sessions/{sessionId}/templates/{id}
Update a template
DELETE /api/sessions/{sessionId}/templates/{id}
Delete a template
GET /api/health
Basic health check
GET /api/health/live
Liveness probe for Kubernetes
GET /api/health/ready
Readiness probe β€” verifies the auth/audit + data databases respond
GET /api/settings
Get application settings
PUT /api/settings
Settings are read-only at runtime (environment-derived)
GET /api/infra/status
Get infrastructure status
GET /api/infra/engines
Get available WhatsApp engines
GET /api/infra/engines/current
Get current active engine
GET /api/infra/health
Health check endpoint
GET /api/infra/config
Read the saved infrastructure configuration for the dashboard form
PUT /api/infra/config
Save infrastructure configuration to .env file
POST /api/infra/restart
Request server restart with Docker orchestration
GET /api/infra/export-data
Export all data from Data DB for migration
POST /api/infra/import-data
Import data to Data DB (replaces existing data)
GET /api/infra/storage/files/count
Get file count in current storage
GET /api/infra/storage/export
Export all storage files as tar.gz
POST /api/infra/storage/import
Import storage files from tar.gz
GET /api/sessions/{sessionId}/contacts
Get all contacts for a session
GET /api/sessions/{sessionId}/contacts/profile-pictures
Batch-resolve profile picture URLs for up to 50 contacts
GET /api/sessions/{sessionId}/contacts/blocked
List the contacts this account has blocked
GET /api/sessions/{sessionId}/contacts/{contactId}
Get a specific contact by ID
Raw JSON Specification
{
  "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_config_updated",
                "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",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AuditListResponseDto"
                }
              }
            }
          }
        },
        "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}/config": {
      "get": {
        "operationId": "SessionController_getConfig",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Effective session configuration",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SessionConfigResponseDto"
                }
              }
            }
          },
          "404": {
            "description": "Session not found"
          }
        },
        "summary": "Get the tunable configuration for a session",
        "tags": [
          "sessions"
        ]
      },
      "patch": {
        "description": "Merges the supplied keys into the session config; omitted keys are left unchanged and an explicit null clears a key back to its default. No restart is required or performed. `autoRejectCalls` is re-read on every incoming call, so it applies immediately; the two reconnect settings are read once per start and therefore apply on the next start.",
        "operationId": "SessionController_updateConfig",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateSessionConfigDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated session configuration",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SessionConfigResponseDto"
                }
              }
            }
          },
          "400": {
            "description": "A supplied value is outside its accepted range"
          },
          "404": {
            "description": "Session not found"
          }
        },
        "summary": "Update the tunable configuration for 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": "John Doe",
                  "connectedAt": "2026-06-24T08:15:00.000Z",
                  "lastActive": "2026-06-25T09:01:55.000Z",
                  "createdAt": "2026-06-20T11:30:00.000Z",
                  "updatedAt": "2026-06-25T09:11:00.000Z",
                  "lastError": null,
                  "restriction": null,
                  "engineLoaded": false
                }
              }
            }
          },
          "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"
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          }
        },
        "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"
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          }
        },
        "summary": "Request an 8-char pairing code to link via phone number (alternative to QR)",
        "tags": [
          "sessions"
        ]
      }
    },
    "/api/sessions/{sessionId}/groups": {
      "get": {
        "operationId": "SessionController_getGroups",
        "parameters": [
          {
            "name": "sessionId",
            "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"
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "503": {
            "description": "WhatsApp did not answer the group-list query. Deliberately not reported as an empty list β€” the engine returns the same empty value for \"you are in no groups\", and a caller cannot tell those apart from the body."
          }
        },
        "summary": "Get all groups for a session",
        "tags": [
          "sessions"
        ]
      },
      "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",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GroupSummaryDto"
                }
              }
            }
          },
          "400": {
            "description": "A participant does not name an individual. Pass a phone number, `<phone>@c.us` or `<lid>@lid`; a group id or free text is rejected."
          },
          "403": {
            "description": "WhatsApp refused the operation. The request was well formed β€” the refusal happened WhatsApp-side, most often because the account lacks the admin rights the operation requires."
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "501": {
            "description": "Not supported by the active engine: whatsapp-web.js reaches a WhatsApp Web internal that no longer exists, so group creation is Baileys-only."
          }
        },
        "summary": "Create a new group",
        "tags": [
          "groups"
        ]
      }
    },
    "/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"
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "503": {
            "description": "The whatsapp-web.js page connection died mid-read, so nothing could be read. Deliberately not reported as an empty list β€” a page that went away says nothing about the chats."
          }
        },
        "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"
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "503": {
            "description": "WhatsApp did not answer within the request budget. The change may or may not have been applied β€” the gateway stopped waiting for a confirmation that never came."
          }
        },
        "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"
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "501": {
            "description": "The active engine cannot observe presence (whatsapp-web.js)"
          }
        },
        "summary": "Subscribe to a chat's presence",
        "tags": [
          "sessions"
        ]
      }
    },
    "/api/sessions/{id}/presence": {
      "put": {
        "description": "Publishes whether this account appears online. WhatsApp routes notifications away from the phone while a linked device announces itself online, so a headless bot that never goes offline suppresses the phone's own alerts β€” set `available: false` to hand them back.\n\nThe setting belongs to the connection: it does not survive a restart or reconnect and must be re-issued after `session.status` reports one (on Baileys the socket re-announces itself per its connect-time behaviour). Supported on both engines.",
        "operationId": "SessionController_setOnlinePresence",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SetOwnPresenceDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Presence published"
          },
          "400": {
            "description": "Session not started, or validation failed"
          },
          "404": {
            "description": "Session not found"
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          }
        },
        "summary": "Set the account's own global presence (appear online or offline)",
        "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"
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "503": {
            "description": "WhatsApp did not answer within the request budget. The change may or may not have been applied β€” the gateway stopped waiting for a confirmation that never came."
          }
        },
        "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. [email protected] (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"
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "503": {
            "description": "WhatsApp did not answer within the request budget. The change may or may not have been applied β€” the gateway stopped waiting for a confirmation that never came."
          }
        },
        "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"
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "503": {
            "description": "WhatsApp did not answer within the request budget. The change may or may not have been applied β€” the gateway stopped waiting for a confirmation that never came."
          }
        },
        "summary": "Archive or unarchive a chat",
        "tags": [
          "sessions"
        ]
      }
    },
    "/api/sessions/{id}/chats/mute": {
      "post": {
        "operationId": "SessionController_muteChat",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MuteChatDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Returns `{ success: true }`. Unlike the archive route there is no declined outcome: the mute change is not keyed to the chat's last message on either engine, so a chat with no known history mutes like any other."
          },
          "400": {
            "description": "Session not ready, or an invalid chatId / muteUntil"
          },
          "404": {
            "description": "Session not found"
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "503": {
            "description": "WhatsApp did not answer within the request budget. The change may or may not have been applied β€” the gateway stopped waiting for a confirmation that never came."
          }
        },
        "summary": "Mute or unmute a chat",
        "tags": [
          "sessions"
        ]
      }
    },
    "/api/sessions/{id}/chats/pin": {
      "post": {
        "operationId": "SessionController_pinChat",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PinChatDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Returns `{ success }`. `false` means the engine declined, and only a pin can: WhatsApp allows at most three pinned chats and the whatsapp-web.js engine reports the refusal. Unpinning always succeeds, and the Baileys engine always reports success because it cannot observe the cap."
          },
          "400": {
            "description": "Session not ready, or a chatId the session cannot resolve. An unknown chat is reported here rather than as `success: false`, which on this route means only that the three-pin cap refused a real chat. The Baileys engine cannot resolve chats ahead of the write and answers `success: true` for an unknown chat."
          },
          "404": {
            "description": "Session not found"
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "503": {
            "description": "WhatsApp did not answer within the request budget. The change may or may not have been applied β€” the gateway stopped waiting for a confirmation that never came."
          }
        },
        "summary": "Pin or unpin a chat at the top of the chat list",
        "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"
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "503": {
            "description": "WhatsApp did not answer within the request budget. The change may or may not have been applied β€” the gateway stopped waiting for a confirmation that never came."
          }
        },
        "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"
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          }
        },
        "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.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/AutomationRuleResponseDto"
                  }
                }
              }
            }
          }
        },
        "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": "[email protected]",
                    "text": "Hello from OpenWA!"
                  }
                },
                "withMentions": {
                  "summary": "Group message with an @mention (the text must carry the @<number> token)",
                  "value": {
                    "chatId": "[email protected]",
                    "text": "Hello @62811",
                    "mentions": [
                      "[email protected]"
                    ]
                  }
                }
              }
            }
          }
        },
        "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"
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "501": {
            "description": "A caller-supplied `customLinkPreview` is not supported by the whatsapp-web.js engine β€” only Baileys can attach one. The send itself is supported on both; omit the field, or use `linkPreview`."
          }
        },
        "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"
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          }
        },
        "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": "[email protected]",
                    "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"
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "501": {
            "description": "Sending media to a channel (`<id>@newsletter`) is not supported by the whatsapp-web.js engine β€” the page method it needs was removed by a WhatsApp Web update. Text to a channel still works."
          }
        },
        "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": "[email protected]",
                    "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"
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "501": {
            "description": "Sending media to a channel (`<id>@newsletter`) is not supported by the whatsapp-web.js engine β€” the page method it needs was removed by a WhatsApp Web update. Text to a channel still works."
          }
        },
        "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": "[email protected]",
                    "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"
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "501": {
            "description": "Sending media to a channel (`<id>@newsletter`) is not supported by the whatsapp-web.js engine β€” the page method it needs was removed by a WhatsApp Web update. Text to a channel still works."
          }
        },
        "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": "[email protected]",
                    "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"
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "501": {
            "description": "Sending media to a channel (`<id>@newsletter`) is not supported by the whatsapp-web.js engine β€” the page method it needs was removed by a WhatsApp Web update. Text to a channel still works."
          }
        },
        "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"
              },
              "examples": {
                "minimal": {
                  "summary": "Share a location",
                  "value": {
                    "chatId": "[email protected]",
                    "latitude": -6.2088,
                    "longitude": 106.8456,
                    "description": "Jakarta"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Location sent",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MessageResponseDto"
                }
              }
            }
          },
          "400": {
            "description": "The request was rejected before anything was sent. Among the causes: the recipient could not be addressed β€” WhatsApp reports no deliverable id for that `chatId`, which is how it says the number is not on WhatsApp β€” as well as body validation and a session that is not active."
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          }
        },
        "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"
              },
              "examples": {
                "minimal": {
                  "summary": "Share a contact card",
                  "value": {
                    "chatId": "[email protected]",
                    "contactName": "Alice",
                    "contactNumber": "628999888777"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Contact sent",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MessageResponseDto"
                }
              }
            }
          },
          "400": {
            "description": "The request was rejected before anything was sent. Among the causes: the recipient could not be addressed β€” WhatsApp reports no deliverable id for that `chatId`, which is how it says the number is not on WhatsApp β€” as well as body validation and a session that is not active."
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          }
        },
        "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": "[email protected]",
                    "url": "https://example.com/sticker.webp"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Sticker sent",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MessageResponseDto"
                }
              }
            }
          },
          "400": {
            "description": "The request was rejected before anything was sent. Among the causes: the recipient could not be addressed β€” WhatsApp reports no deliverable id for that `chatId`, which is how it says the number is not on WhatsApp β€” as well as body validation and a session that is not active."
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "501": {
            "description": "Sending media to a channel (`<id>@newsletter`) is not supported by the whatsapp-web.js engine β€” the page method it needs was removed by a WhatsApp Web update. Text to a channel still works."
          }
        },
        "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"
              },
              "examples": {
                "minimal": {
                  "summary": "Ask a single-choice poll",
                  "value": {
                    "chatId": "[email protected]",
                    "name": "Where should we meet?",
                    "options": [
                      "Park",
                      "Beach"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Poll sent",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MessageResponseDto"
                }
              }
            }
          },
          "400": {
            "description": "The request was rejected before anything was sent. Among the causes: the recipient could not be addressed β€” WhatsApp reports no deliverable id for that `chatId`, which is how it says the number is not on WhatsApp β€” as well as body validation and a session that is not active."
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          }
        },
        "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"
                }
              }
            }
          },
          "400": {
            "description": "The request was rejected before anything was sent. Among the causes: the recipient could not be addressed β€” WhatsApp reports no deliverable id for that `chatId`, which is how it says the number is not on WhatsApp β€” as well as body validation and a session that is not active."
          },
          "404": {
            "description": "No such message β€” the id is outside the engine's lookup window (roughly the last hundred messages of the chat, or absent from the Baileys store) or the message was revoked."
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          }
        },
        "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"
                }
              }
            }
          },
          "400": {
            "description": "The request was rejected before anything was sent. Among the causes: the recipient could not be addressed β€” WhatsApp reports no deliverable id for that `chatId`, which is how it says the number is not on WhatsApp β€” as well as body validation and a session that is not active."
          },
          "404": {
            "description": "No such message β€” the id is outside the engine's lookup window (roughly the last hundred messages of the chat, or absent from the Baileys store) or the message was revoked."
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          }
        },
        "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"
          },
          "404": {
            "description": "No such message β€” the id is outside the engine's lookup window (roughly the last hundred messages of the chat, or absent from the Baileys store) or the message was revoked."
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          }
        },
        "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. [email protected] or [email protected])",
            "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)"
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "501": {
            "description": "The active engine cannot serve this operation. It is part of the engine contract, but the engine currently running has no implementation for it."
          }
        },
        "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"
          },
          "404": {
            "description": "No such message β€” the id is outside the engine's lookup window (roughly the last hundred messages of the chat, or absent from the Baileys store) or the message was revoked."
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "501": {
            "description": "The active engine cannot serve this operation. It is part of the engine contract, but the engine currently running has no implementation for it."
          }
        },
        "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 media bytes β€” the archived file when one exists, else the inline copy stored on the message row (which is how media sent by this account is served) β€” as an attachment.",
            "content": {
              "application/octet-stream": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "404": {
            "description": "No stored media for this message β€” it carries no media, media download was disabled or the payload was over the cap when it was stored (size-only marker), it was a URL-based API send (those bytes are never stored), or the message is not in this gateway’s history."
          }
        },
        "summary": "Download a message’s stored 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"
          },
          "404": {
            "description": "No such message β€” the id is outside the engine's lookup window (roughly the last hundred messages of the chat, or absent from the Baileys store) or the message was revoked."
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "503": {
            "description": "WhatsApp did not answer within the request budget. The change may or may not have been applied β€” the gateway stopped waiting for a confirmation that never came."
          }
        },
        "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"
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "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"
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          }
        },
        "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"
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          }
        },
        "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"
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "503": {
            "description": "WhatsApp did not answer within the request budget. The change may or may not have been applied β€” the gateway stopped waiting for a confirmation that never came."
          }
        },
        "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"
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          }
        },
        "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",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HealthCheckResponseDto"
                }
              }
            }
          }
        },
        "summary": "Basic health check",
        "tags": [
          "health"
        ],
        "security": []
      }
    },
    "/api/health/live": {
      "get": {
        "operationId": "HealthController_liveness",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Application is alive",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LivenessResponseDto"
                }
              }
            }
          }
        },
        "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",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReadinessResponseDto"
                }
              }
            }
          },
          "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",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SettingsResponseDto"
                }
              }
            }
          }
        },
        "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",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InfraStatusResponseDto"
                }
              }
            }
          }
        },
        "summary": "Get infrastructure status",
        "tags": [
          "infrastructure"
        ]
      }
    },
    "/api/infra/engines": {
      "get": {
        "operationId": "InfraStatusController_getEngines",
        "parameters": [],
        "responses": {
          "200": {
            "description": "List of available engines",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/AvailableEngineDto"
                  }
                }
              }
            }
          }
        },
        "summary": "Get available WhatsApp engines",
        "tags": [
          "infrastructure"
        ]
      }
    },
    "/api/infra/engines/current": {
      "get": {
        "operationId": "InfraStatusController_getCurrentEngine",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Current engine info",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InfraCurrentEngineResponseDto"
                }
              }
            }
          }
        },
        "summary": "Get current active engine",
        "tags": [
          "infrastructure"
        ]
      }
    },
    "/api/infra/health": {
      "get": {
        "operationId": "InfraStatusController_healthCheck",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Process is up. This route does not probe dependencies β€” read /infra/status for those.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InfraHealthResponseDto"
                }
              }
            }
          }
        },
        "summary": "Health check endpoint",
        "tags": [
          "infrastructure"
        ],
        "security": []
      }
    },
    "/api/infra/config": {
      "get": {
        "operationId": "InfraConfigController_getConfig",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Saved configuration (secrets omitted)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InfraConfigResponseDto"
                }
              }
            }
          }
        },
        "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": "Save outcome. A failed write also answers 200 with `saved: false` β€” read the flag, not the status.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InfraConfigSaveResponseDto"
                }
              }
            }
          }
        },
        "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",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InfraRestartResponseDto"
                }
              }
            }
          }
        },
        "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",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InfraExportDataResponseDto"
                }
              }
            }
          }
        },
        "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": {
                "$ref": "#/components/schemas/ImportDataDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Data imported successfully",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InfraImportDataResponseDto"
                }
              }
            }
          },
          "400": {
            "description": "Body rejected before the restore ran: `tables` absent or not an object, a table whose value is not an array of rows, a row that is not an object (`null`, a bare string, a nested array), a flag spelled as anything but a boolean or exact `true`/`false`, or a property this route does not accept. Nothing was written. Field-level detail is suppressed in production unless VALIDATION_ERROR_DETAIL=true."
          },
          "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",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StorageFileCountResponseDto"
                }
              }
            }
          }
        },
        "summary": "Get file count in current storage",
        "tags": [
          "infrastructure"
        ]
      }
    },
    "/api/infra/storage/export": {
      "get": {
        "operationId": "InfraStorageController_exportStorage",
        "parameters": [],
        "responses": {
          "200": {
            "description": "JSON pointing at the archive that was written under data/exports/. This route does NOT stream the tar.gz itself β€” fetch it from the returned `download` path.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StorageExportResponseDto"
                }
              }
            }
          }
        },
        "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": {
                "$ref": "#/components/schemas/ImportStorageDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Import result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StorageImportResponseDto"
                }
              }
            }
          }
        },
        "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, windowed by limit/offset. A bare array β€” there is no envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ContactDto"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Session not ready"
          },
          "404": {
            "description": "Session not found"
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          }
        },
        "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 5 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": "Picture URL per requested id",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProfilePicturesResponseDto"
                }
              }
            }
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          }
        },
        "summary": "Batch-resolve profile picture URLs for up to 50 contacts",
        "tags": [
          "contacts"
        ]
      }
    },
    "/api/sessions/{sessionId}/contacts/blocked": {
      "get": {
        "description": "The read half of the block/unblock endpoints. A bare array of neutral contact ids β€” ids only, because that is the honest common subset: whatsapp-web.js resolves full contact models but Baileys' blocklist query answers bare jids, and inventing the other fields on one engine would make the two engines claim different things about the same account.",
        "operationId": "ContactController_getBlockedContacts",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Blocked contact ids",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                }
              }
            }
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "503": {
            "description": "WhatsApp did not answer the blocklist query β€” retry shortly"
          }
        },
        "summary": "List the contacts this account has blocked",
        "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., [email protected])",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Contact details",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ContactDto"
                }
              }
            }
          },
          "404": {
            "description": "Contact not found"
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          }
        },
        "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., [email protected])",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpsertContactDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Contact saved",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ContactAckResponseDto"
                }
              }
            }
          },
          "400": {
            "description": "Session not active or invalid request"
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "503": {
            "description": "WhatsApp did not answer within the request budget. The change may or may not have been applied β€” the gateway stopped waiting for a confirmation that never came."
          }
        },
        "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., [email protected])",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Contact deleted",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ContactAckResponseDto"
                }
              }
            }
          },
          "400": {
            "description": "Session not active"
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "503": {
            "description": "WhatsApp did not answer within the request budget. The change may or may not have been applied β€” the gateway stopped waiting for a confirmation that never came."
          }
        },
        "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",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NumberCheckResponseDto"
                }
              }
            }
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "503": {
            "description": "WhatsApp did not answer the lookup. Deliberately not reported as `exists: false` β€” that would be a claim about the number rather than about the query, and this route exists to be trusted before a send."
          }
        },
        "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., [email protected])",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Profile picture URL",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProfilePictureResponseDto"
                }
              }
            }
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "503": {
            "description": "WhatsApp did not answer the lookup. Deliberately not reported as `url: null` β€” that is the same answer a contact with no picture gives, and a caller cannot tell them apart."
          }
        },
        "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",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResolvedPhoneResponseDto"
                }
              }
            }
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          }
        },
        "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., [email protected])",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Contact blocked",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ContactAckResponseDto"
                }
              }
            }
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "503": {
            "description": "WhatsApp did not answer within the request budget. The change may or may not have been applied β€” the gateway stopped waiting for a confirmation that never came."
          }
        },
        "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., [email protected])",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Contact unblocked",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ContactAckResponseDto"
                }
              }
            }
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "503": {
            "description": "WhatsApp did not answer within the request budget. The change may or may not have been applied β€” the gateway stopped waiting for a confirmation that never came."
          }
        },
        "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",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GroupJoinInfoDto"
                }
              }
            }
          },
          "400": {
            "description": "Session not started, or no code supplied"
          },
          "404": {
            "description": "No such invite β€” invalid, expired or revoked"
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "503": {
            "description": "WhatsApp did not answer within the request budget. Deliberately not folded into the 404 above β€” a query that never came back is not the same claim as a group that does not exist."
          }
        },
        "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., [email protected])",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Group details with participants",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GroupInfoDto"
                }
              }
            }
          },
          "404": {
            "description": "Group not found"
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "503": {
            "description": "WhatsApp did not answer within the request budget. Deliberately not folded into the 404 above β€” a query that never came back is not the same claim as a group that does not exist."
          }
        },
        "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",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GroupJoinedResponseDto"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or expired invite code, or session is not started"
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "503": {
            "description": "WhatsApp did not answer within the request budget. The change may or may not have been applied β€” the gateway stopped waiting for a confirmation that never came."
          }
        },
        "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",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GroupSettingsResponseDto"
                }
              }
            }
          },
          "404": {
            "description": "Group not found"
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "503": {
            "description": "WhatsApp did not answer within the request budget β€” nothing could be read."
          }
        },
        "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",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GroupAckResponseDto"
                }
              }
            }
          },
          "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"
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "501": {
            "description": "The active engine does not support a requested setting"
          },
          "503": {
            "description": "WhatsApp did not answer within the request budget. The change may or may not have been applied β€” the gateway stopped waiting for a confirmation that never came."
          }
        },
        "summary": "Update group settings (announce / locked / ephemeral timer)",
        "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)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ParticipantsOperationResponseDto"
                }
              }
            }
          },
          "400": {
            "description": "A participant does not name an individual. Pass a phone number, `<phone>@c.us` or `<lid>@lid`; a group id or free text is rejected."
          },
          "403": {
            "description": "WhatsApp refused the operation. The request was well formed β€” the refusal happened WhatsApp-side, most often because the account lacks the admin rights the operation requires."
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "503": {
            "description": "WhatsApp did not answer within the request budget, so no per-participant outcome was read at all. Deliberately not folded into the 200 above β€” a participant WhatsApp turned down is reported inside `results` and is an answer; an update that never came back is not."
          }
        },
        "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)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ParticipantsOperationResponseDto"
                }
              }
            }
          },
          "400": {
            "description": "A participant does not name an individual. Pass a phone number, `<phone>@c.us` or `<lid>@lid`; a group id or free text is rejected."
          },
          "403": {
            "description": "WhatsApp refused the operation. The request was well formed β€” the refusal happened WhatsApp-side, most often because the account lacks the admin rights the operation requires."
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "503": {
            "description": "WhatsApp did not answer within the request budget, so no per-participant outcome was read at all. Deliberately not folded into the 200 above β€” a participant WhatsApp turned down is reported inside `results` and is an answer; an update that never came back is not."
          }
        },
        "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)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ParticipantsOperationResponseDto"
                }
              }
            }
          },
          "400": {
            "description": "A participant does not name an individual. Pass a phone number, `<phone>@c.us` or `<lid>@lid`; a group id or free text is rejected."
          },
          "403": {
            "description": "WhatsApp refused the operation. The request was well formed β€” the refusal happened WhatsApp-side, most often because the account lacks the admin rights the operation requires."
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "503": {
            "description": "WhatsApp did not answer within the request budget, so no per-participant outcome was read at all. Deliberately not folded into the 200 above β€” a participant WhatsApp turned down is reported inside `results` and is an answer; an update that never came back is not."
          }
        },
        "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)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ParticipantsOperationResponseDto"
                }
              }
            }
          },
          "400": {
            "description": "A participant does not name an individual. Pass a phone number, `<phone>@c.us` or `<lid>@lid`; a group id or free text is rejected."
          },
          "403": {
            "description": "WhatsApp refused the operation. The request was well formed β€” the refusal happened WhatsApp-side, most often because the account lacks the admin rights the operation requires."
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "503": {
            "description": "WhatsApp did not answer within the request budget, so no per-participant outcome was read at all. Deliberately not folded into the 200 above β€” a participant WhatsApp turned down is reported inside `results` and is an answer; an update that never came back is not."
          }
        },
        "summary": "Demote participants from admin",
        "tags": [
          "groups"
        ]
      }
    },
    "/api/sessions/{sessionId}/groups/{groupId}/membership-requests": {
      "get": {
        "description": "The join-approval queue of a group the account administers (join-approval mode on). Admin-only on both engines β€” a non-admin read is refused. Fields the engine does not report are omitted rather than defaulted.",
        "operationId": "GroupController_getMembershipRequests",
        "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": "Pending membership requests",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/GroupMembershipRequestDto"
                  }
                }
              }
            }
          },
          "403": {
            "description": "WhatsApp refused the operation. The request was well formed β€” the refusal happened WhatsApp-side, most often because the account lacks the admin rights the operation requires."
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "503": {
            "description": "WhatsApp did not answer within the request budget β€” retry shortly"
          }
        },
        "summary": "List pending join requests for a group",
        "tags": [
          "groups"
        ]
      }
    },
    "/api/sessions/{sessionId}/groups/{groupId}/membership-requests/approve": {
      "post": {
        "description": "Approves the named requesters, or EVERY pending request when the body names none. Approving an empty queue is a no-op that returns an empty results list. On whatsapp-web.js the engine pauses 250-500ms between requesters (upstream anti-abuse pacing), so acting on a large queue is a proportionally long request.",
        "operationId": "GroupController_approveMembershipRequests",
        "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/MembershipRequestActionDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Requests processed β€” `results` carries the per-participant outcome (a partial refusal does not fail the batch; a total refusal of NAMED requesters is an error)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ParticipantsOperationResponseDto"
                }
              }
            }
          },
          "403": {
            "description": "WhatsApp refused the operation. The request was well formed β€” the refusal happened WhatsApp-side, most often because the account lacks the admin rights the operation requires."
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "503": {
            "description": "WhatsApp did not answer within the request budget, so no per-participant outcome was read at all. Deliberately not folded into the 200 above β€” a participant WhatsApp turned down is reported inside `results` and is an answer; an update that never came back is not."
          }
        },
        "summary": "Approve pending join requests",
        "tags": [
          "groups"
        ]
      }
    },
    "/api/sessions/{sessionId}/groups/{groupId}/membership-requests/reject": {
      "post": {
        "description": "Rejects the named requesters, or EVERY pending request when the body names none. Rejecting an empty queue is a no-op that returns an empty results list. On whatsapp-web.js the engine pauses 250-500ms between requesters (upstream anti-abuse pacing), so acting on a large queue is a proportionally long request.",
        "operationId": "GroupController_rejectMembershipRequests",
        "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/MembershipRequestActionDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Requests processed β€” `results` carries the per-participant outcome (a partial refusal does not fail the batch; a total refusal of NAMED requesters is an error)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ParticipantsOperationResponseDto"
                }
              }
            }
          },
          "403": {
            "description": "WhatsApp refused the operation. The request was well formed β€” the refusal happened WhatsApp-side, most often because the account lacks the admin rights the operation requires."
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "503": {
            "description": "WhatsApp did not answer within the request budget, so no per-participant outcome was read at all. Deliberately not folded into the 200 above β€” a participant WhatsApp turned down is reported inside `results` and is an answer; an update that never came back is not."
          }
        },
        "summary": "Reject pending join requests",
        "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",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GroupAckResponseDto"
                }
              }
            }
          },
          "403": {
            "description": "The engine refused the change β€” admin rights are required"
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "503": {
            "description": "WhatsApp did not answer within the request budget. The change may or may not have been applied β€” the gateway stopped waiting for a confirmation that never came."
          }
        },
        "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",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GroupAckResponseDto"
                }
              }
            }
          },
          "403": {
            "description": "The engine refused the change β€” admin rights are required"
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "503": {
            "description": "WhatsApp did not answer within the request budget. The change may or may not have been applied β€” the gateway stopped waiting for a confirmation that never came."
          }
        },
        "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",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GroupAckResponseDto"
                }
              }
            }
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "503": {
            "description": "WhatsApp did not answer within the request budget. The change may or may not have been applied β€” the gateway stopped waiting for a confirmation that never came."
          }
        },
        "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",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GroupPictureResponseDto"
                }
              }
            }
          },
          "400": {
            "description": "The id does not name a group, or the session is not active"
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "503": {
            "description": "WhatsApp did not answer within the request budget β€” nothing could be read."
          }
        },
        "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",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GroupAckResponseDto"
                }
              }
            }
          },
          "400": {
            "description": "The id does not name a group, or the session is not active, or neither url nor base64 was supplied"
          },
          "403": {
            "description": "The engine refused the change β€” admin rights required"
          },
          "404": {
            "description": "No such group β€” the id is unknown."
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "503": {
            "description": "WhatsApp did not answer within the request budget. The change may or may not have been applied β€” the gateway stopped waiting for a confirmation that never came."
          }
        },
        "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",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GroupAckResponseDto"
                }
              }
            }
          },
          "400": {
            "description": "The id does not name a group, or the session is not active"
          },
          "403": {
            "description": "The engine refused the change β€” admin rights required"
          },
          "404": {
            "description": "No such group β€” the id is unknown."
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "503": {
            "description": "WhatsApp did not answer within the request budget. The change may or may not have been applied β€” the gateway stopped waiting for a confirmation that never came."
          }
        },
        "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",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GroupInviteCodeResponseDto"
                }
              }
            }
          },
          "403": {
            "description": "The engine refused the request β€” admin rights required for this group"
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "503": {
            "description": "WhatsApp did not answer the invite-code query β€” retry shortly"
          }
        },
        "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",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GroupInviteCodeRevokedResponseDto"
                }
              }
            }
          },
          "403": {
            "description": "The engine refused the request β€” admin rights required for this group"
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "503": {
            "description": "WhatsApp did not answer the invite-code query β€” retry shortly"
          }
        },
        "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",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProfileAckResponseDto"
                }
              }
            }
          },
          "400": {
            "description": "Session is not started"
          },
          "403": {
            "description": "The engine refused the name change"
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "503": {
            "description": "WhatsApp did not answer within the request budget. The change may or may not have been applied β€” the gateway stopped waiting for a confirmation that never came."
          }
        },
        "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",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProfileAckResponseDto"
                }
              }
            }
          },
          "400": {
            "description": "Session is not started"
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "503": {
            "description": "WhatsApp did not answer within the request budget. The change may or may not have been applied β€” the gateway stopped waiting for a confirmation that never came."
          }
        },
        "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",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProfileAckResponseDto"
                }
              }
            }
          },
          "400": {
            "description": "Neither url nor base64 provided, base64 without mimetype, or session is not started"
          },
          "403": {
            "description": "The engine refused the picture change"
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "413": {
            "description": "Decoded base64 image exceeds the configured media cap"
          },
          "503": {
            "description": "WhatsApp did not answer within the request budget. The change may or may not have been applied β€” the gateway stopped waiting for a confirmation that never came."
          }
        },
        "summary": "Set the account profile picture (URL or base64 image)",
        "tags": [
          "profile"
        ]
      },
      "delete": {
        "operationId": "ProfileController_deletePicture",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Profile picture removed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProfileAckResponseDto"
                }
              }
            }
          },
          "400": {
            "description": "Session is not started"
          },
          "403": {
            "description": "The engine refused the picture removal"
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "503": {
            "description": "WhatsApp did not answer within the request budget. The change may or may not have been applied β€” the gateway stopped waiting for a confirmation that never came."
          }
        },
        "summary": "Remove the account's profile picture",
        "tags": [
          "profile"
        ]
      }
    },
    "/api/sessions/{sessionId}/calls/link": {
      "post": {
        "operationId": "CallController_createLink",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateCallLinkDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The generated link",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CallLinkResponseDto"
                }
              }
            }
          },
          "400": {
            "description": "Session is not started, or an invalid type / startTime"
          },
          "403": {
            "description": "WhatsApp generated no link for this request"
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "503": {
            "description": "WhatsApp did not answer within the request budget. The link may or may not have been created β€” the gateway stopped waiting for a reply that never came."
          }
        },
        "summary": "Generate a shareable WhatsApp call link",
        "tags": [
          "calls"
        ]
      }
    },
    "/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",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CallAckResponseDto"
                }
              }
            }
          },
          "400": {
            "description": "Session is not started"
          },
          "404": {
            "description": "Call not found or no longer ringing"
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "503": {
            "description": "WhatsApp did not answer within the request budget. The change may or may not have been applied β€” the gateway stopped waiting for a confirmation that never came."
          }
        },
        "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",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/LabelDto"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Session not ready or not a business account"
          },
          "404": {
            "description": "Session not found"
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "501": {
            "description": "The active engine cannot serve this operation. It is part of the engine contract, but the engine currently running has no implementation for it."
          }
        },
        "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",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LabelDto"
                }
              }
            }
          },
          "404": {
            "description": "Label not found"
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "501": {
            "description": "The active engine cannot serve this operation. It is part of the engine contract, but the engine currently running has no implementation for it."
          }
        },
        "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",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LabelAckResponseDto"
                }
              }
            }
          },
          "400": {
            "description": "Session not started, or validation failed"
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "501": {
            "description": "The active engine cannot edit labels (whatsapp-web.js)"
          },
          "503": {
            "description": "WhatsApp did not answer within the request budget. The change may or may not have been applied β€” the gateway stopped waiting for a confirmation that never came."
          }
        },
        "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",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LabelAckResponseDto"
                }
              }
            }
          },
          "400": {
            "description": "Session not started"
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "501": {
            "description": "The active engine cannot edit labels (whatsapp-web.js)"
          },
          "503": {
            "description": "WhatsApp did not answer within the request budget. The change may or may not have been applied β€” the gateway stopped waiting for a confirmation that never came."
          }
        },
        "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",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/LabelChatDto"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Session not started"
          },
          "404": {
            "description": "No such label β€” it was never created, or the account is not a WhatsApp Business one and holds no labels at all. Both are the same answer to the caller."
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "501": {
            "description": "The active engine cannot list chats by label (Baileys)"
          },
          "503": {
            "description": "The whatsapp-web.js page connection died mid-read, so nothing could be read. Deliberately not reported as a missing label β€” a page that went away says nothing about whether the label exists. The other engine never answers this: Baileys has no label query at all and answers 501 above."
          }
        },
        "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",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/LabelDto"
                  }
                }
              }
            }
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "501": {
            "description": "The active engine cannot serve this operation. It is part of the engine contract, but the engine currently running has no implementation for it."
          }
        },
        "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",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LabelAckResponseDto"
                }
              }
            }
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "422": {
            "description": "Labels require a WhatsApp Business account, or the chat type has no labels"
          },
          "503": {
            "description": "WhatsApp did not answer within the request budget. The change may or may not have been applied β€” the gateway stopped waiting for a confirmation that never came."
          }
        },
        "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",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LabelAckResponseDto"
                }
              }
            }
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "422": {
            "description": "Labels require a WhatsApp Business account, or the chat type has no labels"
          },
          "503": {
            "description": "WhatsApp did not answer within the request budget. The change may or may not have been applied β€” the gateway stopped waiting for a confirmation that never came."
          }
        },
        "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",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ChannelDto"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Session not ready"
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "501": {
            "description": "Not supported by the active engine: the Baileys adapter cannot list subscribed channels."
          }
        },
        "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",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChannelDto"
                }
              }
            }
          },
          "400": {
            "description": "Session not started, or validation failed"
          },
          "403": {
            "description": "The engine refused β€” channel creation may be disabled for this account"
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          }
        },
        "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",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChannelDto"
                }
              }
            }
          },
          "404": {
            "description": "Channel not found"
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "503": {
            "description": "WhatsApp did not answer within the request budget β€” the operation may or may not have applied."
          }
        },
        "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",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChannelAckResponseDto"
                }
              }
            }
          },
          "403": {
            "description": "WhatsApp refused the operation. The request was well formed β€” the refusal happened WhatsApp-side, most often because the account lacks the admin rights the operation requires."
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "503": {
            "description": "WhatsApp did not answer within the request budget β€” the operation may or may not have applied."
          }
        },
        "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",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ChannelMessageDto"
                  }
                }
              }
            }
          },
          "404": {
            "description": "No such channel β€” the id is not among the session's subscribed channels, either because it is wrong or because the channel has not synced into the local collection yet."
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "501": {
            "description": "Not supported by the active engine: the Baileys adapter cannot read 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",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChannelAckResponseDto"
                }
              }
            }
          },
          "400": {
            "description": "Session not started"
          },
          "403": {
            "description": "The engine refused β€” not found, or this account does not own it"
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "503": {
            "description": "WhatsApp did not answer within the request budget β€” the operation may or may not have applied."
          }
        },
        "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",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChannelAckResponseDto"
                }
              }
            }
          },
          "400": {
            "description": "Session not started, or validation failed"
          },
          "403": {
            "description": "The engine refused"
          },
          "404": {
            "description": "No such channel β€” the id is not among the session's subscribed channels, either because it is wrong or because the channel has not synced into the local collection yet."
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "503": {
            "description": "WhatsApp did not answer within the request budget β€” the operation may or may not have applied."
          }
        },
        "summary": "Mute or unmute a channel",
        "tags": [
          "channels"
        ]
      }
    },
    "/api/sessions/{sessionId}/channels/{channelId}/admins/demote": {
      "post": {
        "description": "Requires this account to own the channel. There is no promote counterpart: neither engine library exposes one, so an admin is promoted from the WhatsApp app and demoted here.",
        "operationId": "ChannelController_demoteAdmin",
        "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/DemoteChannelAdminDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Admin demoted",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChannelAckResponseDto"
                }
              }
            }
          },
          "400": {
            "description": "Session not started, or validation failed"
          },
          "403": {
            "description": "The engine refused β€” not the owner, or the user is not an admin"
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "501": {
            "description": "The whatsapp-web.js engine cannot perform this: the WhatsApp Web module its library method targets no longer exports the function. Use the Baileys engine."
          },
          "503": {
            "description": "WhatsApp did not answer within the request budget β€” the operation may or may not have applied."
          }
        },
        "summary": "Demote a channel admin back to a subscriber",
        "tags": [
          "channels"
        ]
      }
    },
    "/api/sessions/{sessionId}/channels/{channelId}/owner/transfer": {
      "post": {
        "description": "IRREVERSIBLE. Once the transfer lands, this session is no longer the owner and cannot take the channel back through this API. Requires this account to currently own the channel. The upstream option to also dismiss yourself as an admin in the same call is not exposed, because the WhatsApp Web function it depends on no longer exists and the branch swallows its own errors, so it would fail silently.",
        "operationId": "ChannelController_transferOwnership",
        "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/TransferChannelOwnershipDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Ownership transferred",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChannelAckResponseDto"
                }
              }
            }
          },
          "400": {
            "description": "Session not started, or validation failed"
          },
          "403": {
            "description": "The engine did not transfer the channel. On whatsapp-web.js this is the only outcome a failure can produce: the page swallows every error into a plain false, so the cause is not reported."
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "501": {
            "description": "The whatsapp-web.js engine cannot perform this: its page function rejects every call locally against a subscriber list the page cannot repopulate. Use the Baileys engine."
          },
          "503": {
            "description": "WhatsApp did not answer within the request budget β€” the transfer may or may not have applied."
          }
        },
        "summary": "Transfer channel ownership to another account",
        "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",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChannelDto"
                }
              }
            }
          },
          "404": {
            "description": "The invite code does not resolve to a channel β€” it is wrong, expired, or revoked. This route takes an invite code rather than a channel id."
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "501": {
            "description": "Not supported by the active engine: whatsapp-web.js cannot subscribe by invite code."
          },
          "503": {
            "description": "WhatsApp did not answer within the request budget β€” the operation may or may not have applied."
          }
        },
        "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.).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OverviewStatsResponseDto"
                }
              }
            }
          }
        },
        "summary": "Get overall statistics",
        "tags": [
          "statistics"
        ]
      }
    },
    "/api/stats/messages": {
      "get": {
        "operationId": "StatsController_getMessageStats",
        "parameters": [
          {
            "name": "period",
            "required": false,
            "in": "query",
            "description": "Time window for the returned series.",
            "schema": {
              "default": "24h",
              "type": "string",
              "enum": [
                "24h",
                "7d",
                "30d"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Message statistics with a time series for the requested period.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MessageStatsResponseDto"
                }
              }
            }
          }
        },
        "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.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SessionStatsResponseDto"
                }
              }
            }
          }
        },
        "summary": "Get statistics for a specific session",
        "tags": [
          "statistics"
        ]
      }
    },
    "/api/metrics": {
      "get": {
        "operationId": "MetricsController_scrape",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Prometheus exposition text",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "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.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StatusListResponseDto"
                }
              }
            }
          }
        },
        "summary": "Get all contact status updates",
        "tags": [
          "status"
        ]
      }
    },
    "/api/sessions/{sessionId}/status/{id}": {
      "get": {
        "operationId": "StatusController_getContactStatus",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "Contact ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Status updates from the requested contact.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StatusListResponseDto"
                }
              }
            }
          }
        },
        "summary": "Get status updates from a specific contact",
        "tags": [
          "status"
        ]
      },
      "delete": {
        "operationId": "StatusController_deleteStatus",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "required": true,
            "in": "path",
            "description": "Status ID",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Status deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StatusDeletedResponseDto"
                }
              }
            }
          },
          "404": {
            "description": "No session is running under that id β€” it was never started, was stopped, or does not exist. These routes report an unstarted session as `404`, where the message and group routes report it as `400`."
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          }
        },
        "summary": "Delete own status",
        "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.",
            "content": {
              "application/octet-stream": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "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.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StatusResultDto"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request, or the post was blocked by a plugin."
          },
          "404": {
            "description": "No session is running under that id β€” it was never started, was stopped, or does not exist. These routes report an unstarted session as `404`, where the message and group routes report it as `400`."
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          }
        },
        "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.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StatusResultDto"
                }
              }
            }
          },
          "400": {
            "description": "Neither url nor base64 provided, or the post was blocked by a plugin."
          },
          "404": {
            "description": "No session is running under that id β€” it was never started, was stopped, or does not exist. These routes report an unstarted session as `404`, where the message and group routes report it as `400`."
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "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.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StatusResultDto"
                }
              }
            }
          },
          "400": {
            "description": "Neither url nor base64 provided, or the post was blocked by a plugin."
          },
          "404": {
            "description": "No session is running under that id β€” it was never started, was stopped, or does not exist. These routes report an unstarted session as `404`, where the message and group routes report it as `400`."
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "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.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StatusResultDto"
                }
              }
            }
          },
          "400": {
            "description": "Neither url nor base64 provided, or the post was blocked by a plugin."
          },
          "404": {
            "description": "No session is running under that id β€” it was never started, was stopped, or does not exist. These routes report an unstarted session as `404`, where the message and group routes report it as `400`."
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "413": {
            "description": "Base64 media exceeds MEDIA_DOWNLOAD_MAX_BYTES."
          }
        },
        "summary": "Post an audio status as a voice note",
        "tags": [
          "status"
        ]
      }
    },
    "/api/sessions/{sessionId}/media/convert": {
      "get": {
        "operationId": "MediaController_conversionStatus",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID the API key must be authorized for",
            "schema": {
              "type": "string"
            }
          }
        ],
        "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.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConversionStatusResponseDto"
                }
              }
            }
          }
        },
        "summary": "Whether server-side media conversion is available",
        "tags": [
          "media"
        ]
      }
    },
    "/api/sessions/{sessionId}/media/convert/voice": {
      "post": {
        "operationId": "MediaController_convertVoice",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID the API key must be authorized for",
            "schema": {
              "type": "string"
            }
          }
        ],
        "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.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConvertedMediaResponseDto"
                }
              }
            }
          },
          "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": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "description": "Session ID the API key must be authorized for",
            "schema": {
              "type": "string"
            }
          }
        ],
        "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.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConvertedMediaResponseDto"
                }
              }
            }
          },
          "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": {
          "200": {
            "description": "Catalog summary, or an empty body when the account has no collection to describe.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CatalogDto"
                }
              }
            }
          },
          "404": {
            "description": "No session is running under that id β€” it was never started, was stopped, or does not exist. These routes report an unstarted session as `404`, where the message and group routes report it as `400`."
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "501": {
            "description": "Not supported by the active engine: whatsapp-web.js has no catalog API."
          },
          "503": {
            "description": "WhatsApp did not answer the catalog query within the request budget β€” retry shortly."
          }
        },
        "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": {
          "200": {
            "description": "One page of catalog products",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaginatedProductsDto"
                }
              }
            }
          },
          "404": {
            "description": "No session is running under that id β€” it was never started, was stopped, or does not exist. These routes report an unstarted session as `404`, where the message and group routes report it as `400`."
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "501": {
            "description": "Not supported by the active engine: whatsapp-web.js has no catalog API."
          },
          "503": {
            "description": "WhatsApp did not answer the catalog query within the request budget β€” retry shortly."
          }
        },
        "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": {
          "200": {
            "description": "The product, or an empty body when no product in the catalog carries that id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProductDto"
                }
              }
            }
          },
          "404": {
            "description": "No session is running under that id β€” it was never started, was stopped, or does not exist. These routes report an unstarted session as `404`, where the message and group routes report it as `400`."
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "501": {
            "description": "Not supported by the active engine: whatsapp-web.js has no catalog API."
          },
          "503": {
            "description": "WhatsApp did not answer the catalog query within the request budget β€” retry shortly."
          }
        },
        "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": {
          "201": {
            "description": "Product message accepted for sending",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProductMessageResponseDto"
                }
              }
            }
          },
          "400": {
            "description": "Product has no image β€” a product card requires one."
          },
          "404": {
            "description": "Product id not found in the session catalog."
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "501": {
            "description": "Not supported by the active engine: whatsapp-web.js cannot send product messages."
          },
          "503": {
            "description": "WhatsApp did not answer the catalog query within the request budget β€” retry shortly."
          }
        },
        "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": {
          "404": {
            "description": "No session is running under that id β€” it was never started, was stopped, or does not exist. These routes report an unstarted session as `404`, where the message and group routes report it as `400`."
          },
          "409": {
            "description": "The session is not connected β€” an engine exists for it but is not `ready`: disconnected, reconnecting, or still initializing, so the request never reached WhatsApp. Wait for `ready` and retry. A session that was never started answers `400` instead, and the session lifecycle routes answer `409` for a conflicting state rather than this. One window answers this while the session still reads `ready`: WhatsApp Web periodically reloads its own page and the engine re-injects into it β€” for those few seconds the answer is a `409` naming the reload; retry shortly."
          },
          "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",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/PluginDto"
                  }
                }
              }
            }
          }
        },
        "summary": "List all plugins",
        "tags": [
          "plugins"
        ]
      }
    },
    "/api/plugins/install": {
      "post": {
        "operationId": "PluginsController_install",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "The plugin .zip package"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Plugin installed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PluginDto"
                }
              }
            }
          },
          "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",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PluginDto"
                }
              }
            }
          },
          "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",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PluginDto"
                }
              }
            }
          },
          "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",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "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",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PluginDto"
                }
              }
            }
          },
          "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"
            }
          },
          {
            "name": "path",
            "required": true,
            "in": "path",
            "description": "Provider-defined trailing path the plugin claims (may contain slashes).",
            "schema": {
              "example": "events/message",
              "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"
            }
          },
          {
            "name": "path",
            "required": true,
            "in": "path",
            "description": "Provider-defined trailing path the plugin claims (may contain slashes).",
            "schema": {
              "example": "events/message",
              "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"
            }
          },
          {
            "name": "path",
            "required": true,
            "in": "path",
            "description": "Provider-defined trailing path the plugin claims (may contain slashes).",
            "schema": {
              "example": "events/message",
              "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"
            }
          },
          {
            "name": "path",
            "required": true,
            "in": "path",
            "description": "Provider-defined trailing path the plugin claims (may contain slashes).",
            "schema": {
              "example": "events/message",
              "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"
            }
          },
          {
            "name": "path",
            "required": true,
            "in": "path",
            "description": "Provider-defined trailing path the plugin claims (may contain slashes).",
            "schema": {
              "example": "events/message",
              "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"
            }
          },
          {
            "name": "path",
            "required": true,
            "in": "path",
            "description": "Provider-defined trailing path the plugin claims (may contain slashes).",
            "schema": {
              "example": "events/message",
              "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"
            }
          },
          {
            "name": "path",
            "required": true,
            "in": "path",
            "description": "Provider-defined trailing path the plugin claims (may contain slashes).",
            "schema": {
              "example": "events/message",
              "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",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SearchResultsResponseDto"
                }
              }
            }
          },
          "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\n\n**Gateway-wide responses.** Two statuses are returned by middleware before routing, so any operation can emit them:\n\n- `415 Unsupported Media Type` β€” the request body carries a `Content-Encoding` other than `identity`. The aggregate in-flight body cap counts wire bytes, so a compressed body would be admitted on its compressed size and then inflated past the memory it is meant to bound. Send the body uncompressed.\n- `503 Service Unavailable` with `Retry-After` β€” the gateway already has too much request body data in flight. The body is not read; retry after the given delay.",
    "version": "0.18.0",
    "contact": {
      "name": "OpenWA",
      "url": "https://github.com/rmyndharis/OpenWA",
      "email": "[email protected]"
    }
  },
  "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": {
      "AuditLogDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "4f1c9b2a-8d3e-4c5b-9a17-2e6f0b4d8c31"
          },
          "action": {
            "type": "string",
            "description": "What happened.",
            "example": "session.create"
          },
          "severity": {
            "type": "string",
            "enum": [
              "info",
              "warn",
              "error"
            ],
            "example": "info"
          },
          "apiKeyId": {
            "type": "string",
            "nullable": true,
            "description": "Null for an unauthenticated or system action."
          },
          "apiKeyName": {
            "type": "string",
            "nullable": true
          },
          "sessionId": {
            "type": "string",
            "nullable": true,
            "description": "Null for a deployment-global action."
          },
          "sessionName": {
            "type": "string",
            "nullable": true
          },
          "ipAddress": {
            "type": "string",
            "nullable": true
          },
          "userAgent": {
            "type": "string",
            "nullable": true
          },
          "method": {
            "type": "string",
            "nullable": true,
            "example": "POST"
          },
          "path": {
            "type": "string",
            "nullable": true,
            "example": "/api/sessions"
          },
          "statusCode": {
            "type": "number",
            "nullable": true,
            "example": 201
          },
          "metadata": {
            "type": "object",
            "nullable": true,
            "description": "Free-form context for the action. Shape varies per action and is not part of the contract."
          },
          "errorMessage": {
            "type": "string",
            "nullable": true,
            "description": "Present only for a failed action."
          },
          "createdAt": {
            "type": "string",
            "description": "ISO-8601 timestamp the entry was written.",
            "example": "2026-08-07T12:00:00.000Z"
          }
        },
        "required": [
          "id",
          "action",
          "severity",
          "apiKeyId",
          "apiKeyName",
          "sessionId",
          "sessionName",
          "ipAddress",
          "userAgent",
          "method",
          "path",
          "statusCode",
          "metadata",
          "errorMessage",
          "createdAt"
        ]
      },
      "AuditListResponseDto": {
        "type": "object",
        "properties": {
          "data": {
            "description": "The requested page. A session-scoped API key sees only entries within its scope.",
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AuditLogDto"
            }
          },
          "total": {
            "type": "number",
            "description": "Total matching entries, for paging.",
            "example": 128
          }
        },
        "required": [
          "data",
          "total"
        ]
      },
      "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": "Session **ids** this key may act on β€” the server-generated UUIDs, not session names. Matched by exact equality against the id in the request path, so a name never matches and would silently scope the key to nothing. Omit or leave empty to let the key reach every session.",
            "example": [
              "0a941dac-a965-45e7-b318-74ae8be134f0",
              "8f3c2b1a-9d4e-4c7a-8b2f-1e6d5a4c3b2a"
            ],
            "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. Only three keys are read: autoRejectCalls (boolean, default false) rejects incoming calls as soon as they ring β€” the call.received event is still emitted first; maxReconnectAttempts (0-20, default unlimited) caps consecutive reconnects; and reconnectBaseDelay (1000-300000 ms, default 5000) sets the backoff base. Anything else is stored but ignored. All three can be changed later with PATCH /api/sessions/{id}/config, without restarting the session.",
            "example": {
              "autoRejectCalls": false,
              "maxReconnectAttempts": 5,
              "reconnectBaseDelay": 5000
            }
          },
          "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": "0a941dac-a965-45e7-b318-74ae8be134f0"
          },
          "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"
        ]
      },
      "SessionConfigResponseDto": {
        "type": "object",
        "properties": {
          "autoRejectCalls": {
            "type": "boolean",
            "description": "Whether incoming calls are auto-rejected",
            "example": false
          },
          "maxReconnectAttempts": {
            "type": "number",
            "description": "Reconnect attempt cap; `null` means unlimited",
            "example": 5,
            "nullable": true
          },
          "reconnectBaseDelay": {
            "type": "number",
            "description": "Base reconnect backoff in milliseconds",
            "example": 5000
          }
        },
        "required": [
          "autoRejectCalls",
          "maxReconnectAttempts",
          "reconnectBaseDelay"
        ]
      },
      "UpdateSessionConfigDto": {
        "type": "object",
        "properties": {
          "autoRejectCalls": {
            "type": "boolean",
            "description": "Auto-reject every incoming call as soon as it rings. The call.received event is still emitted first, so a webhook consumer sees the call regardless. Takes effect on the next incoming call β€” the session is not restarted.",
            "example": true,
            "nullable": true
          },
          "maxReconnectAttempts": {
            "type": "number",
            "description": "Cap on consecutive reconnect attempts (`0` disables reconnect entirely). Send `null` for unlimited, which is the default. Applies on the next session start, not to a reconnect sequence already in flight.",
            "minimum": 0,
            "maximum": 20,
            "example": 5,
            "nullable": true
          },
          "reconnectBaseDelay": {
            "type": "number",
            "description": "Base delay of the reconnect backoff in milliseconds. Applies on the next session start, not to a reconnect sequence already in flight.",
            "minimum": 1000,
            "maximum": 300000,
            "example": 5000,
            "nullable": true
          }
        }
      },
      "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": "[email protected]"
          },
          "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. [email protected] on whatsapp-web.js)",
            "example": "[email protected]"
          }
        },
        "required": [
          "chatId"
        ]
      },
      "SubscribePresenceDto": {
        "type": "object",
        "properties": {
          "chatId": {
            "type": "string",
            "description": "Chat ID in the active engine's native format (e.g. [email protected] on whatsapp-web.js)",
            "example": "[email protected]"
          }
        },
        "required": [
          "chatId"
        ]
      },
      "SetOwnPresenceDto": {
        "type": "object",
        "properties": {
          "available": {
            "type": "boolean",
            "description": "true = appear online; false = appear offline (hands notifications back to the phone β€” an always-online linked device suppresses the phone's own alerts)",
            "example": false
          }
        },
        "required": [
          "available"
        ]
      },
      "ParticipantPresenceDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Participant id. In a 1:1 chat this is the chat itself.",
            "example": "[email protected]"
          },
          "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": "[email protected]"
          },
          "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. [email protected] on whatsapp-web.js)",
            "example": "[email protected]"
          },
          "archive": {
            "type": "boolean",
            "description": "true to archive, false to unarchive."
          }
        },
        "required": [
          "chatId",
          "archive"
        ]
      },
      "MuteChatDto": {
        "type": "object",
        "properties": {
          "chatId": {
            "type": "string",
            "description": "Chat ID in the active engine's native format (e.g. [email protected] on whatsapp-web.js)",
            "example": "[email protected]"
          },
          "muteUntil": {
            "type": "number",
            "description": "Absolute epoch-MILLISECONDS timestamp at which the mute expires, or `null` to unmute now. Required β€” omitting it is rejected rather than guessed, because the two plausible readings (unmute vs mute forever) are opposites. To mute indefinitely, send a far-future timestamp. Milliseconds, not seconds: a seconds-scale value is an instant in 1970, so the mute expires immediately while the request still answers 200.",
            "example": 1800000000000,
            "nullable": true
          }
        },
        "required": [
          "chatId",
          "muteUntil"
        ]
      },
      "PinChatDto": {
        "type": "object",
        "properties": {
          "chatId": {
            "type": "string",
            "description": "Chat ID in the active engine's native format (e.g. [email protected] on whatsapp-web.js)",
            "example": "[email protected]"
          },
          "pin": {
            "type": "boolean",
            "description": "true to pin the chat to the top of the list, false to unpin it."
          }
        },
        "required": [
          "chatId",
          "pin"
        ]
      },
      "DeleteChatDto": {
        "type": "object",
        "properties": {
          "chatId": {
            "type": "string",
            "description": "Chat ID in the active engine's native format (e.g. [email protected] on whatsapp-web.js)",
            "example": "[email protected]"
          }
        },
        "required": [
          "chatId"
        ]
      },
      "SendChatStateDto": {
        "type": "object",
        "properties": {
          "chatId": {
            "type": "string",
            "description": "Chat ID, in the active engine's native format (e.g. [email protected])",
            "example": "[email protected]"
          },
          "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",
                "group.join_request",
                "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": [
                    "[email protected]"
                  ]
                },
                {
                  "field": "body",
                  "operator": "contains",
                  "value": "invoice"
                }
              ]
            },
            "nullable": true
          },
          "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": [
                    "[email protected]"
                  ]
                },
                {
                  "field": "body",
                  "operator": "contains",
                  "value": "invoice"
                }
              ]
            },
            "nullable": true
          },
          "active": {
            "type": "boolean"
          },
          "retryCount": {
            "type": "number"
          },
          "lastTriggeredAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "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",
                "group.join_request",
                "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": [
                    "[email protected]"
                  ]
                },
                {
                  "field": "body",
                  "operator": "contains",
                  "value": "invoice"
                }
              ]
            },
            "nullable": true
          },
          "active": {
            "type": "boolean",
            "description": "Enable/disable webhook"
          },
          "retryCount": {
            "type": "number",
            "description": "Delivery attempts before the webhook is parked. Same range the create route enforces.",
            "example": 3,
            "minimum": 0,
            "maximum": 5
          }
        }
      },
      "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 ([email protected] for individual, [email protected] for groups)",
            "example": "[email protected]"
          },
          "text": {
            "type": "string",
            "description": "Text message content",
            "example": "Hello from OpenWA!",
            "maxLength": 4096
          },
          "mentions": {
            "description": "WIDs to @mention (e.g. [\"[email protected]\"]). The text/caption must also contain the @<number> token.",
            "example": [
              "[email protected]"
            ],
            "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"
              }
            ]
          },
          "quotedMessageId": {
            "type": "string",
            "description": "Quote an earlier message, turning this send into a reply. The id is engine-specific: whatsapp-web.js matches the serialized message id, Baileys matches the raw message key id and can only quote a message it has already stored. An id that cannot be resolved fails the send rather than delivering it unquoted.",
            "example": "[email protected]_3EB0ABCD"
          }
        },
        "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": "[email protected]_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 ([email protected] for individual, [email protected] for groups)",
            "example": "[email protected]"
          },
          "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": "[email protected]"
          },
          "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. [\"[email protected]\"]). The text/caption must also contain the @<number> token.",
            "example": [
              "[email protected]"
            ],
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "quotedMessageId": {
            "type": "string",
            "description": "Quote an earlier message, turning this send into a reply. The id is engine-specific: whatsapp-web.js matches the serialized message id, Baileys matches the raw message key id and can only quote a message it has already stored. An id that cannot be resolved fails the send rather than delivering it unquoted.",
            "example": "[email protected]_3EB0ABCD"
          }
        },
        "required": [
          "chatId"
        ]
      },
      "SendAudioMessageDto": {
        "type": "object",
        "properties": {
          "chatId": {
            "type": "string",
            "description": "WhatsApp chat ID",
            "example": "[email protected]"
          },
          "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. [\"[email protected]\"]). The text/caption must also contain the @<number> token.",
            "example": [
              "[email protected]"
            ],
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "quotedMessageId": {
            "type": "string",
            "description": "Quote an earlier message, turning this send into a reply. The id is engine-specific: whatsapp-web.js matches the serialized message id, Baileys matches the raw message key id and can only quote a message it has already stored. An id that cannot be resolved fails the send rather than delivering it unquoted.",
            "example": "[email protected]_3EB0ABCD"
          },
          "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. [email protected])"
          },
          "latitude": {
            "type": "number",
            "example": -6.2088
          },
          "longitude": {
            "type": "number",
            "example": 106.8456
          },
          "description": {
            "type": "string",
            "maxLength": 1024
          },
          "address": {
            "type": "string",
            "maxLength": 1024
          },
          "quotedMessageId": {
            "type": "string",
            "description": "Quote an earlier message, turning this send into a reply. The id is engine-specific: whatsapp-web.js matches the serialized message id, Baileys matches the raw message key id and can only quote a message it has already stored. An id that cannot be resolved fails the send rather than delivering it unquoted.",
            "example": "[email protected]_3EB0ABCD"
          }
        },
        "required": [
          "chatId",
          "latitude",
          "longitude"
        ]
      },
      "SendContactDto": {
        "type": "object",
        "properties": {
          "chatId": {
            "type": "string"
          },
          "contactName": {
            "type": "string",
            "maxLength": 255
          },
          "contactNumber": {
            "type": "string",
            "maxLength": 30
          },
          "quotedMessageId": {
            "type": "string",
            "description": "Quote an earlier message, turning this send into a reply. The id is engine-specific: whatsapp-web.js matches the serialized message id, Baileys matches the raw message key id and can only quote a message it has already stored. An id that cannot be resolved fails the send rather than delivering it unquoted.",
            "example": "[email protected]_3EB0ABCD"
          }
        },
        "required": [
          "chatId",
          "contactName",
          "contactNumber"
        ]
      },
      "SendPollDto": {
        "type": "object",
        "properties": {
          "chatId": {
            "type": "string",
            "description": "Chat ID (e.g. [email protected] or [email protected])"
          },
          "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)"
          },
          "quotedMessageId": {
            "type": "string",
            "description": "Quote an earlier message, turning this send into a reply. The id is engine-specific: whatsapp-web.js matches the serialized message id, Baileys matches the raw message key id and can only quote a message it has already stored. An id that cannot be resolved fails the send rather than delivering it unquoted.",
            "example": "[email protected]_3EB0ABCD"
          }
        },
        "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": "[email protected]"
          },
          "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.",
            "default": 3000,
            "minimum": 1000,
            "maximum": 60000
          },
          "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
          }
        }
      },
      "HealthCheckResponseDto": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "description": "Liveness marker. This route does not probe dependencies.",
            "example": "ok"
          },
          "timestamp": {
            "type": "string",
            "description": "ISO-8601 timestamp of the reply.",
            "example": "2026-08-07T12:00:00.000Z"
          },
          "version": {
            "type": "string",
            "description": "Running application version.",
            "example": "0.14.4"
          }
        },
        "required": [
          "status",
          "timestamp",
          "version"
        ]
      },
      "LivenessResponseDto": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "description": "Always `ok` when the process can answer at all.",
            "example": "ok"
          }
        },
        "required": [
          "status"
        ]
      },
      "ReadinessResponseDto": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "description": "Only `ok` reaches a 200 β€” a failing dependency answers 503 with this same shape.",
            "example": "ok"
          },
          "details": {
            "type": "object",
            "description": "Per-dependency outcome, keyed by dependency name. Present on both the 200 and the 503.",
            "example": {
              "mainDatabase": {
                "status": "up"
              },
              "dataDatabase": {
                "status": "up"
              }
            },
            "additionalProperties": {
              "type": "object"
            }
          }
        },
        "required": [
          "status",
          "details"
        ]
      },
      "SettingsGeneralDto": {
        "type": "object",
        "properties": {
          "apiBaseUrl": {
            "type": "string",
            "description": "The advertised base URL (BASE_URL), the same value the startup banner and ingress URLs use.",
            "example": "https://wa.example.com"
          },
          "autoReconnect": {
            "type": "boolean",
            "description": "Always true: the engine auto-reconnects on a transient disconnect and there is no global off switch β€” reconnection is bounded per session by RECONNECT_MAX_ATTEMPTS.",
            "example": true
          },
          "debugMode": {
            "type": "boolean",
            "description": "Whether database query logging is on.",
            "example": false
          }
        },
        "required": [
          "apiBaseUrl",
          "autoReconnect",
          "debugMode"
        ]
      },
      "SettingsApiDto": {
        "type": "object",
        "properties": {
          "rateLimit": {
            "type": "number",
            "description": "Requests allowed per window.",
            "example": 100
          },
          "rateLimitWindow": {
            "type": "number",
            "description": "Window length in milliseconds.",
            "example": 60000
          },
          "enableDocs": {
            "type": "boolean",
            "description": "Whether Swagger is actually served β€” off by default in production.",
            "example": false
          }
        },
        "required": [
          "rateLimit",
          "rateLimitWindow",
          "enableDocs"
        ]
      },
      "SettingsNotificationsDto": {
        "type": "object",
        "properties": {
          "emailEnabled": {
            "type": "boolean",
            "example": false
          },
          "notificationEmail": {
            "type": "string",
            "example": ""
          },
          "webhookAlerts": {
            "type": "boolean",
            "example": true
          }
        },
        "required": [
          "emailEnabled",
          "notificationEmail",
          "webhookAlerts"
        ]
      },
      "SettingsResponseDto": {
        "type": "object",
        "properties": {
          "general": {
            "$ref": "#/components/schemas/SettingsGeneralDto"
          },
          "api": {
            "$ref": "#/components/schemas/SettingsApiDto"
          },
          "notifications": {
            "$ref": "#/components/schemas/SettingsNotificationsDto"
          }
        },
        "required": [
          "general",
          "api",
          "notifications"
        ]
      },
      "InfraDatabaseStatusDto": {
        "type": "object",
        "properties": {
          "connected": {
            "type": "boolean",
            "description": "Whether a `SELECT 1` probe succeeded just now.",
            "example": true
          },
          "type": {
            "type": "string",
            "description": "Configured driver.",
            "example": "postgres"
          },
          "host": {
            "type": "string",
            "description": "Host the app is configured to reach.",
            "example": "openwa-postgres"
          },
          "builtIn": {
            "type": "boolean",
            "description": "Whether OpenWA's own bundled container is actually running and backing this service, detected live from the labelled container rather than read from the saved intent. Falls back to the saved flag when Docker is unavailable.",
            "example": true
          }
        },
        "required": [
          "connected",
          "type",
          "host",
          "builtIn"
        ]
      },
      "InfraRedisStatusDto": {
        "type": "object",
        "properties": {
          "enabled": {
            "type": "boolean",
            "description": "Whether Redis is switched on in configuration.",
            "example": true
          },
          "connected": {
            "type": "boolean",
            "description": "Whether a live probe reached it.",
            "example": true
          },
          "host": {
            "type": "string",
            "example": "openwa-redis"
          },
          "port": {
            "type": "number",
            "example": 6379
          },
          "builtIn": {
            "type": "boolean",
            "description": "Whether OpenWA's bundled Redis container is backing this.",
            "example": true
          }
        },
        "required": [
          "enabled",
          "connected",
          "host",
          "port",
          "builtIn"
        ]
      },
      "InfraQueueDepthDto": {
        "type": "object",
        "properties": {
          "pending": {
            "type": "number",
            "description": "Jobs waiting to be delivered.",
            "example": 0
          },
          "completed": {
            "type": "number",
            "description": "Jobs delivered successfully.",
            "example": 128
          },
          "failed": {
            "type": "number",
            "description": "Jobs that exhausted their retries.",
            "example": 2
          }
        },
        "required": [
          "pending",
          "completed",
          "failed"
        ]
      },
      "InfraQueueStatusDto": {
        "type": "object",
        "properties": {
          "enabled": {
            "type": "boolean",
            "description": "Whether queue processing is switched on.",
            "example": true
          },
          "webhooks": {
            "description": "Webhook queue depths.",
            "allOf": [
              {
                "$ref": "#/components/schemas/InfraQueueDepthDto"
              }
            ]
          }
        },
        "required": [
          "enabled",
          "webhooks"
        ]
      },
      "InfraStorageStatusDto": {
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "local",
              "s3"
            ],
            "example": "local"
          },
          "path": {
            "type": "string",
            "description": "Local storage root. Present only for `local`.",
            "example": "./data/storage"
          },
          "bucket": {
            "type": "string",
            "description": "Bucket name. Present only for `s3`.",
            "example": "openwa-media"
          },
          "builtIn": {
            "type": "boolean",
            "description": "Whether OpenWA's bundled MinIO container is backing this.",
            "example": false
          },
          "s3Available": {
            "type": "boolean",
            "description": "Whether the S3 endpoint answered a probe. Present only for `s3`."
          }
        },
        "required": [
          "type",
          "builtIn"
        ]
      },
      "InfraEngineStatusDto": {
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "description": "Active engine.",
            "example": "baileys"
          },
          "headless": {
            "type": "boolean",
            "description": "Whether Chromium runs headless. Meaningful for whatsapp-web.js only.",
            "example": true
          },
          "sessionDataPath": {
            "type": "string",
            "example": "./data/sessions"
          },
          "browserArgs": {
            "type": "string",
            "description": "Extra Chromium arguments, as configured.",
            "example": ""
          },
          "webVersion": {
            "type": "string",
            "nullable": true,
            "description": "whatsapp-web.js only: the WhatsApp Web build actually in use, which is distinct from the library version. Omitted for other engines.",
            "example": "2.3000.1234567890"
          },
          "webVersionSource": {
            "type": "string",
            "enum": [
              "pinned",
              "auto",
              "native"
            ],
            "description": "How that build was chosen. Omitted for other engines.",
            "example": "pinned"
          }
        },
        "required": [
          "type",
          "headless",
          "sessionDataPath",
          "browserArgs"
        ]
      },
      "InfraStatusResponseDto": {
        "type": "object",
        "properties": {
          "database": {
            "$ref": "#/components/schemas/InfraDatabaseStatusDto"
          },
          "redis": {
            "$ref": "#/components/schemas/InfraRedisStatusDto"
          },
          "queue": {
            "$ref": "#/components/schemas/InfraQueueStatusDto"
          },
          "storage": {
            "$ref": "#/components/schemas/InfraStorageStatusDto"
          },
          "engine": {
            "$ref": "#/components/schemas/InfraEngineStatusDto"
          },
          "envPinned": {
            "description": "Which of the four settings the dashboard can edit are supplied by a layer ABOVE `data/.env.generated` β€” the container environment or a project `.env` β€” and so cannot be changed from the dashboard until that layer is. Reported, not inferred from a running-vs-saved mismatch: a save that has not been restarted yet looks identical and needs the opposite advice.",
            "example": [
              "ENGINE_TYPE"
            ],
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "database",
          "redis",
          "queue",
          "storage",
          "engine",
          "envPinned"
        ]
      },
      "EngineLibraryDto": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "The underlying library, distinct from the engine plugin's own manifest name.",
            "example": "whatsapp-web.js"
          },
          "version": {
            "type": "string",
            "description": "Library version actually loaded.",
            "example": "1.34.7"
          }
        },
        "required": [
          "name",
          "version"
        ]
      },
      "AvailableEngineDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Engine plugin id β€” the value ENGINE_TYPE takes.",
            "example": "baileys"
          },
          "name": {
            "type": "string",
            "description": "Display name from the plugin manifest.",
            "example": "Baileys"
          },
          "enabled": {
            "type": "boolean",
            "description": "Whether the plugin is enabled. Disabled engines are still listed.",
            "example": true
          },
          "features": {
            "description": "Capabilities the engine declares.",
            "example": [
              "text-messages",
              "media-messages",
              "group-management"
            ],
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "library": {
            "description": "Absent when the plugin does not report a library, which includes any disabled engine.",
            "allOf": [
              {
                "$ref": "#/components/schemas/EngineLibraryDto"
              }
            ]
          }
        },
        "required": [
          "id",
          "name",
          "enabled",
          "features"
        ]
      },
      "InfraCurrentEngineResponseDto": {
        "type": "object",
        "properties": {
          "engineType": {
            "type": "string",
            "description": "Engine the process resolved at boot.",
            "example": "baileys"
          }
        },
        "required": [
          "engineType"
        ]
      },
      "InfraHealthResponseDto": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "description": "Liveness marker. This route does not probe dependencies.",
            "example": "ok"
          },
          "timestamp": {
            "type": "string",
            "description": "ISO-8601 timestamp of the reply.",
            "example": "2026-08-07T12:00:00.000Z"
          }
        },
        "required": [
          "status",
          "timestamp"
        ]
      },
      "InfraConfigDatabaseDto": {
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "sqlite",
              "postgres"
            ],
            "example": "postgres"
          },
          "builtIn": {
            "type": "boolean",
            "example": true
          },
          "host": {
            "type": "string",
            "example": "openwa-postgres"
          },
          "port": {
            "type": "string",
            "description": "Kept as a string: it is echoed back from the saved env verbatim.",
            "example": "5432"
          },
          "username": {
            "type": "string",
            "example": "openwa"
          },
          "database": {
            "type": "string",
            "description": "Database name, or the SQLite file path.",
            "example": "openwa"
          },
          "schema": {
            "type": "string",
            "example": "public"
          },
          "poolSize": {
            "type": "number",
            "example": 10
          },
          "sslEnabled": {
            "type": "boolean",
            "example": false
          },
          "sslRejectUnauthorized": {
            "type": "boolean",
            "example": true
          },
          "passwordSet": {
            "type": "boolean",
            "description": "Whether a password is stored. The secret itself is never returned.",
            "example": true
          }
        },
        "required": [
          "type",
          "builtIn",
          "host",
          "port",
          "username",
          "database",
          "schema",
          "poolSize",
          "sslEnabled",
          "sslRejectUnauthorized",
          "passwordSet"
        ]
      },
      "InfraConfigRedisDto": {
        "type": "object",
        "properties": {
          "enabled": {
            "type": "boolean",
            "example": true
          },
          "builtIn": {
            "type": "boolean",
            "example": true
          },
          "host": {
            "type": "string",
            "example": "openwa-redis"
          },
          "port": {
            "type": "string",
            "description": "Echoed from the saved env verbatim.",
            "example": "6379"
          },
          "passwordSet": {
            "type": "boolean",
            "description": "Whether a password is stored. The secret itself is never returned.",
            "example": false
          }
        },
        "required": [
          "enabled",
          "builtIn",
          "host",
          "port",
          "passwordSet"
        ]
      },
      "InfraConfigQueueDto": {
        "type": "object",
        "properties": {
          "enabled": {
            "type": "boolean",
            "example": true
          }
        },
        "required": [
          "enabled"
        ]
      },
      "InfraConfigStorageDto": {
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "local",
              "s3"
            ],
            "example": "local"
          },
          "builtIn": {
            "type": "boolean",
            "example": false
          },
          "localPath": {
            "type": "string",
            "example": "./data/storage"
          },
          "s3Bucket": {
            "type": "string",
            "example": ""
          },
          "s3Region": {
            "type": "string",
            "example": ""
          },
          "s3Endpoint": {
            "type": "string",
            "example": ""
          },
          "s3CredentialsSet": {
            "type": "boolean",
            "description": "Whether both an access key id and a secret are stored. Neither is ever returned.",
            "example": false
          }
        },
        "required": [
          "type",
          "builtIn",
          "localPath",
          "s3Bucket",
          "s3Region",
          "s3Endpoint",
          "s3CredentialsSet"
        ]
      },
      "InfraConfigEngineDto": {
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "example": "whatsapp-web.js"
          },
          "headless": {
            "type": "boolean",
            "example": true
          },
          "sessionDataPath": {
            "type": "string",
            "example": "./data/sessions"
          },
          "browserArgs": {
            "type": "string",
            "example": ""
          }
        },
        "required": [
          "type",
          "headless",
          "sessionDataPath",
          "browserArgs"
        ]
      },
      "InfraConfigResponseDto": {
        "type": "object",
        "properties": {
          "database": {
            "$ref": "#/components/schemas/InfraConfigDatabaseDto"
          },
          "redis": {
            "$ref": "#/components/schemas/InfraConfigRedisDto"
          },
          "queue": {
            "$ref": "#/components/schemas/InfraConfigQueueDto"
          },
          "storage": {
            "$ref": "#/components/schemas/InfraConfigStorageDto"
          },
          "engine": {
            "$ref": "#/components/schemas/InfraConfigEngineDto"
          }
        },
        "required": [
          "database",
          "redis",
          "queue",
          "storage",
          "engine"
        ]
      },
      "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"
          }
        }
      },
      "InfraConfigSaveResponseDto": {
        "type": "object",
        "properties": {
          "message": {
            "type": "string",
            "example": "Configuration saved successfully. Server restart required to apply changes."
          },
          "saved": {
            "type": "boolean",
            "description": "False when the write failed. This route answers 200 either way, so a client must read this flag rather than the status code.",
            "example": true
          },
          "envPath": {
            "type": "string",
            "description": "Path of the env file written, relative to the working directory so the host layout is not disclosed.",
            "example": "data/.env.generated"
          },
          "profiles": {
            "description": "Docker profiles the saved selection requires.",
            "example": [
              "postgres"
            ],
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "message",
          "saved",
          "envPath",
          "profiles"
        ]
      },
      "RestartDto": {
        "type": "object",
        "properties": {
          "profiles": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "profilesToRemove": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "InfraRestartResponseDto": {
        "type": "object",
        "properties": {
          "message": {
            "type": "string",
            "example": "Restart scheduled."
          },
          "restarting": {
            "type": "boolean",
            "description": "Whether a restart was actually scheduled.",
            "example": true
          },
          "profiles": {
            "description": "Docker profiles to bring up.",
            "example": [
              "postgres"
            ],
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "profilesToRemove": {
            "description": "Profiles to tear down. Teardown is stop-only: containers are stopped and retained for re-enable, never deleted.",
            "example": [],
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "estimatedTime": {
            "type": "number",
            "description": "Rough seconds before the process is expected back.",
            "example": 15
          },
          "orchestration": {
            "type": "object",
            "description": "Raw per-service orchestration outcome, present only when profiles were started."
          },
          "removal": {
            "type": "object",
            "description": "Raw per-service stop outcome, present only when profiles were torn down."
          }
        },
        "required": [
          "message",
          "restarting",
          "profiles",
          "profilesToRemove",
          "estimatedTime"
        ]
      },
      "MigrationTablesDto": {
        "type": "object",
        "properties": {
          "sessions": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "webhooks": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "messages": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "messageBatches": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "templates": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "baileysStoredMessages": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "lidMappings": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "pluginInstances": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "conversationMappings": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "ingressEvents": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "webhookDeliveryFailures": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "integrationDeliveryFailures": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "statusUpdates": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "automationRules": {
            "type": "array",
            "items": {
              "type": "object"
            }
          }
        },
        "required": [
          "sessions",
          "webhooks",
          "messages",
          "messageBatches",
          "templates",
          "baileysStoredMessages",
          "lidMappings",
          "pluginInstances",
          "conversationMappings",
          "ingressEvents",
          "webhookDeliveryFailures",
          "integrationDeliveryFailures",
          "statusUpdates",
          "automationRules"
        ]
      },
      "TableCountsDto": {
        "type": "object",
        "properties": {
          "sessions": {
            "type": "number",
            "example": 2
          },
          "webhooks": {
            "type": "number",
            "example": 1
          },
          "messages": {
            "type": "number",
            "example": 1024
          },
          "messageBatches": {
            "type": "number",
            "example": 0
          },
          "templates": {
            "type": "number",
            "example": 3
          },
          "baileysStoredMessages": {
            "type": "number",
            "example": 512
          },
          "lidMappings": {
            "type": "number",
            "example": 64
          },
          "pluginInstances": {
            "type": "number",
            "example": 12
          },
          "conversationMappings": {
            "type": "number",
            "example": 8
          },
          "ingressEvents": {
            "type": "number",
            "example": 40
          },
          "webhookDeliveryFailures": {
            "type": "number",
            "example": 0
          },
          "integrationDeliveryFailures": {
            "type": "number",
            "example": 0
          },
          "statusUpdates": {
            "type": "number",
            "example": 5
          },
          "automationRules": {
            "type": "number",
            "example": 2
          }
        },
        "required": [
          "sessions",
          "webhooks",
          "messages",
          "messageBatches",
          "templates",
          "baileysStoredMessages",
          "lidMappings",
          "pluginInstances",
          "conversationMappings",
          "ingressEvents",
          "webhookDeliveryFailures",
          "integrationDeliveryFailures",
          "statusUpdates",
          "automationRules"
        ]
      },
      "OmittedInlineMediaDto": {
        "type": "object",
        "properties": {
          "messages": {
            "type": "number",
            "description": "Message attachments replaced with an omitted marker.",
            "example": 0
          },
          "messageBatches": {
            "type": "number",
            "description": "Batch attachments replaced with an omitted marker.",
            "example": 0
          }
        },
        "required": [
          "messages",
          "messageBatches"
        ]
      },
      "InfraExportDataResponseDto": {
        "type": "object",
        "properties": {
          "exportedAt": {
            "type": "string",
            "description": "ISO-8601 timestamp the export was taken.",
            "example": "2026-08-07T12:00:00.000Z"
          },
          "dataDbType": {
            "type": "string",
            "description": "Driver the data came from.",
            "example": "postgres"
          },
          "tables": {
            "$ref": "#/components/schemas/MigrationTablesDto"
          },
          "counts": {
            "description": "Row counts, so a truncated restore is detectable.",
            "allOf": [
              {
                "$ref": "#/components/schemas/TableCountsDto"
              }
            ]
          },
          "skippedTables": {
            "description": "Tables the running database does not have, which were skipped rather than exported. A restore from this file will not repopulate them.",
            "example": [],
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "omittedInlineMedia": {
            "description": "Inline media dropped for exceeding the export budget. Non-zero means the archive is TRUNCATED β€” the rows are present but their attachments are replaced with an omitted marker.",
            "allOf": [
              {
                "$ref": "#/components/schemas/OmittedInlineMediaDto"
              }
            ]
          }
        },
        "required": [
          "exportedAt",
          "dataDbType",
          "tables",
          "counts",
          "skippedTables",
          "omittedInlineMedia"
        ]
      },
      "ImportDataDto": {
        "type": "object",
        "properties": {
          "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"
              }
            }
          },
          "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."
          },
          "exportedAt": {
            "type": "string",
            "description": "Ignored. Present so the export file posts back unmodified."
          },
          "dataDbType": {
            "type": "string",
            "description": "Ignored. Present so the export file posts back unmodified."
          },
          "counts": {
            "type": "object",
            "additionalProperties": {
              "type": "number"
            },
            "description": "Ignored. Present so the export file posts back unmodified."
          },
          "skippedTables": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Ignored. Present so the export file posts back unmodified."
          },
          "omittedInlineMedia": {
            "type": "object",
            "additionalProperties": {
              "type": "number"
            },
            "description": "Ignored. Present so the export file posts back unmodified."
          }
        },
        "required": [
          "tables"
        ]
      },
      "InfraImportDataResponseDto": {
        "type": "object",
        "properties": {
          "imported": {
            "type": "boolean",
            "example": true
          },
          "counts": {
            "description": "Rows written per table.",
            "allOf": [
              {
                "$ref": "#/components/schemas/TableCountsDto"
              }
            ]
          },
          "warnings": {
            "description": "Problems that did not stop the import.",
            "example": [],
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "notices": {
            "description": "Informational messages about what the import did.",
            "example": [],
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "restartRequired": {
            "type": "boolean",
            "description": "Whether the process must restart before the imported state is live.",
            "example": true
          },
          "orphanedEngines": {
            "description": "Sessions running here that the imported set does not contain β€” they were about to be deleted.",
            "example": [],
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "stoppedOrphanEngines": {
            "description": "Of those, the ones successfully stopped.",
            "example": [],
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "failedOrphanEngines": {
            "description": "Of those, the ones that could not be stopped.",
            "example": [],
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "imported",
          "counts",
          "warnings",
          "notices",
          "restartRequired",
          "orphanedEngines",
          "stoppedOrphanEngines",
          "failedOrphanEngines"
        ]
      },
      "StorageFileCountResponseDto": {
        "type": "object",
        "properties": {
          "storageType": {
            "type": "string",
            "enum": [
              "local",
              "s3"
            ],
            "example": "local"
          },
          "count": {
            "type": "number",
            "description": "Number of stored objects.",
            "example": 128
          },
          "sizeBytes": {
            "type": "number",
            "description": "Total size in bytes.",
            "example": 10485760
          },
          "sizeMB": {
            "type": "string",
            "description": "The same size in MB, pre-rendered to two decimals.",
            "example": "10.00"
          }
        },
        "required": [
          "storageType",
          "count",
          "sizeBytes",
          "sizeMB"
        ]
      },
      "StorageExportResponseDto": {
        "type": "object",
        "properties": {
          "message": {
            "type": "string",
            "example": "Storage archive created."
          },
          "download": {
            "type": "string",
            "description": "Path to download the archive from.",
            "example": "/api/infra/storage/download/xyz.tar"
          }
        },
        "required": [
          "message",
          "download"
        ]
      },
      "ImportStorageDto": {
        "type": "object",
        "properties": {
          "filePath": {
            "type": "string",
            "description": "Path to the tar.gz file to import (must be inside the data directory)"
          }
        },
        "required": [
          "filePath"
        ]
      },
      "StorageImportResponseDto": {
        "type": "object",
        "properties": {
          "imported": {
            "type": "boolean",
            "example": true
          },
          "count": {
            "type": "number",
            "description": "Objects written.",
            "example": 128
          },
          "storageType": {
            "type": "string",
            "enum": [
              "local",
              "s3"
            ],
            "example": "local"
          }
        },
        "required": [
          "imported",
          "count",
          "storageType"
        ]
      },
      "ContactDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Contact id in the active engine's native format. On Baileys this is frequently an `@lid` rather than a phone-number JID; use GET /contacts/{contactId}/phone to resolve one.",
            "example": "[email protected]"
          },
          "name": {
            "type": "string",
            "description": "The name from the account's own addressbook. Absent for a contact that was never saved.",
            "example": "Ada Lovelace"
          },
          "pushName": {
            "type": "string",
            "description": "The display name the contact set for themselves. Present even when the contact is not saved.",
            "example": "Ada"
          },
          "number": {
            "type": "string",
            "description": "MSISDN digits, without a leading + or any separators.",
            "example": "628123456789"
          },
          "isMyContact": {
            "type": "boolean",
            "description": "Whether the entry exists in the account's addressbook.",
            "example": true
          },
          "isBlocked": {
            "type": "boolean",
            "description": "Whether this account has blocked the contact.",
            "example": false
          },
          "profilePicUrl": {
            "type": "string",
            "description": "Profile picture URL. Absent when the contact has none or their privacy settings hide it.",
            "example": "https://pps.whatsapp.net/v/t61.24694-24/12345_678_910_n.jpg"
          }
        },
        "required": [
          "id",
          "number",
          "isMyContact",
          "isBlocked"
        ]
      },
      "ProfilePicturesResponseDto": {
        "type": "object",
        "properties": {
          "pictures": {
            "type": "object",
            "description": "Map of the requested contact ids to their picture URL. An id resolves to null when the contact has no picture, hides it, or its individual lookup failed β€” a per-id failure does not fail the batch. Ids beyond the first 50 are not looked up and are absent from the map.",
            "example": {
              "[email protected]": "https://pps.whatsapp.net/v/t61.24694-24/12345_678_910_n.jpg"
            },
            "additionalProperties": {
              "type": "string",
              "nullable": true
            }
          }
        },
        "required": [
          "pictures"
        ]
      },
      "NumberCheckResponseDto": {
        "type": "object",
        "properties": {
          "number": {
            "type": "string",
            "description": "The number exactly as supplied in the path.",
            "example": "628123456789"
          },
          "exists": {
            "type": "boolean",
            "description": "Whether the number is a registered WhatsApp account.",
            "example": true
          },
          "whatsappId": {
            "type": "string",
            "nullable": true,
            "description": "The canonical chat id in the engine's native format, or null when the number is not registered.",
            "example": "[email protected]"
          }
        },
        "required": [
          "number",
          "exists",
          "whatsappId"
        ]
      },
      "ProfilePictureResponseDto": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "nullable": true,
            "description": "Profile picture URL, or null when the contact has none or their privacy settings hide it.",
            "example": "https://pps.whatsapp.net/v/t61.24694-24/12345_678_910_n.jpg"
          }
        },
        "required": [
          "url"
        ]
      },
      "ResolvedPhoneResponseDto": {
        "type": "object",
        "properties": {
          "contactId": {
            "type": "string",
            "description": "The contact id exactly as supplied in the path.",
            "example": "12345678901234@lid"
          },
          "phone": {
            "type": "string",
            "nullable": true,
            "description": "MSISDN digits for the contact, or null when the engine has not learned the mapping. Best-effort: a null here is not a statement that the contact has no number.",
            "example": "628123456789"
          }
        },
        "required": [
          "contactId",
          "phone"
        ]
      },
      "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"
        ]
      },
      "ContactAckResponseDto": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "description": "Always true β€” a failure is reported as a non-2xx status, not as false.",
            "example": true
          },
          "message": {
            "type": "string",
            "description": "Human-readable confirmation of what was done.",
            "example": "Contact saved"
          }
        },
        "required": [
          "success",
          "message"
        ]
      },
      "GroupJoinInfoDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Group id.",
            "example": "[email protected]"
          },
          "name": {
            "type": "string",
            "description": "Group subject.",
            "example": "Engineering"
          },
          "description": {
            "type": "string",
            "description": "Group description.",
            "example": "Release coordination"
          },
          "owner": {
            "type": "string",
            "description": "Owner id in the engine's native format.",
            "example": "[email protected]"
          },
          "createdAt": {
            "type": "number",
            "description": "Unix SECONDS the group was created.",
            "example": 1786000000
          },
          "participantCount": {
            "type": "number",
            "description": "Member count, when disclosed.",
            "example": 42
          }
        },
        "required": [
          "id",
          "name"
        ]
      },
      "GroupParticipantDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Participant id in the engine's native format.",
            "example": "[email protected]"
          },
          "number": {
            "type": "string",
            "description": "MSISDN digits, without a leading + or separators.",
            "example": "628123456789"
          },
          "name": {
            "type": "string",
            "description": "Display name, when the engine reports one.",
            "example": "Ada Lovelace"
          },
          "isAdmin": {
            "type": "boolean",
            "description": "Whether the participant is a group admin.",
            "example": false
          },
          "isSuperAdmin": {
            "type": "boolean",
            "description": "Whether the participant created the group.",
            "example": false
          }
        },
        "required": [
          "id",
          "number",
          "isAdmin",
          "isSuperAdmin"
        ]
      },
      "GroupInfoDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Group id.",
            "example": "[email protected]"
          },
          "name": {
            "type": "string",
            "description": "Group subject.",
            "example": "Engineering"
          },
          "linkedParentJID": {
            "type": "string",
            "nullable": true,
            "description": "JID of the parent community, or null when the group is standalone.",
            "example": null
          },
          "description": {
            "type": "string",
            "description": "Group description.",
            "example": "Release coordination"
          },
          "owner": {
            "type": "string",
            "description": "Owner id in the engine's native format.",
            "example": "[email protected]"
          },
          "createdAt": {
            "type": "number",
            "description": "Unix SECONDS the group was created.",
            "example": 1786000000
          },
          "participants": {
            "description": "Members of the group.",
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GroupParticipantDto"
            }
          },
          "isReadOnly": {
            "type": "boolean",
            "description": "Whether this account can no longer post (left or removed).",
            "example": false
          },
          "isAnnounce": {
            "type": "boolean",
            "description": "Engine-reported announce flag.",
            "example": false
          },
          "announce": {
            "type": "boolean",
            "description": "Only admins may send messages.",
            "example": false
          },
          "locked": {
            "type": "boolean",
            "description": "Only admins may edit subject, description and picture.",
            "example": false
          },
          "ephemeralSeconds": {
            "type": "number",
            "description": "Disappearing-messages timer in seconds; 0 or absent means off.",
            "example": 0
          },
          "memberAddMode": {
            "type": "string",
            "enum": [
              "all",
              "admins"
            ],
            "description": "Who may add participants. Absent when the engine did not report it.",
            "example": "admins"
          }
        },
        "required": [
          "id",
          "name",
          "participants"
        ]
      },
      "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"
        ]
      },
      "GroupJoinedResponseDto": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "description": "Always true β€” a failure is reported as a non-2xx status.",
            "example": true
          },
          "groupId": {
            "type": "string",
            "description": "Id of the group that was joined.",
            "example": "[email protected]"
          }
        },
        "required": [
          "success",
          "groupId"
        ]
      },
      "GroupSettingsResponseDto": {
        "type": "object",
        "properties": {
          "announce": {
            "type": "boolean",
            "description": "Only admins may send messages.",
            "example": false
          },
          "locked": {
            "type": "boolean",
            "description": "Only admins may edit subject, description and picture.",
            "example": false
          },
          "ephemeralSeconds": {
            "type": "number",
            "description": "Disappearing-messages timer in seconds. Absent when the engine did not report it.",
            "example": 0
          },
          "memberAddMode": {
            "type": "string",
            "enum": [
              "all",
              "admins"
            ],
            "description": "Who may add participants. Absent when the engine did not report it.",
            "example": "admins"
          }
        }
      },
      "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"
            ]
          }
        }
      },
      "GroupAckResponseDto": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "description": "Always true β€” a failure is reported as a non-2xx status, not as false.",
            "example": true
          },
          "message": {
            "type": "string",
            "description": "Human-readable confirmation of what was done.",
            "example": "Group subject updated"
          }
        },
        "required": [
          "success",
          "message"
        ]
      },
      "CreateGroupDto": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Group subject/name",
            "maxLength": 100
          },
          "participants": {
            "description": "Participant WhatsApp IDs (e.g. [email protected])",
            "maxItems": 256,
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "name",
          "participants"
        ]
      },
      "GroupSummaryDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Group id.",
            "example": "[email protected]"
          },
          "name": {
            "type": "string",
            "description": "Group subject.",
            "example": "Engineering"
          },
          "participantsCount": {
            "type": "number",
            "description": "Member count, when the engine reports it.",
            "example": 42
          },
          "isAdmin": {
            "type": "boolean",
            "description": "Whether this account is an admin of the group.",
            "example": true
          },
          "linkedParentJID": {
            "type": "string",
            "nullable": true,
            "description": "JID of the parent community, or null when the group is standalone.",
            "example": null
          }
        },
        "required": [
          "id",
          "name"
        ]
      },
      "ParticipantsDto": {
        "type": "object",
        "properties": {
          "participants": {
            "description": "Participant WhatsApp IDs (e.g. [email protected])",
            "maxItems": 256,
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "participants"
        ]
      },
      "ParticipantOperationResultDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Neutral participant id the outcome belongs to.",
            "example": "[email protected]"
          },
          "success": {
            "type": "boolean",
            "description": "True only when the engine confirmed the change for this participant.",
            "example": true
          },
          "status": {
            "type": "number",
            "description": "The engine's own status code, when it gave one.",
            "example": 200
          },
          "message": {
            "type": "string",
            "description": "Engine-reported reason, when it gave one.",
            "example": "ok"
          }
        },
        "required": [
          "id",
          "success"
        ]
      },
      "ParticipantsOperationResponseDto": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "description": "Always true β€” a failure is reported as a non-2xx status, not as false.",
            "example": true
          },
          "message": {
            "type": "string",
            "description": "Human-readable confirmation of what was done.",
            "example": "Group subject updated"
          },
          "results": {
            "description": "One entry per requested participant.",
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ParticipantOperationResultDto"
            }
          }
        },
        "required": [
          "success",
          "message",
          "results"
        ]
      },
      "GroupMembershipRequestDto": {
        "type": "object",
        "properties": {
          "participantId": {
            "type": "string",
            "description": "Neutral id of the user asking to join.",
            "example": "[email protected]"
          },
          "addedById": {
            "type": "string",
            "description": "Who created the request, when the engine reports it (differs from the requester on a non-admin add).",
            "example": "[email protected]"
          },
          "method": {
            "type": "string",
            "description": "How the request was made, when the engine reports it.",
            "enum": [
              "invite_link",
              "non_admin_add",
              "linked_group_join"
            ],
            "example": "invite_link"
          },
          "requestedAt": {
            "type": "number",
            "description": "Unix seconds the request was created, when the engine reports it.",
            "example": 1754700000
          }
        },
        "required": [
          "participantId"
        ]
      },
      "MembershipRequestActionDto": {
        "type": "object",
        "properties": {
          "participants": {
            "description": "Requester WhatsApp IDs (e.g. [email protected]). Omit to act on every pending request.",
            "maxItems": 256,
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "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"
        ]
      },
      "GroupPictureResponseDto": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "nullable": true,
            "description": "Group picture URL, or null when the group has none.",
            "example": "https://pps.whatsapp.net/v/t61.24694-24/12345_678_910_n.jpg"
          }
        },
        "required": [
          "url"
        ]
      },
      "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"
          }
        }
      },
      "GroupInviteCodeResponseDto": {
        "type": "object",
        "properties": {
          "inviteCode": {
            "type": "string",
            "description": "The invite code on its own.",
            "example": "GTvX9c8H8l718ewOH22Zk5"
          },
          "inviteLink": {
            "type": "string",
            "description": "The same code as a joinable link.",
            "example": "https://chat.whatsapp.com/GTvX9c8H8l718ewOH22Zk5"
          }
        },
        "required": [
          "inviteCode",
          "inviteLink"
        ]
      },
      "GroupInviteCodeRevokedResponseDto": {
        "type": "object",
        "properties": {
          "inviteCode": {
            "type": "string",
            "description": "The invite code on its own.",
            "example": "GTvX9c8H8l718ewOH22Zk5"
          },
          "inviteLink": {
            "type": "string",
            "description": "The same code as a joinable link.",
            "example": "https://chat.whatsapp.com/GTvX9c8H8l718ewOH22Zk5"
          },
          "message": {
            "type": "string",
            "description": "Confirmation that the previous code was revoked.",
            "example": "Invite code revoked and new one generated"
          }
        },
        "required": [
          "inviteCode",
          "inviteLink",
          "message"
        ]
      },
      "SetProfileNameDto": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "New display name (WhatsApp limit: 25 characters)",
            "maxLength": 25
          }
        },
        "required": [
          "name"
        ]
      },
      "ProfileAckResponseDto": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "description": "Always true β€” a failure is reported as a non-2xx status, not as false.",
            "example": true
          },
          "message": {
            "type": "string",
            "description": "Human-readable confirmation of what was changed.",
            "example": "Profile name updated"
          }
        },
        "required": [
          "success",
          "message"
        ]
      },
      "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"
          }
        }
      },
      "CreateCallLinkDto": {
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "description": "Which kind of call the link opens. WhatsApp's own URL path for `audio` is `/voice/`.",
            "enum": [
              "audio",
              "video"
            ],
            "example": "video"
          },
          "startTime": {
            "type": "number",
            "description": "Absolute epoch-MILLISECONDS timestamp the call is scheduled to start at. Required: whatsapp-web.js generates an event-linked call and has no notion of \"no start time\", so a link for right now is `Date.now()` rather than an omitted field.",
            "example": 1800000000000
          }
        },
        "required": [
          "type",
          "startTime"
        ]
      },
      "CallLinkResponseDto": {
        "type": "object",
        "properties": {
          "link": {
            "type": "string",
            "description": "The shareable WhatsApp call link.",
            "example": "https://call.whatsapp.com/video/XxXxXxXxXxXxXx"
          }
        },
        "required": [
          "link"
        ]
      },
      "CallAckResponseDto": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "description": "Always true β€” a failure is reported as a non-2xx status, not as false.",
            "example": true
          }
        },
        "required": [
          "success"
        ]
      },
      "LabelDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Label id, assigned by WhatsApp.",
            "example": "1"
          },
          "name": {
            "type": "string",
            "description": "Label text.",
            "example": "Paid"
          },
          "hexColor": {
            "type": "string",
            "description": "Display colour as hex. The write path takes a colour INDEX (0-19) instead β€” neither engine exposes the index-to-hex mapping, so the two directions deliberately differ.",
            "example": "#5bc0de"
          }
        },
        "required": [
          "id",
          "name",
          "hexColor"
        ]
      },
      "LabelChatDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Chat id in the engine's native format.",
            "example": "[email protected]"
          },
          "name": {
            "type": "string",
            "description": "Chat title β€” contact name or group subject.",
            "example": "Ada Lovelace"
          },
          "isGroup": {
            "type": "boolean",
            "description": "Retained for back-compat; `kind` is the full discriminator.",
            "example": false
          },
          "kind": {
            "type": "string",
            "description": "User-facing chat kind.",
            "example": "individual"
          },
          "unreadCount": {
            "type": "number",
            "description": "Unread messages in this chat.",
            "example": 0
          },
          "timestamp": {
            "type": "number",
            "description": "Unix SECONDS of the most recent activity.",
            "example": 1786000000
          },
          "lastMessage": {
            "type": "string",
            "description": "Preview of the last message, when there is one.",
            "example": "See you then"
          }
        },
        "required": [
          "id",
          "name",
          "isGroup",
          "kind",
          "unreadCount",
          "timestamp"
        ]
      },
      "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
          }
        }
      },
      "LabelAckResponseDto": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "description": "Always true β€” a failure is reported as a non-2xx status, not as false.",
            "example": true
          }
        },
        "required": [
          "success"
        ]
      },
      "ChannelDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Channel (newsletter) id.",
            "example": "120363000000000000@newsletter"
          },
          "name": {
            "type": "string",
            "description": "Channel name.",
            "example": "Release notes"
          },
          "description": {
            "type": "string",
            "description": "Channel description, when set."
          },
          "inviteCode": {
            "type": "string",
            "description": "Invite code, when the engine discloses one. Absent does not mean the channel is private.",
            "example": "0029Va..."
          },
          "subscriberCount": {
            "type": "number",
            "description": "Subscriber count, when the engine reports it.",
            "example": 1024
          },
          "picture": {
            "type": "string",
            "description": "Channel picture URL, when set."
          },
          "verified": {
            "type": "boolean",
            "description": "Whether WhatsApp marks the channel verified.",
            "example": false
          },
          "createdAt": {
            "type": "number",
            "description": "Unix SECONDS the channel was created, when reported.",
            "example": 1786000000
          }
        },
        "required": [
          "id",
          "name"
        ]
      },
      "ChannelMessageDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Message id.",
            "example": "ABCD1234"
          },
          "body": {
            "type": "string",
            "description": "Message text. Empty for a media-only post.",
            "example": "v0.14.4 is out"
          },
          "timestamp": {
            "type": "number",
            "description": "Unix SECONDS the message was posted.",
            "example": 1786000000
          },
          "hasMedia": {
            "type": "boolean",
            "description": "Whether the post carries media.",
            "example": false
          },
          "mediaUrl": {
            "type": "string",
            "description": "Media URL, when the engine resolved one."
          }
        },
        "required": [
          "id",
          "body",
          "timestamp",
          "hasMedia"
        ]
      },
      "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"
        ]
      },
      "ChannelAckResponseDto": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "description": "Always true β€” a failure is reported as a non-2xx status, not as false.",
            "example": true
          }
        },
        "required": [
          "success"
        ]
      },
      "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"
        ]
      },
      "DemoteChannelAdminDto": {
        "type": "object",
        "properties": {
          "userId": {
            "type": "string",
            "description": "WhatsApp ID of the admin to demote back to a subscriber β€” a phone number, `<phone>@c.us` or `<lid>@lid`; a bare number is qualified for you. Anything that does not name an individual is rejected with 400 rather than handed to WhatsApp.",
            "example": "[email protected]"
          }
        },
        "required": [
          "userId"
        ]
      },
      "TransferChannelOwnershipDto": {
        "type": "object",
        "properties": {
          "newOwnerId": {
            "type": "string",
            "description": "WhatsApp ID of the account that becomes the new owner β€” a phone number, `<phone>@c.us` or `<lid>@lid`; a bare number is qualified for you. Anything that does not name an individual is rejected with 400 rather than handed to WhatsApp. This is irreversible: once the transfer lands, this session can no longer take the channel back.",
            "example": "[email protected]"
          }
        },
        "required": [
          "newOwnerId"
        ]
      },
      "OverviewSessionsDto": {
        "type": "object",
        "properties": {
          "active": {
            "type": "number",
            "description": "Sessions currently connected.",
            "example": 2
          },
          "total": {
            "type": "number",
            "description": "Sessions on record, connected or not.",
            "example": 5
          },
          "byStatus": {
            "type": "object",
            "description": "Session count per status value.",
            "example": {
              "ready": 2,
              "disconnected": 3
            },
            "additionalProperties": {
              "type": "integer"
            }
          }
        },
        "required": [
          "active",
          "total",
          "byStatus"
        ]
      },
      "OverviewTodayDto": {
        "type": "object",
        "properties": {
          "sent": {
            "type": "number",
            "example": 12
          },
          "received": {
            "type": "number",
            "example": 34
          }
        },
        "required": [
          "sent",
          "received"
        ]
      },
      "OverviewMessagesDto": {
        "type": "object",
        "properties": {
          "sent": {
            "type": "number",
            "description": "All-time sent count.",
            "example": 1024
          },
          "received": {
            "type": "number",
            "description": "All-time received count.",
            "example": 2048
          },
          "failed": {
            "type": "number",
            "description": "Sends the gateway recorded as failed.",
            "example": 3
          },
          "today": {
            "description": "Today's counts, in the server's timezone.",
            "allOf": [
              {
                "$ref": "#/components/schemas/OverviewTodayDto"
              }
            ]
          }
        },
        "required": [
          "sent",
          "received",
          "failed",
          "today"
        ]
      },
      "OverviewStatsResponseDto": {
        "type": "object",
        "properties": {
          "sessions": {
            "$ref": "#/components/schemas/OverviewSessionsDto"
          },
          "messages": {
            "$ref": "#/components/schemas/OverviewMessagesDto"
          }
        },
        "required": [
          "sessions",
          "messages"
        ]
      },
      "TimeSeriesPointDto": {
        "type": "object",
        "properties": {
          "timestamp": {
            "type": "string",
            "description": "Bucket start, ISO-8601.",
            "example": "2026-08-07T12:00:00.000Z"
          },
          "sent": {
            "type": "number",
            "example": 12
          },
          "received": {
            "type": "number",
            "example": 34
          }
        },
        "required": [
          "timestamp",
          "sent",
          "received"
        ]
      },
      "StatsBySessionDto": {
        "type": "object",
        "properties": {
          "sessionId": {
            "type": "string",
            "example": "0a941dac-a965-45e7-b318-74ae8be134f0"
          },
          "name": {
            "type": "string",
            "example": "primary"
          },
          "sent": {
            "type": "number",
            "example": 12
          },
          "received": {
            "type": "number",
            "example": 34
          }
        },
        "required": [
          "sessionId",
          "name",
          "sent",
          "received"
        ]
      },
      "StatsTopChatDto": {
        "type": "object",
        "properties": {
          "chatId": {
            "type": "string",
            "example": "[email protected]"
          },
          "chatName": {
            "type": "string",
            "nullable": true,
            "description": "Null when no name is known for the chat."
          },
          "messageCount": {
            "type": "number",
            "example": 42
          }
        },
        "required": [
          "chatId",
          "chatName",
          "messageCount"
        ]
      },
      "MessageStatsResponseDto": {
        "type": "object",
        "properties": {
          "timeSeries": {
            "description": "One point per bucket over the requested period.",
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TimeSeriesPointDto"
            }
          },
          "byType": {
            "type": "object",
            "description": "Message count per message type.",
            "example": {
              "text": 900,
              "image": 124
            },
            "additionalProperties": {
              "type": "integer"
            }
          },
          "bySession": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/StatsBySessionDto"
            }
          },
          "topChats": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/StatsTopChatDto"
            }
          }
        },
        "required": [
          "timeSeries",
          "byType",
          "bySession",
          "topChats"
        ]
      },
      "SessionStatsSessionDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "0a941dac-a965-45e7-b318-74ae8be134f0"
          },
          "name": {
            "type": "string",
            "example": "primary"
          },
          "status": {
            "type": "string",
            "example": "ready"
          }
        },
        "required": [
          "id",
          "name",
          "status"
        ]
      },
      "SessionStatsMessagesDto": {
        "type": "object",
        "properties": {
          "sent": {
            "type": "number",
            "example": 1024
          },
          "received": {
            "type": "number",
            "example": 2048
          },
          "today": {
            "type": "number",
            "example": 46
          },
          "failed": {
            "type": "number",
            "example": 3
          }
        },
        "required": [
          "sent",
          "received",
          "today",
          "failed"
        ]
      },
      "SessionStatsTopChatDto": {
        "type": "object",
        "properties": {
          "chatId": {
            "type": "string",
            "example": "[email protected]"
          },
          "chatName": {
            "type": "string",
            "nullable": true,
            "description": "Null when no name is known for the chat."
          },
          "count": {
            "type": "number",
            "example": 42
          },
          "lastActive": {
            "type": "string",
            "description": "ISO-8601 timestamp of the last message.",
            "example": "2026-08-07T12:00:00.000Z"
          }
        },
        "required": [
          "chatId",
          "chatName",
          "count",
          "lastActive"
        ]
      },
      "SessionHourlyActivityDto": {
        "type": "object",
        "properties": {
          "hour": {
            "type": "number",
            "description": "Hour of day, 0-23.",
            "example": 9
          },
          "sent": {
            "type": "number",
            "example": 12
          },
          "received": {
            "type": "number",
            "example": 34
          }
        },
        "required": [
          "hour",
          "sent",
          "received"
        ]
      },
      "SessionStatsResponseDto": {
        "type": "object",
        "properties": {
          "session": {
            "$ref": "#/components/schemas/SessionStatsSessionDto"
          },
          "messages": {
            "$ref": "#/components/schemas/SessionStatsMessagesDto"
          },
          "topChats": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SessionStatsTopChatDto"
            }
          },
          "hourlyActivity": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SessionHourlyActivityDto"
            }
          }
        },
        "required": [
          "session",
          "messages",
          "topChats",
          "hourlyActivity"
        ]
      },
      "StatusContactDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Poster id.",
            "example": "[email protected]"
          },
          "name": {
            "type": "string",
            "description": "Name from the account's addressbook, when saved."
          },
          "pushName": {
            "type": "string",
            "description": "Name the poster set for themselves.",
            "example": "Ada"
          }
        },
        "required": [
          "id"
        ]
      },
      "StatusDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Status id.",
            "example": "ABCD1234"
          },
          "contact": {
            "description": "Who posted it.",
            "allOf": [
              {
                "$ref": "#/components/schemas/StatusContactDto"
              }
            ]
          },
          "type": {
            "type": "string",
            "enum": [
              "text",
              "image",
              "video",
              "voice"
            ],
            "description": "`voice` is an audio status posted as a voice note. Before voice posting existed, such a status read back as `text`, because anything that was not an image or a video collapsed to it.",
            "example": "image"
          },
          "caption": {
            "type": "string",
            "description": "Caption, for an image or video status."
          },
          "mediaUrl": {
            "type": "string",
            "description": "Media URL as the engine reported it."
          },
          "media": {
            "type": "object",
            "description": "Downloaded media bytes, present only when the engine fetched them and they fit the inbound media cap. Absent is not an error β€” fetch the bytes from the media route instead."
          },
          "backgroundColor": {
            "type": "string",
            "description": "Background colour of a text or voice status.",
            "example": "#0a5c36"
          },
          "font": {
            "type": "number",
            "description": "Font index of a text or voice status.",
            "example": 2
          },
          "timestamp": {
            "type": "string",
            "description": "ISO-8601 timestamp the status was posted.",
            "example": "2026-08-07T12:00:00.000Z"
          },
          "expiresAt": {
            "type": "string",
            "description": "ISO-8601 timestamp the status expires.",
            "example": "2026-08-08T12:00:00.000Z"
          }
        },
        "required": [
          "id",
          "contact",
          "type",
          "timestamp",
          "expiresAt"
        ]
      },
      "StatusListResponseDto": {
        "type": "object",
        "properties": {
          "statuses": {
            "description": "Statuses, newest first.",
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/StatusDto"
            }
          }
        },
        "required": [
          "statuses"
        ]
      },
      "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": [
              "[email protected]"
            ],
            "maxItems": 256,
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "text"
        ]
      },
      "StatusResultDto": {
        "type": "object",
        "properties": {
          "statusId": {
            "type": "string",
            "description": "Id of the status that was posted.",
            "example": "ABCD1234"
          },
          "timestamp": {
            "type": "string",
            "description": "ISO-8601 timestamp the engine stamped on it.",
            "example": "2026-08-07T12:00:00.000Z"
          },
          "expiresAt": {
            "type": "string",
            "description": "ISO-8601 timestamp it expires β€” 24 hours later.",
            "example": "2026-08-08T12:00:00.000Z"
          }
        },
        "required": [
          "statusId",
          "timestamp",
          "expiresAt"
        ]
      },
      "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": [
              "[email protected]"
            ],
            "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": [
              "[email protected]"
            ],
            "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": [
              "[email protected]"
            ],
            "maxItems": 256,
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "audio"
        ]
      },
      "StatusDeletedResponseDto": {
        "type": "object",
        "properties": {
          "message": {
            "type": "string",
            "example": "Status deleted successfully"
          }
        },
        "required": [
          "message"
        ]
      },
      "ConversionStatusResponseDto": {
        "type": "object",
        "properties": {
          "available": {
            "type": "boolean",
            "description": "True only when conversion is switched on AND the ffmpeg binary is runnable. False means a convert call will be refused, so check this before offering conversion in a UI.",
            "example": true
          }
        },
        "required": [
          "available"
        ]
      },
      "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..."
          }
        }
      },
      "ConvertedMediaResponseDto": {
        "type": "object",
        "properties": {
          "base64": {
            "type": "string",
            "description": "The converted bytes, ready to hand straight to a send endpoint's `base64` field.",
            "example": "T2dnUwACAAAAAAAAAAA..."
          },
          "mimetype": {
            "type": "string",
            "description": "The type the bytes now are β€” not the type they were.",
            "example": "audio/ogg; codecs=opus"
          },
          "bytes": {
            "type": "number",
            "description": "Decoded size, so a caller can check a send limit without decoding.",
            "example": 20480
          }
        },
        "required": [
          "base64",
          "mimetype",
          "bytes"
        ]
      },
      "CatalogDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Catalog id. Synthesized from the first collection β€” the only named grouping the engine exposes.",
            "example": "1234567890123456"
          },
          "name": {
            "type": "string",
            "description": "Collection name.",
            "example": "Default"
          },
          "description": {
            "type": "string",
            "description": "Collection description, when the engine reports one."
          },
          "productCount": {
            "type": "number",
            "description": "How many products the first collection holds.",
            "example": 12
          },
          "url": {
            "type": "string",
            "description": "Public catalog link for the account.",
            "example": "https://wa.me/c/628123456789"
          }
        },
        "required": [
          "id",
          "name",
          "productCount",
          "url"
        ]
      },
      "ProductDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Product id in the engine's native format.",
            "example": "7891234567890"
          },
          "name": {
            "type": "string",
            "description": "Product name.",
            "example": "Kopi Gayo 200g"
          },
          "description": {
            "type": "string",
            "description": "Product description, when set."
          },
          "price": {
            "type": "number",
            "description": "Price in the currency’s minor-unit-free numeric form.",
            "example": 85000
          },
          "currency": {
            "type": "string",
            "description": "ISO currency code.",
            "example": "IDR"
          },
          "priceFormatted": {
            "type": "string",
            "description": "Price rendered for display. Synthesized by the gateway from price + currency, so an unrecognised currency code falls back to a plain \"CODE amount\" pair.",
            "example": "IDR 85,000.00"
          },
          "imageUrl": {
            "type": "string",
            "description": "First product image URL. Absent when the product carries no image.",
            "example": "https://pps.whatsapp.net/v/t61.24694-24/12345_678_910_n.jpg"
          },
          "url": {
            "type": "string",
            "description": "Product link, empty when the engine reports none.",
            "example": "https://wa.me/p/123/628"
          },
          "isAvailable": {
            "type": "boolean",
            "description": "Whether the engine reports the product as in stock.",
            "example": true
          },
          "retailerId": {
            "type": "string",
            "description": "The merchant's own SKU, when set.",
            "example": "SKU-001"
          }
        },
        "required": [
          "id",
          "name",
          "price",
          "currency",
          "priceFormatted",
          "url",
          "isAvailable"
        ]
      },
      "ProductPaginationDto": {
        "type": "object",
        "properties": {
          "page": {
            "type": "number",
            "description": "Page that was returned (1-based).",
            "example": 1
          },
          "limit": {
            "type": "number",
            "description": "Page size that was applied.",
            "example": 20
          },
          "total": {
            "type": "number",
            "description": "Total products in the catalog, not on this page.",
            "example": 12
          },
          "totalPages": {
            "type": "number",
            "description": "Total pages at this page size.",
            "example": 1
          }
        },
        "required": [
          "page",
          "limit",
          "total",
          "totalPages"
        ]
      },
      "PaginatedProductsDto": {
        "type": "object",
        "properties": {
          "products": {
            "description": "Products on the requested page.",
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ProductDto"
            }
          },
          "pagination": {
            "$ref": "#/components/schemas/ProductPaginationDto"
          }
        },
        "required": [
          "products",
          "pagination"
        ]
      },
      "SendProductDto": {
        "type": "object",
        "properties": {
          "chatId": {
            "type": "string",
            "description": "Chat to send the product card to (@c.us or @g.us).",
            "example": "[email protected]"
          },
          "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"
        ]
      },
      "ProductMessageResponseDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The message id, assigned when the gateway accepts the message for sending. Note the field name: the send routes served by MessageService answer `messageId` for the same value. A 2xx here means the message was handed to the WhatsApp client, not that it was delivered.\n\nThis route is Baileys-only, so the id is the bare key id Baileys assigns β€” not the `true_<jid>_<id>` serialized form the whatsapp-web.js send routes report.",
            "example": "3EB0C767D26B8A3F6E"
          },
          "timestamp": {
            "type": "number",
            "description": "Unix SECONDS the engine stamped on the outgoing message.",
            "example": 1786000000
          }
        },
        "required": [
          "id",
          "timestamp"
        ]
      },
      "SendCatalogDto": {
        "type": "object",
        "properties": {
          "chatId": {
            "type": "string",
            "description": "Chat to send the catalog to (@c.us or @g.us).",
            "example": "[email protected]"
          },
          "body": {
            "type": "string",
            "description": "Optional body text accompanying the catalog.",
            "example": "Browse our full catalog"
          }
        },
        "required": [
          "chatId"
        ]
      },
      "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"
        ]
      },
      "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"
        ]
      },
      "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": "string",
            "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": "string",
            "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."
          }
        }
      },
      "SearchHitDto": {
        "type": "object",
        "properties": {
          "messageId": {
            "type": "string",
            "description": "Gateway message row id.",
            "example": "7c2e5a91-3b8d-4f6a-a05c-9d1e4b7f2306"
          },
          "waMessageId": {
            "type": "string",
            "description": "WhatsApp's own message id.",
            "example": "[email protected]_3EB0123"
          },
          "sessionId": {
            "type": "string",
            "example": "0a941dac-a965-45e7-b318-74ae8be134f0"
          },
          "chatId": {
            "type": "string",
            "example": "[email protected]"
          },
          "body": {
            "type": "string",
            "description": "Full message text.",
            "example": "See you at 10"
          },
          "snippet": {
            "type": "string",
            "description": "Excerpt with `<mark>` highlight markers. Render it as TEXT, never as HTML β€” the markers come from the provider and the body is user-supplied.",
            "example": "See you at <mark>10</mark>"
          },
          "timestamp": {
            "type": "number",
            "description": "Unix SECONDS of the message.",
            "example": 1786000000
          },
          "type": {
            "type": "string",
            "example": "text"
          },
          "direction": {
            "type": "string",
            "enum": [
              "incoming",
              "outgoing"
            ],
            "example": "incoming"
          },
          "from": {
            "type": "string",
            "description": "Sender id.",
            "example": "[email protected]"
          },
          "score": {
            "type": "number",
            "description": "Relevance score, when the provider reports one.",
            "example": 0.87
          }
        },
        "required": [
          "messageId",
          "waMessageId",
          "sessionId",
          "chatId",
          "body",
          "snippet",
          "timestamp",
          "type",
          "direction",
          "from"
        ]
      },
      "SearchResultsResponseDto": {
        "type": "object",
        "properties": {
          "hits": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SearchHitDto"
            }
          },
          "total": {
            "type": "number",
            "description": "Bounded exact count for pagination.",
            "example": 42
          },
          "tookMs": {
            "type": "number",
            "description": "How long the provider took, in milliseconds.",
            "example": 12
          },
          "provider": {
            "type": "string",
            "description": "Which provider answered.",
            "example": "builtin-fts"
          }
        },
        "required": [
          "hits",
          "total",
          "tookMs",
          "provider"
        ]
      }
    }
  },
  "security": [
    {
      "X-API-Key": []
    }
  ]
}