openapi: 3.0.3
info:
  title: Paradarum CDN API
  version: v1
  description: |
    The **Paradarum Developer API** lets you automate your CDN: purge cache, manage
    properties and rules, pull statistics, and open support tickets from your own tooling
    or CMS plugins.

    ## Base URL

    ```
    https://api.paradarum.com
    ```

    All paths are prefixed with `/api` and are **case-insensitive** (`/api/property` and
    `/api/Property` are equivalent). There is no version segment in the URL.

    ## Authentication

    There are two ways to authenticate. Most automation should use an **API key**.

    | Mode | Header | Use it for |
    | --- | --- | --- |
    | **API key** | `X-API-Key: pdm_…` | Automation, CMS plugins, cache purge. Create keys in the panel under **Account → API Keys**. |
    | **Bearer (JWT)** | `Authorization: Bearer <jwt>` | The panel / interactive sessions. Obtain a token from `POST /api/auth/login`. |

    Send the **raw** key as the `X-API-Key` value — there is no `Bearer` word for API keys.
    Your account is derived from the credential, so you never send an `accountId` for
    property or purge endpoints (the Tickets endpoints are the exception and take an
    `accountId` query parameter).

    ## Response envelope

    Every response is wrapped in a standard envelope:

    ```json
    {
      "succeeded": true,
      "message": "",
      "data": { },
      "pageNumber": 1,
      "pageSize": 10,
      "totalPages": 1,
      "totalRecords": 1
    }
    ```

    `succeeded` is `true` for any HTTP status below 400. There is no machine-readable error
    code — branch on the HTTP status and read `message`. The pagination fields are only
    meaningful on list endpoints.

    ## Rate limits

    No rate limiting is enforced on the public API today. Purge is **asynchronous**: a `200`
    with a `jobCount` means the purge was **queued**, not that the cache is already cleared.
  contact:
    name: Paradarum Support
    url: https://docs.paradarum.com/support/open-a-ticket

servers:
  - url: https://api.paradarum.com
    description: Production
  - url: http://localhost:5252
    description: Local development

tags:
  - name: Cache Purge
    description: Invalidate cached content for a property. Authenticate with an API key.
  - name: API Keys
    description: Create and revoke the API keys used by automation and CMS plugins.
  - name: Properties
    description: List and inspect your properties and their general settings.
  - name: Rules
    description: Manage cache and header rules at the edge.
  - name: Statistics
    description: Pull raw access logs and summary metrics. Export logs to CSV.
  - name: Support Tickets
    description: Open and manage support tickets.
  - name: Authentication
    description: Obtain a JWT for panel-style access.

security:
  - ApiKeyAuth: []
  - BearerAuth: []

