openapi: 3.0.3
info:
  title: VPNresellers API
  version: "4.1"
  description: |
    Welcome to the VPNresellers API version 4.1.

    Get an access token on the [API access](https://app.vpnresellers.com/api-access) page.

    Most endpoints require `Authorization: Bearer {token}`.

    JSON responses include a top-level `code` field equal to the HTTP status code.

    For configuration endpoints, `Accept` must be exactly one of:
    - `application/json` — return configuration as JSON
    - `text/html; charset=UTF-8` — download the configuration file
  contact:
    name: VPNresellers
    url: https://vpnresellers.com
  license:
    name: Proprietary
    url: https://vpnresellers.com
servers:
  - url: https://api.vpnresellers.com/v4_1
    description: Production
tags:
  - name: Accounts
    description: VPN account management
  - name: Accounts by username
    description: Username-based account operations (`/un/accounts`)
  - name: Configuration
    description: OpenVPN, WireGuard and VLESS configuration retrieval/download
  - name: Servers
    description: OpenVPN/WireGuard server list
  - name: Vless Servers
    description: VLESS server list
  - name: Ports
    description: OpenVPN port list
  - name: Geo Location
    description: Client geo IP lookup
  - name: Profile
    description: Authenticated reseller profile
  - name: Customers
    description: Customer management
  - name: Projects
    description: Project management
paths:
  /accounts/check_username:
    get:
      tags: [Accounts]
      summary: Check a username
      description: "Checks if a username is free to create. Does not verify password — use `validateAccount` for that."
      operationId: checkUsername
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/AcceptJson'
        - name: username
          in: query
          required: true
          description: Username to check
          schema:
            type: string
            minLength: 3
            maxLength: 50
            pattern: '^([0-9a-zA-Z\s\-@_.]*)$'
      responses:
        '200':
          description: Username is available
          content:
            application/json:
              schema:
                type: object
                required: [message, code]
                properties:
                  message:
                    type: string
                    example: The username checking was successfully.
                  code:
                    type: integer
                    example: 200
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '405':
          $ref: '#/components/responses/MethodNotAllowed'
        '422':
          $ref: '#/components/responses/ValidationError'

  /accounts:
    get:
      tags: [Accounts]
      summary: List accounts
      description: "Paginated accounts for the current reseller. Optional `status` (enum name) and `per_page`."
      operationId: listAccounts
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/AcceptJson'
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
        - name: status
          in: query
          required: false
          description: |
            Filter by account status enum name.
            Possible values: `NotActive`, `Active`, `Cancelled`, `Suspended`, `Terminated`, `Expired`.
          schema:
            $ref: '#/components/schemas/AccountStatus'
      responses:
        '200':
          description: Paginated list of accounts
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedAccounts'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '405':
          $ref: '#/components/responses/MethodNotAllowed'
    post:
      tags: [Accounts]
      summary: Create an account
      description: "Creates a VPN account. Needs non-negative balance (`402` otherwise). Optional nested `customer` (link by id or create inline)."
      operationId: createAccount
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/AcceptJson'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateAccountRequest'
            example:
              username: eius
              password: secret1
      responses:
        '201':
          description: Account created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AccountEnvelope'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/InsufficientBalance'
        '405':
          $ref: '#/components/responses/MethodNotAllowed'
        '422':
          $ref: '#/components/responses/ValidationError'

  /accounts/validate:
    post:
      tags: [Accounts]
      summary: Validate an account
      description: "Checks username+password of an existing account. Returns 460/461 on failure (body not wrapped in `data`)."
      operationId: validateAccount
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/AcceptJson'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ValidateAccountRequest'
      responses:
        '200':
          description: Valid credentials
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidateAccountSuccess'
              example:
                id: 12345
                success: true
                message: Username has been successfully validated.
                code: 200
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '405':
          $ref: '#/components/responses/MethodNotAllowed'
        '422':
          $ref: '#/components/responses/ValidationError'
        '460':
          description: Username not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidateAccountFailure'
              example:
                success: false
                message: Username has not been found.
                code: 460
        '461':
          description: Password check failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidateAccountFailure'
              example:
                success: false
                message: Username has not been validated.
                code: 461

  /accounts/{account}:
    parameters:
      - $ref: '#/components/parameters/AccountId'
      - $ref: '#/components/parameters/AcceptJson'
    get:
      tags: [Accounts]
      summary: Retrieve an account
      description: Returns one account by numeric ID. Must belong to the reseller.
      operationId: getAccount
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Account details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AccountEnvelope'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '405':
          $ref: '#/components/responses/MethodNotAllowed'
    delete:
      tags: [Accounts]
      summary: Delete an account
      description: Permanently deletes the account. Must belong to the reseller.
      operationId: deleteAccount
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Account deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EmptySuccess'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '405':
          $ref: '#/components/responses/MethodNotAllowed'

  /accounts/{account}/enable:
    put:
      tags: [Accounts]
      summary: Enable an account
      description: "Sets `status` to `Active` by account ID (Bearer). Prefer this over `/un/accounts` when you have the ID."
      operationId: enableAccount
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/AccountId'
        - $ref: '#/components/parameters/AcceptJson'
      responses:
        '200':
          description: Account enabled
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AccountEnvelope'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '405':
          $ref: '#/components/responses/MethodNotAllowed'

  /accounts/{account}/disable:
    put:
      tags: [Accounts]
      summary: Disable an account
      description: "Sets `status` to `Suspended` by account ID (Bearer)."
      operationId: disableAccount
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/AccountId'
        - $ref: '#/components/parameters/AcceptJson'
      responses:
        '200':
          description: Account disabled
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AccountEnvelope'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '405':
          $ref: '#/components/responses/MethodNotAllowed'

  /accounts/{account}/change_password:
    put:
      tags: [Accounts]
      summary: Change account password
      description: "Changes password by account ID. Body: `password` (6–50 chars)."
      operationId: changeAccountPassword
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/AccountId'
        - $ref: '#/components/parameters/AcceptJson'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ChangePasswordRequest'
      responses:
        '200':
          description: Password changed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AccountEnvelope'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '405':
          $ref: '#/components/responses/MethodNotAllowed'
        '422':
          $ref: '#/components/responses/ValidationError'

  /accounts/{account}/expire:
    put:
      tags: [Accounts]
      summary: Expire an account
      description: "Sets `expire_at` (`Y-m-d`). Null/omitted means auto-renewal."
      operationId: expireAccount
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/AccountId'
        - $ref: '#/components/parameters/AcceptJson'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ExpireAccountRequest'
            example:
              expire_at: "2023-04-18"
      responses:
        '200':
          description: Expiration updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AccountEnvelope'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '405':
          $ref: '#/components/responses/MethodNotAllowed'
        '422':
          $ref: '#/components/responses/ValidationError'

  /un/accounts/enable:
    put:
      tags: [Accounts by username]
      summary: Enable an account by username
      description: "Sets `status` to `Active` by `username` in the body (not path ID)."
      operationId: enableAccountByUsername
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/AcceptJson'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UsernameOnlyRequest'
      responses:
        '200':
          description: Account enabled
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AccountEnvelope'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '405':
          $ref: '#/components/responses/MethodNotAllowed'
        '422':
          $ref: '#/components/responses/ValidationError'

  /un/accounts/disable:
    put:
      tags: [Accounts by username]
      summary: Disable an account by username
      description: "Sets `status` to `Suspended` by `username` in the body."
      operationId: disableAccountByUsername
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/AcceptJson'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UsernameOnlyRequest'
      responses:
        '200':
          description: Account disabled
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AccountEnvelope'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '405':
          $ref: '#/components/responses/MethodNotAllowed'
        '422':
          $ref: '#/components/responses/ValidationError'

  /un/accounts/change_password:
    put:
      tags: [Accounts by username]
      summary: Change password by username
      description: "Changes password by `username` and `password` in the body."
      operationId: changePasswordByUsername
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/AcceptJson'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UsernamePasswordRequest'
      responses:
        '200':
          description: Password changed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AccountEnvelope'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '405':
          $ref: '#/components/responses/MethodNotAllowed'
        '422':
          $ref: '#/components/responses/ValidationError'

  /un/accounts/show:
    get:
      tags: [Accounts by username]
      summary: Retrieve an account by username
      description: "HTTP Basic (`username:password`). Query `username` must match the authenticated account. Requires non-negative balance."
      operationId: getAccountByUsername
      security:
        - basicAuth: []
      parameters:
        - $ref: '#/components/parameters/AcceptJson'
        - name: username
          in: query
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Account details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AccountEnvelope'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/InsufficientBalance'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '405':
          $ref: '#/components/responses/MethodNotAllowed'

  /configuration/openvpn:
    get:
      tags: [Configuration]
      summary: Get an OpenVPN configuration
      description: "OpenVPN config for `server_id` + required `port_id`. JSON or file download via `Accept`."
      operationId: getOpenVpnConfiguration
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/ConfigAccept'
        - name: server_id
          in: query
          required: true
          schema:
            type: integer
            minimum: 1
        - name: port_id
          in: query
          required: true
          schema:
            type: integer
            minimum: 1
      responses:
        '200':
          description: Configuration JSON or file download
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConfigurationEnvelope'
            text/html:
              schema:
                type: string
                format: binary
        '400':
          description: Wrong Accept header
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorMessage'
              example:
                message: Wrong header
                code: 400
        '401':
          $ref: '#/components/responses/Unauthorized'
        '405':
          $ref: '#/components/responses/MethodNotAllowed'
        '422':
          $ref: '#/components/responses/ValidationError'

  /configuration/wireguard:
    get:
      tags: [Configuration]
      summary: Get a WireGuard configuration
      description: "WireGuard config for `server_id` + `account_id`. JSON or file download via `Accept`."
      operationId: getWireguardConfiguration
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/ConfigAccept'
        - name: server_id
          in: query
          required: true
          schema:
            type: integer
            minimum: 1
        - name: account_id
          in: query
          required: true
          description: Account ID
          schema:
            type: integer
            minimum: 1
      responses:
        '200':
          description: Configuration JSON or file download
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConfigurationEnvelope'
            text/html:
              schema:
                type: string
                format: binary
        '400':
          description: Wrong Accept header
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorMessage'
              example:
                message: Wrong header
                code: 400
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '405':
          $ref: '#/components/responses/MethodNotAllowed'
        '422':
          $ref: '#/components/responses/ValidationError'

  /configuration/vless:
    get:
      tags: [Configuration]
      summary: Get a VLESS configuration
      description: "VLESS config for `server_id` + `account_id`. JSON or file download via `Accept`."
      operationId: getVlessConfiguration
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/ConfigAccept'
        - name: server_id
          in: query
          required: true
          description: VLESS server ID
          schema:
            type: integer
            minimum: 1
        - name: account_id
          in: query
          required: true
          schema:
            type: integer
            minimum: 1
      responses:
        '200':
          description: Configuration JSON or file download
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConfigurationEnvelope'
            text/html:
              schema:
                type: string
                format: binary
        '400':
          description: Wrong Accept header
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorMessage'
              example:
                message: Wrong header
                code: 400
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '405':
          $ref: '#/components/responses/MethodNotAllowed'
        '422':
          $ref: '#/components/responses/ValidationError'

  /servers:
    get:
      tags: [Servers]
      summary: List servers
      description: "OpenVPN/WireGuard servers. Use `id` with configuration endpoints."
      operationId: listServers
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/AcceptJson'
      responses:
        '200':
          description: List of servers
          content:
            application/json:
              schema:
                type: object
                required: [data, code]
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Server'
                  code:
                    type: integer
                    example: 200
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '405':
          $ref: '#/components/responses/MethodNotAllowed'

  /vless-servers:
    get:
      tags: [Vless Servers]
      summary: List VLESS servers
      description: "VLESS servers. Use `id` as `server_id` for VLESS configuration."
      operationId: listVlessServers
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/AcceptJson'
      responses:
        '200':
          description: List of VLESS servers
          content:
            application/json:
              schema:
                type: object
                required: [data, code]
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Server'
                  code:
                    type: integer
                    example: 200
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '405':
          $ref: '#/components/responses/MethodNotAllowed'

  /ports:
    get:
      tags: [Ports]
      summary: List ports
      description: "OpenVPN ports. Use `id` as `port_id` for OpenVPN configuration."
      operationId: listPorts
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/AcceptJson'
      responses:
        '200':
          description: List of ports
          content:
            application/json:
              schema:
                type: object
                required: [data, code]
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Port'
                  code:
                    type: integer
                    example: 200
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '405':
          $ref: '#/components/responses/MethodNotAllowed'

  /geoip:
    get:
      tags: [Geo Location]
      summary: Get geo info
      description: Geo data for the client IP. No authentication.
      operationId: getGeoIp
      security: []
      parameters:
        - $ref: '#/components/parameters/AcceptJson'
      responses:
        '200':
          description: Geo IP information
          content:
            application/json:
              schema:
                type: object
                required: [data, code]
                properties:
                  data:
                    $ref: '#/components/schemas/GeoIp'
                  code:
                    type: integer
                    example: 200
        '400':
          $ref: '#/components/responses/BadRequest'
        '405':
          $ref: '#/components/responses/MethodNotAllowed'

  /profile:
    get:
      tags: [Profile]
      summary: Retrieve profile
      description: Reseller profile for the current token (balance).
      operationId: getProfile
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/AcceptJson'
      responses:
        '200':
          description: Profile
          content:
            application/json:
              schema:
                type: object
                required: [data, code]
                properties:
                  data:
                    $ref: '#/components/schemas/Profile'
                  code:
                    type: integer
                    example: 200
              example:
                data:
                  balance: "253.70"
                code: 200
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '405':
          $ref: '#/components/responses/MethodNotAllowed'

  /customers:
    get:
      tags: [Customers]
      summary: List customers
      description: Paginated customers for the current reseller.
      operationId: listCustomers
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/AcceptJson'
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
      responses:
        '200':
          description: Paginated customers
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedCustomers'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '405':
          $ref: '#/components/responses/MethodNotAllowed'
    post:
      tags: [Customers]
      summary: Create a customer
      description: "Creates a customer under a project. Required: email, first_name, last_name, project_id."
      operationId: createCustomer
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/AcceptJson'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateCustomerRequest'
      responses:
        '200':
          description: Customer created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CustomerEnvelope'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '405':
          $ref: '#/components/responses/MethodNotAllowed'
        '422':
          $ref: '#/components/responses/ValidationError'

  /customers/{customer}:
    parameters:
      - $ref: '#/components/parameters/CustomerId'
      - $ref: '#/components/parameters/AcceptJson'
    get:
      tags: [Customers]
      summary: Retrieve a customer
      description: Returns one customer by numeric ID.
      operationId: getCustomer
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Customer details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CustomerEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
    put:
      tags: [Customers]
      summary: Edit a customer
      description: "Partial update of customer fields. Success response is empty with HTTP `201`."
      operationId: updateCustomer
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateCustomerRequest'
      responses:
        '201':
          description: Customer updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EmptySuccess'
              example:
                code: 201
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationError'
    delete:
      tags: [Customers]
      summary: Delete a customer
      description: "Deletes customer. Fails with `422` if it still has accounts."
      operationId: deleteCustomer
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Customer deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EmptySuccess'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationError'

  /customers/{customer}/accounts:
    get:
      tags: [Customers]
      summary: List customer accounts
      description: Paginated VPN accounts linked to this customer.
      operationId: listCustomerAccounts
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/CustomerId'
        - $ref: '#/components/parameters/AcceptJson'
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
      responses:
        '200':
          description: Paginated accounts for customer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedAccounts'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  /projects:
    get:
      tags: [Projects]
      summary: List projects
      description: Paginated projects for the current reseller.
      operationId: listProjects
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/AcceptJson'
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
      responses:
        '200':
          description: Paginated projects
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedProjects'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
    post:
      tags: [Projects]
      summary: Create a project
      description: "Creates a project. Required: name, description."
      operationId: createProject
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/AcceptJson'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateProjectRequest'
      responses:
        '200':
          description: Project created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProjectEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '422':
          $ref: '#/components/responses/ValidationError'

  /projects/{project}:
    parameters:
      - $ref: '#/components/parameters/ProjectId'
      - $ref: '#/components/parameters/AcceptJson'
    get:
      tags: [Projects]
      summary: Retrieve a project
      description: Returns one project by numeric ID.
      operationId: getProject
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Project details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProjectEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
    put:
      tags: [Projects]
      summary: Edit a project
      description: "Updates project name/description. Success response is empty with HTTP `201`."
      operationId: updateProject
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateProjectRequest'
      responses:
        '201':
          description: Project updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EmptySuccess'
              example:
                code: 201
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationError'
    delete:
      tags: [Projects]
      summary: Delete a project
      description: "Deletes project. Fails with `422` if it still has customers."
      operationId: deleteProject
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Project deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EmptySuccess'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationError'

  /projects/{project}/customers:
    get:
      tags: [Projects]
      summary: List project customers
      description: Paginated customers in this project.
      operationId: listProjectCustomers
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/ProjectId'
        - $ref: '#/components/parameters/AcceptJson'
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
      responses:
        '200':
          description: Paginated customers for project
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedCustomers'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  /projects/{project}/accounts:
    get:
      tags: [Projects]
      summary: List project accounts
      description: Paginated VPN accounts in this project.
      operationId: listProjectAccounts
      security:
        - bearerAuth: []
      parameters:
        - $ref: '#/components/parameters/ProjectId'
        - $ref: '#/components/parameters/AcceptJson'
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
      responses:
        '200':
          description: Paginated accounts for project
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedAccounts'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: token
      description: API access token from API access page.
    basicAuth:
      type: http
      scheme: basic
      description: Account username and password (Base64 of `username:password`).

  parameters:
    AcceptJson:
      name: Accept
      in: header
      required: false
      description: Prefer `application/json`
      schema:
        type: string
        default: application/json
        example: application/json
    ConfigAccept:
      name: Accept
      in: header
      required: true
      description: |
        Must be exactly `application/json` or `text/html; charset=UTF-8`.
      schema:
        type: string
        enum:
          - application/json
          - text/html; charset=UTF-8
    AccountId:
      name: account
      in: path
      required: true
      schema:
        type: integer
        minimum: 1
      example: 1
    CustomerId:
      name: customer
      in: path
      required: true
      schema:
        type: integer
        minimum: 1
      example: 1
    ProjectId:
      name: project
      in: path
      required: true
      schema:
        type: integer
        minimum: 1
      example: 1
    Page:
      name: page
      in: query
      required: false
      schema:
        type: integer
        minimum: 1
        default: 1
    PerPage:
      name: per_page
      in: query
      required: false
      description: Items per page (default 15)
      schema:
        type: integer
        minimum: 1
        maximum: 10000
        default: 15

  schemas:
    AccountStatus:
      type: string
      enum: [NotActive, Active, Cancelled, Suspended, Terminated, Expired]

    Account:
      type: object
      required: [id, username, status, updated, created, wg_ip, wg_private_key, wg_public_key, expire_at]
      properties:
        id:
          type: integer
          example: 9
        username:
          type: string
          example: plmkojnhbgv
        status:
          $ref: '#/components/schemas/AccountStatus'
        wg_ip:
          type: string
          nullable: true
          example: 10.250.121.219
        wg_private_key:
          type: string
          nullable: true
        wg_public_key:
          type: string
          nullable: true
        expire_at:
          type: string
          nullable: true
          description: |
            Expiration datetime. Request body for expire uses `Y-m-d`.
            Response may be an ISO-8601 datetime or `null` (auto-renewal).
          example: "2023-04-18T00:00:00.000000Z"
        updated:
          type: string
          example: "2017-03-28 17:44:21"
        created:
          type: string
          example: "2017-03-24 19:26:24"

    AccountEnvelope:
      type: object
      required: [data, code]
      properties:
        data:
          $ref: '#/components/schemas/Account'
        code:
          type: integer
          example: 200

    CreateAccountRequest:
      type: object
      required: [username, password]
      properties:
        username:
          type: string
          minLength: 3
          maxLength: 50
          pattern: '^([0-9a-zA-Z\s\-@_.]*)$'
          description: VPN username; must be unique in `vpn_accounts` and RADIUS `radcheck`
        password:
          type: string
          minLength: 6
          maxLength: 50
          format: password
          description: VPN account password
        customer:
          description: |
            Optional. Attach the new account to a customer using **exactly one** mode:

            1. **Link existing** — send only `customer_id` (must exist in `customers`).
            2. **Create inline** — send `email`, `first_name`, `last_name`, and `project_id`
               together (no `customer_id`). Email must be unique; `project_id` must exist.

            Mixing modes (e.g. `customer_id` together with `email`) fails validation (`422`).
          oneOf:
            - title: LinkExistingCustomer
              type: object
              required: [customer_id]
              additionalProperties: false
              properties:
                customer_id:
                  type: integer
                  minimum: 1
                  description: ID of an existing customer to link
            - title: CreateCustomerInline
              type: object
              required: [email, first_name, last_name, project_id]
              additionalProperties: false
              properties:
                email:
                  type: string
                  format: email
                  description: New customer email; must be unique in `customers`
                first_name:
                  type: string
                  maxLength: 255
                  description: New customer first name
                last_name:
                  type: string
                  maxLength: 255
                  description: New customer last name
                project_id:
                  type: integer
                  minimum: 1
                  description: Project ID for the new customer; must exist in `projects`

    ChangePasswordRequest:
      type: object
      required: [password]
      properties:
        password:
          type: string
          minLength: 6
          maxLength: 50
          format: password

    ExpireAccountRequest:
      type: object
      properties:
        expire_at:
          type: string
          format: date
          nullable: true
          example: "2023-04-18"

    UsernameOnlyRequest:
      type: object
      required: [username]
      properties:
        username:
          type: string
          maxLength: 250

    UsernamePasswordRequest:
      type: object
      required: [username, password]
      properties:
        username:
          type: string
        password:
          type: string
          format: password

    ValidateAccountRequest:
      type: object
      required: [username, password]
      properties:
        username:
          type: string
          maxLength: 512
        password:
          type: string
          maxLength: 512
          format: password

    ValidateAccountSuccess:
      type: object
      required: [success, message, code, id]
      properties:
        id:
          type: integer
        success:
          type: boolean
          example: true
        message:
          type: string
        code:
          type: integer
          example: 200

    ValidateAccountFailure:
      type: object
      required: [success, message, code]
      properties:
        success:
          type: boolean
          example: false
        message:
          type: string
        code:
          type: integer
          enum: [460, 461]

    Configuration:
      type: object
      required: [content, name]
      properties:
        content:
          type: string
          description: Configuration file body
        name:
          type: string
          description: Suggested filename
          example: TH-123.ovpn

    ConfigurationEnvelope:
      type: object
      required: [data, code]
      properties:
        data:
          $ref: '#/components/schemas/Configuration'
        code:
          type: integer
          example: 200

    Server:
      type: object
      required: [id, name, ip, country_code, city]
      properties:
        id:
          type: integer
        name:
          type: string
        ip:
          type: string
        country_code:
          type: string
        city:
          type: string
        capacity:
          type: integer
          example: 0
      additionalProperties: true

    Port:
      type: object
      required: [id, protocol, number, default]
      properties:
        id:
          type: integer
        protocol:
          type: string
          example: udp
        number:
          type: integer
          example: 1194
        default:
          type: integer
          enum: [0, 1]

    GeoIp:
      type: object
      required: [ip, country, iso_code, city, lat, lon, continent]
      properties:
        ip:
          type: string
        country:
          type: string
        iso_code:
          type: string
        city:
          type: string
        lat:
          type: number
        lon:
          type: number
        continent:
          type: string

    Profile:
      type: object
      required: [balance]
      properties:
        balance:
          type: string
          description: Reseller balance formatted to 2 decimals
          example: "253.70"

    Customer:
      type: object
      required: [id, first_name, last_name, email, user_id, project_id]
      properties:
        id:
          type: integer
        first_name:
          type: string
        last_name:
          type: string
        email:
          type: string
          format: email
        user_id:
          type: integer
        project_id:
          type: integer

    CustomerEnvelope:
      type: object
      required: [data, code]
      properties:
        data:
          $ref: '#/components/schemas/Customer'
        code:
          type: integer

    CreateCustomerRequest:
      type: object
      required: [email, first_name, last_name, project_id]
      properties:
        email:
          type: string
          format: email
        first_name:
          type: string
          minLength: 1
          maxLength: 50
        last_name:
          type: string
          minLength: 1
          maxLength: 50
        project_id:
          type: integer

    UpdateCustomerRequest:
      type: object
      properties:
        email:
          type: string
          format: email
        first_name:
          type: string
          minLength: 3
          maxLength: 50
        last_name:
          type: string
          minLength: 3
          maxLength: 50

    Project:
      type: object
      required: [id, name, description, user_id]
      properties:
        id:
          type: integer
        name:
          type: string
        description:
          type: string
        user_id:
          type: integer

    ProjectEnvelope:
      type: object
      required: [data, code]
      properties:
        data:
          $ref: '#/components/schemas/Project'
        code:
          type: integer

    CreateProjectRequest:
      type: object
      required: [name, description]
      properties:
        name:
          type: string
          minLength: 3
          maxLength: 50
        description:
          type: string
          minLength: 0
          maxLength: 500

    PaginationLinks:
      type: object
      properties:
        first:
          type: string
          nullable: true
        last:
          type: string
          nullable: true
        prev:
          type: string
          nullable: true
        next:
          type: string
          nullable: true

    PaginationMeta:
      type: object
      properties:
        current_page:
          type: integer
        from:
          type: integer
          nullable: true
        last_page:
          type: integer
        path:
          type: string
        per_page:
          type: integer
        to:
          type: integer
          nullable: true
        total:
          type: integer

    PaginatedAccounts:
      type: object
      required: [data, links, meta, code]
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Account'
        links:
          $ref: '#/components/schemas/PaginationLinks'
        meta:
          $ref: '#/components/schemas/PaginationMeta'
        code:
          type: integer
          example: 200

    PaginatedCustomers:
      type: object
      required: [data, links, meta, code]
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Customer'
        links:
          $ref: '#/components/schemas/PaginationLinks'
        meta:
          $ref: '#/components/schemas/PaginationMeta'
        code:
          type: integer

    PaginatedProjects:
      type: object
      required: [data, links, meta, code]
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Project'
        links:
          $ref: '#/components/schemas/PaginationLinks'
        meta:
          $ref: '#/components/schemas/PaginationMeta'
        code:
          type: integer

    EmptySuccess:
      type: object
      required: [code]
      properties:
        code:
          type: integer
          example: 200

    ErrorMessage:
      type: object
      required: [message]
      properties:
        message:
          type: string
        code:
          type: integer

    ValidationErrorBody:
      type: object
      required: [message, errors]
      properties:
        message:
          type: string
          example: The given data was invalid.
        errors:
          type: object
          additionalProperties:
            type: array
            items:
              type: string
        code:
          type: integer
          example: 422

  responses:
    BadRequest:
      description: Bad Request
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorMessage'
          example:
            message: Bad Request.
            code: 400
    Unauthorized:
      description: Unauthorized
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorMessage'
          example:
            message: Unauthorized.
            code: 401
    Forbidden:
      description: Forbidden
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorMessage'
          example:
            message: Forbidden.
            code: 403
    NotFound:
      description: Not Found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorMessage'
          example:
            message: Not Found.
            code: 404
    MethodNotAllowed:
      description: Method Not Allowed
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorMessage'
          example:
            message: Method Not Allowed.
            code: 405
    InsufficientBalance:
      description: Insufficient balance
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorMessage'
          example:
            message: Insufficient Balance.
            code: 402
    ValidationError:
      description: Validation failed
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ValidationErrorBody'

security:
  - bearerAuth: []