paths:
  /api/property/{id}/purge:
    post:
      tags: [Cache Purge]
      summary: Purge cache (single instruction)
      description: |
        Queue a cache invalidation for one property. The property must have at least one
        **Active** hostname and a healthy edge, otherwise you get `400`/`500`. Disabled or
        suspended properties return `403`.
      operationId: purgeCache
      security:
        - ApiKeyAuth: []
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/PropertyId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PurgeRequest'
            examples:
              all:
                summary: Purge everything
                value: { type: All }
              path:
                summary: Purge a single path
                value: { type: Path, value: /images/logo.png }
              extension:
                summary: Purge by file extension
                value: { type: Extension, value: jpg }
              regex:
                summary: Purge by pattern
                value: { type: Regex, value: "^/blog/", matchType: starts_with }
      responses:
        '200':
          description: Purge accepted and queued.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PurgeResultEnvelope'
              example:
                succeeded: true
                message: "Purge request accepted. 12 jobs queued across 2 shields + 4 PoPs."
                data: { jobCount: 12, shields: 2, nodes: 4, hostnames: 1 }
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PropertyDisabled'
        '404':
          $ref: '#/components/responses/PropertyNotFound'
        '500':
          $ref: '#/components/responses/NoActiveNodes'

  /api/property/{id}/purge/batch:
    post:
      tags: [Cache Purge]
      summary: Purge cache (batch)
      description: |
        Queue several purge instructions in one request. Each item is validated
        independently: valid items are queued and invalid ones are returned in `rejected`.
        Only an all-invalid batch returns `400`.
      operationId: purgeCacheBatch
      security:
        - ApiKeyAuth: []
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/PropertyId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PurgeBatchRequest'
            example:
              items:
                - { type: All }
                - { type: Path, value: /index.html }
                - { type: Extension, value: css }
                - { type: Regex, value: "^/assets/", matchType: regex }
      responses:
        '200':
          description: Batch processed (some items may be rejected).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BatchPurgeResultEnvelope'
              example:
                succeeded: true
                message: "3 of 4 purge items queued."
                data:
                  jobCount: 9
                  accepted: 3
                  rejected:
                    - { index: 2, reason: "Value is required for type 'extension'." }
                  shields: 2
                  nodes: 4
                  hostnames: 1
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/PropertyDisabled'
        '404':
          $ref: '#/components/responses/PropertyNotFound'

  /api/account/apikeys:
    get:
      tags: [API Keys]
      summary: List API keys
      description: Returns your account's API keys. The raw key is never returned — only the `keyPrefix`.
      operationId: listApiKeys
      security:
        - BearerAuth: []
      responses:
        '200':
          description: List of API keys.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/ApiKey' }
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      tags: [API Keys]
      summary: Create an API key
      description: |
        Creates a new API key. The **raw key is returned exactly once** in `data.key` — store
        it securely, it cannot be retrieved later. Maximum **10 active keys** per account.
      operationId: createApiKey
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name:
                  type: string
                  description: A human label so you can recognise the key later.
                  example: WordPress purge plugin
      responses:
        '201':
          description: API key created. The raw key is shown only here.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/ApiKeyCreated' }
              example:
                succeeded: true
                message: "API key created successfully."
                data:
                  id: 12
                  name: WordPress purge plugin
                  key: pdm_AbCdEf0123456789AbCdEf012345
                  keyPrefix: pdm_AbCd
                  createdAt: "2026-06-10T12:00:00Z"
        '400':
          description: Maximum number of active API keys reached (10).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Envelope' }
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/account/apikeys/{keyId}:
    delete:
      tags: [API Keys]
      summary: Revoke an API key
      description: Soft-revokes a key. This is **irreversible** — the key stops working immediately.
      operationId: revokeApiKey
      security:
        - BearerAuth: []
      parameters:
        - name: keyId
          in: path
          required: true
          schema: { type: integer }
      responses:
        '200':
          description: Key revoked.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Envelope' }
        '400':
          description: Key already revoked.
        '404':
          description: API key not found.

  /api/property:
    get:
      tags: [Properties]
      summary: List properties
      description: Paginated list of your properties, including their origins, hostnames and rules.
      operationId: listProperties
      parameters:
        - $ref: '#/components/parameters/PageNumber'
        - $ref: '#/components/parameters/PageSize'
        - $ref: '#/components/parameters/OrderBy'
        - $ref: '#/components/parameters/Filter'
      responses:
        '200':
          description: Paginated properties.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/Property' }
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/property/{id}:
    get:
      tags: [Properties]
      summary: Get a property
      operationId: getProperty
      parameters:
        - $ref: '#/components/parameters/PropertyId'
      responses:
        '200':
          description: The property.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/Property' }
        '404':
          $ref: '#/components/responses/PropertyNotFound'

  /api/property/{id}/general-settings:
    get:
      tags: [Properties]
      summary: Get general settings
      description: Returns the property-wide settings JSON (security headers, caching, image optimization, origin shield).
      operationId: getGeneralSettings
      parameters:
        - $ref: '#/components/parameters/PropertyId'
      responses:
        '200':
          description: General settings.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/GeneralSettings' }
    put:
      tags: [Properties]
      summary: Update general settings
      description: |
        Replaces the general settings object. **The API does not merge** — always send the
        full object or you will overwrite other fields. Returns `403` if the property is
        disabled or suspended.
      operationId: updateGeneralSettings
      parameters:
        - $ref: '#/components/parameters/PropertyId'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/GeneralSettings' }
            example:
              hsts: { enabled: true, max_age: 31536000, include_subdomains: true, preload: true }
              clickjacking_protection: true
              mime_sniffing_protection: true
              always_online: { enabled: true, codes: [error, timeout, "500", "502", "503", "504"] }
              query_string: { mode: except, values: [page, lang] }
              browser_caching: { mode: override, ttl: 3600 }
              image_optimization: { enabled: true, quality: 80, max_width: 4000, max_height: 4000, only_if_smaller: true }
              origin_shield: { enabled: true, shield_id: 3 }
      responses:
        '200':
          description: General settings updated.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Envelope' }
        '403':
          $ref: '#/components/responses/PropertyDisabled'

  /api/property/{id}/rules:
    get:
      tags: [Rules]
      summary: List rules
      operationId: listRules
      parameters:
        - $ref: '#/components/parameters/PropertyId'
        - name: includeSystem
          in: query
          description: Include platform-managed (system) rules.
          schema: { type: boolean, default: false }
      responses:
        '200':
          description: Rules for the property.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/Rule' }

  /api/property/{id}/rules/cache:
    post:
      tags: [Rules]
      summary: Create a cache rule
      operationId: createCacheRule
      parameters:
        - $ref: '#/components/parameters/PropertyId'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CacheRuleInput' }
            example:
              name: Cache static assets
              pattern: "~* \\.(jpg|jpeg|png|gif|webp|svg|css|js|woff2)$"
              matchType: 4
              ttlSeconds: 2592000
              order: 1
              isEnabled: true
      responses:
        '201':
          description: Cache rule created.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Envelope' }

  /api/property/{id}/rules/cache/{ruleId}:
    put:
      tags: [Rules]
      summary: Update a cache rule
      operationId: updateCacheRule
      parameters:
        - $ref: '#/components/parameters/PropertyId'
        - $ref: '#/components/parameters/RuleId'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CacheRuleInput' }
      responses:
        '200': { description: Cache rule updated. }
        '404': { description: Rule not found for this property. }

  /api/property/{id}/rules/header:
    post:
      tags: [Rules]
      summary: Create a header rule
      description: Returns `400` if `headerName` is a protected header (see `GET /api/protected-headers`).
      operationId: createHeaderRule
      parameters:
        - $ref: '#/components/parameters/PropertyId'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/HeaderRuleInput' }
            example:
              name: CORS for players
              pattern: ""
              matchType: 1
              action: 1
              headerName: Access-Control-Allow-Origin
              headerValue: "*"
              applyToRequest: false
              order: 3
              isEnabled: true
      responses:
        '201': { description: Header rule created. }
        '400': { description: Header is protected and cannot be modified by user rules. }

  /api/property/{id}/rules/header/{ruleId}:
    put:
      tags: [Rules]
      summary: Update a header rule
      operationId: updateHeaderRule
      parameters:
        - $ref: '#/components/parameters/PropertyId'
        - $ref: '#/components/parameters/RuleId'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/HeaderRuleInput' }
      responses:
        '200': { description: Header rule updated. }

  /api/property/{id}/rules/{ruleId}:
    delete:
      tags: [Rules]
      summary: Delete a rule
      operationId: deleteRule
      parameters:
        - $ref: '#/components/parameters/PropertyId'
        - $ref: '#/components/parameters/RuleId'
      responses:
        '200': { description: Rule deleted. }
        '404': { description: Rule not found for this property. }

  /api/monitoring/logs:
    get:
      tags: [Statistics]
      summary: List raw access logs
      operationId: listLogs
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/From'
        - $ref: '#/components/parameters/To'
        - $ref: '#/components/parameters/PropertyIdQuery'
        - name: search
          in: query
          schema: { type: string }
        - name: statusCode
          in: query
          description: A multiple of 100 (e.g. `400`) filters the whole category; any other value is an exact match.
          schema: { type: integer }
        - name: method
          in: query
          schema: { type: string, example: GET }
        - name: page
          in: query
          schema: { type: integer, default: 1 }
        - name: pageSize
          in: query
          schema: { type: integer, default: 50 }
      responses:
        '200':
          description: Page of log entries.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/LogEntry' }

  /api/monitoring/logs/export:
    get:
      tags: [Statistics]
      summary: Export raw logs to CSV
      description: |
        Streams matching access logs as a CSV download. Honors the same filters as
        `/api/monitoring/logs` but **ignores pagination** and is capped at the **most recent
        100,000 rows**. Returns `403` if your account has no hostnames. Only raw logs can be
        exported — aggregate statistics have no export endpoint.
      operationId: exportLogs
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/From'
        - $ref: '#/components/parameters/To'
        - $ref: '#/components/parameters/PropertyIdQuery'
        - name: search
          in: query
          schema: { type: string }
        - name: statusCode
          in: query
          schema: { type: integer }
        - name: method
          in: query
          schema: { type: string }
      responses:
        '200':
          description: "CSV file. Columns: Time, Host, Method, URI, Status, Size, Duration_ms, IP, Country, Device, Browser, OS, Cache."
          content:
            text/csv:
              schema:
                type: string
                format: binary
        '403':
          description: The account has no hostnames.

  /api/monitoring/stats/summary:
    get:
      tags: [Statistics]
      summary: Summary metrics
      operationId: statsSummary
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/From'
        - $ref: '#/components/parameters/To'
        - $ref: '#/components/parameters/PropertyIdQuery'
      responses:
        '200':
          description: Summary counters for the window.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/StatsSummary' }
              example:
                succeeded: true
                message: success
                data:
                  totalRequests: 1532210
                  totalBytes: 84210334567
                  cacheHitRate: 92.41
                  avgResponseTime: 38.2
                  errors4xx: 1204
                  errors403: 12
                  errors404: 990
                  errors429: 3
                  errors5xx: 27
                  uniqueVisitors: 48211

  /api/ticket:
    get:
      tags: [Support Tickets]
      summary: List tickets
      operationId: listTickets
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/AccountIdQuery'
      responses:
        '200':
          description: The account's tickets, newest first.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/Ticket' }
    post:
      tags: [Support Tickets]
      summary: Open a ticket
      operationId: createTicket
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/AccountIdQuery'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CreateTicketRequest' }
            example:
              subject: SSL certificate not provisioning
              description: My site example.com shows a TLS error after adding the domain.
              topic: 4
              propertyId: 55
      responses:
        '201':
          description: Ticket created.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/Ticket' }
        '400':
          description: Missing subject/description, or invalid property.

  /api/ticket/{id}:
    get:
      tags: [Support Tickets]
      summary: Get a ticket with messages
      operationId: getTicket
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/TicketId'
        - $ref: '#/components/parameters/AccountIdQuery'
      responses:
        '200':
          description: Ticket detail with ordered messages.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Envelope' }

  /api/ticket/{id}/message:
    post:
      tags: [Support Tickets]
      summary: Reply to a ticket
      description: Replying to a **Resolved** ticket automatically reopens it.
      operationId: addTicketMessage
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/TicketId'
        - $ref: '#/components/parameters/AccountIdQuery'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [message]
              properties:
                message: { type: string }
      responses:
        '201': { description: Message added. }
        '400': { description: Empty message. }

  /api/ticket/{id}/close:
    post:
      tags: [Support Tickets]
      summary: Close a ticket
      operationId: closeTicket
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/TicketId'
        - $ref: '#/components/parameters/AccountIdQuery'
      responses:
        '200': { description: Ticket closed. }
        '400': { description: Ticket already closed. }

  /api/auth/login:
    post:
      tags: [Authentication]
      summary: Log in and get a JWT
      description: "Returns a JWT in `data.Token`, valid for 24h. Use it as `Authorization: Bearer <token>`."
      operationId: login
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [username, password]
              properties:
                username: { type: string, example: you@example.com }
                password: { type: string, format: password }
      responses:
        '200':
          description: Authenticated.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          Token: { type: string }
                          Message: { type: string }
        '400':
          description: Invalid credentials.

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: Send your raw API key (`pdm_…`) as the header value.
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

  parameters:
    PropertyId:
      name: id
      in: path
      required: true
      description: Numeric property ID.
      schema: { type: integer, example: 123 }
    RuleId:
      name: ruleId
      in: path
      required: true
      schema: { type: integer }
    TicketId:
      name: id
      in: path
      required: true
      schema: { type: integer }
    AccountIdQuery:
      name: accountId
      in: query
      required: true
      description: Your account ID. Required on all Ticket endpoints.
      schema: { type: integer }
    PropertyIdQuery:
      name: propertyId
      in: query
      schema: { type: integer }
    From:
      name: from
      in: query
      description: Start of the window (UTC ISO‑8601).
      schema: { type: string, format: date-time }
    To:
      name: to
      in: query
      description: End of the window (UTC ISO‑8601).
      schema: { type: string, format: date-time }
    PageNumber:
      name: PageNumber
      in: query
      schema: { type: integer, default: 1 }
    PageSize:
      name: PageSize
      in: query
      schema: { type: integer, default: 10, maximum: 9999 }
    OrderBy:
      name: OrderBy
      in: query
      description: e.g. `createdAt desc`.
      schema: { type: string, default: Id asc }
    Filter:
      name: Filter
      in: query
      description: URL-encoded LIKE expression, e.g. `name.Contains("cdn")`.
      schema: { type: string }

  schemas:
    Envelope:
      type: object
      properties:
        succeeded: { type: boolean, example: true }
        message: { type: string }
        data: {}
        pageNumber: { type: integer }
        pageSize: { type: integer }
        totalPages: { type: integer }
        totalRecords: { type: integer }

    PurgeRequest:
      type: object
      required: [type]
      properties:
        type:
          type: string
          description: |
            What to purge. `All` ignores `value`. `Extension`, `Path` and `Regex` require `value`.
          enum: [All, Extension, Path, Regex]
        value:
          type: string
          description: Required for `Extension`, `Path` and `Regex`.
        matchType:
          type: string
          description: Only used when `type=Regex`. Defaults to `regex`.
          enum: [equals, contains, starts_with, ends_with, regex]

    PurgeBatchRequest:
      type: object
      required: [items]
      properties:
        items:
          type: array
          minItems: 1
          items: { $ref: '#/components/schemas/PurgeRequest' }

    PurgeResultEnvelope:
      allOf:
        - $ref: '#/components/schemas/Envelope'
        - type: object
          properties:
            data:
              type: object
              properties:
                jobCount: { type: integer }
                shields: { type: integer }
                nodes: { type: integer }
                hostnames: { type: integer }

    BatchPurgeResultEnvelope:
      allOf:
        - $ref: '#/components/schemas/Envelope'
        - type: object
          properties:
            data:
              type: object
              properties:
                jobCount: { type: integer }
                accepted: { type: integer }
                rejected:
                  type: array
                  items:
                    type: object
                    properties:
                      index: { type: integer }
                      reason: { type: string }
                shields: { type: integer }
                nodes: { type: integer }
                hostnames: { type: integer }

    ApiKey:
      type: object
      properties:
        id: { type: integer }
        name: { type: string }
        keyPrefix: { type: string, example: pdm_AbCd }
        createdAt: { type: string, format: date-time }
        lastUsedAt: { type: string, format: date-time, nullable: true }
        isRevoked: { type: boolean }

    ApiKeyCreated:
      type: object
      properties:
        id: { type: integer }
        name: { type: string }
        key:
          type: string
          description: The raw key. Shown only once.
          example: pdm_AbCdEf0123456789AbCdEf012345
        keyPrefix: { type: string }
        createdAt: { type: string, format: date-time }

    Property:
      type: object
      properties:
        id: { type: integer }
        name: { type: string, example: my-site }
        status:
          type: integer
          description: 0 Active, 1 Disabled, 2 Pending, 3 Suspended.
          enum: [0, 1, 2, 3]
        origins:
          type: array
          items: { $ref: '#/components/schemas/Origin' }
        hostnames:
          type: array
          items: { $ref: '#/components/schemas/Hostname' }

    Origin:
      type: object
      properties:
        id: { type: integer }
        host: { type: string, example: origin.example.com }
        hostHeader: { type: string, nullable: true }
        sslVerify: { type: boolean, default: false }
        isEnabled: { type: boolean }

    Hostname:
      type: object
      properties:
        id: { type: integer }
        host: { type: string, example: www.example.com }
        status:
          type: integer
          description: 0 PendingValidation, 1 Active, 2 Error, 3 Disabled.
          enum: [0, 1, 2, 3]
        isSystemDefault: { type: boolean }

    GeneralSettings:
      type: object
      description: Stored as snake_case JSON. Send the full object on update — the API does not merge.
      properties:
        hsts:
          type: object
          properties:
            enabled: { type: boolean }
            max_age: { type: integer, example: 31536000 }
            include_subdomains: { type: boolean }
            preload: { type: boolean }
        clickjacking_protection: { type: boolean }
        mime_sniffing_protection: { type: boolean }
        always_online:
          type: object
          properties:
            enabled: { type: boolean }
            codes:
              type: array
              items: { type: string }
              description: "Allowlist: error, updating, timeout, invalid_header, 500, 502, 503, 504, 403, 404, 429."
        query_string:
          type: object
          properties:
            mode: { type: string, enum: [none, ignore_all, except, only] }
            values:
              type: array
              items: { type: string }
        browser_caching:
          type: object
          properties:
            mode: { type: string, enum: [origin, override] }
            ttl: { type: integer, example: 3600 }
        image_optimization:
          type: object
          properties:
            enabled: { type: boolean }
            quality: { type: integer, minimum: 1, maximum: 100, default: 80 }
            max_width: { type: integer, default: 4000 }
            max_height: { type: integer, default: 4000 }
            only_if_smaller: { type: boolean }
        origin_shield:
          type: object
          properties:
            enabled: { type: boolean }
            shield_id: { type: integer, nullable: true }

    Rule:
      type: object
      properties:
        id: { type: integer }
        type:
          type: string
          enum: [CacheRule, HeaderRule]
        name: { type: string }
        order: { type: integer }
        isEnabled: { type: boolean }
        isSystem: { type: boolean }

    CacheRuleInput:
      type: object
      required: [name, pattern, ttlSeconds]
      properties:
        name: { type: string, maxLength: 200 }
        pattern: { type: string, maxLength: 500 }
        matchType:
          type: integer
          description: 0 Equals, 1 Contains, 2 StartsWith, 3 EndsWith, 4 Regex.
          enum: [0, 1, 2, 3, 4]
        ttlSeconds:
          type: integer
          minimum: 0
          description: Time to live. `0` means do not cache.
        description: { type: string }
        order: { type: integer }
        isEnabled: { type: boolean }
        featuresJson:
          type: string
          description: |
            Optional JSON string with edge features:
            `bypass_cookies[]`, `cache_key{include_query_string,include_headers,include_cookies}`,
            `minification{html,css,js}`, `webp{enabled,quality,only_if_smaller}`.

    HeaderRuleInput:
      type: object
      required: [name, headerName, action]
      properties:
        name: { type: string, maxLength: 200 }
        pattern:
          type: string
          description: Empty string = applies globally.
        matchType:
          type: integer
          description: 0 Equals, 1 Contains, 2 StartsWith, 3 EndsWith, 4 Regex. Defaults to Contains.
          enum: [0, 1, 2, 3, 4]
        action:
          type: integer
          description: 0 Add, 1 Set, 2 Delete.
          enum: [0, 1, 2]
        headerName: { type: string }
        headerValue: { type: string, maxLength: 1000 }
        applyToRequest:
          type: boolean
          description: "false = response header (default), true = request header."
        order: { type: integer }
        isEnabled: { type: boolean }

    LogEntry:
      type: object
      properties:
        timestamp: { type: string, format: date-time }
        requestHost: { type: string }
        requestMethod: { type: string }
        requestUri: { type: string }
        responseStatus: { type: integer }
        responseSize: { type: integer }
        duration: { type: number, description: Seconds. }
        clientIp: { type: string }
        cacheStatus: { type: string, example: HIT }
        deviceType: { type: string }
        countryCode: { type: string }
        userAgent: { type: string }

    StatsSummary:
      type: object
      properties:
        totalRequests: { type: integer }
        totalBytes: { type: integer }
        cacheHitRate: { type: number }
        avgResponseTime: { type: number, description: Milliseconds. }
        errors4xx: { type: integer }
        errors403: { type: integer }
        errors404: { type: integer }
        errors429: { type: integer }
        errors5xx: { type: integer }
        uniqueVisitors: { type: integer }

    CreateTicketRequest:
      type: object
      required: [subject, description]
      properties:
        subject: { type: string, maxLength: 200 }
        description: { type: string }
        propertyId:
          type: integer
          nullable: true
          description: Optional. Omit for a general question.
        topic:
          type: integer
          description: 0 General, 1 Networking, 2 Rules, 3 Billing, 4 SSL, 5 Performance.
          enum: [0, 1, 2, 3, 4, 5]
          default: 0

    Ticket:
      type: object
      properties:
        id: { type: integer }
        subject: { type: string }
        status:
          type: integer
          description: 0 Open, 1 InProgress, 2 Resolved, 3 Closed.
          enum: [0, 1, 2, 3]
        topic:
          type: integer
          enum: [0, 1, 2, 3, 4, 5]
        propertyId: { type: integer, nullable: true }
        propertyName: { type: string, nullable: true }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
        messagesCount: { type: integer }

  responses:
    BadRequest:
      description: Invalid request. Read `message` for details.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Envelope' }
          example: { succeeded: false, message: "Value is required for type 'path'." }
    Unauthorized:
      description: Missing or invalid credentials.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Envelope' }
          example: { succeeded: false, message: "Unauthorized" }
    PropertyNotFound:
      description: Property not found (or it belongs to another account).
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Envelope' }
          example: { succeeded: false, message: "Property not found." }
    PropertyDisabled:
      description: The property is disabled or suspended; changes are not allowed.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Envelope' }
          example: { succeeded: false, message: "This property is currently Suspended. Configuration changes are not allowed until it is re-enabled." }
    NoActiveNodes:
      description: No active cache nodes are available to serve the purge.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Envelope' }
          example: { succeeded: false, message: "No active cache nodes found." }
