openapi: 3.1.0

info:
  title: Code Heaven Developer API
  version: 1.0.0
  description: |
    Server-to-server API for Code Heaven plugin/theme vendors to validate and
    manage developer licenses, control per-domain activations, check for product
    updates, and obtain short-lived signed package download URLs.

    ## Authentication
    All requests authenticate with a **vendor server-to-server API key** passed in
    the `X-CH-Vendor-Key` request header. This is the vendor (developer) credential
    and is distinct from the buyer-facing OAuth flow. Keep this key secret and never
    expose it in client-side code or end-user browsers.

    ## Errors
    Error responses share a common envelope:

    ```json
    { "error": { "code": "license_invalid", "message": "The license key is not valid." } }
    ```

    | HTTP | code                | Meaning                                   |
    |------|---------------------|-------------------------------------------|
    | 400  | invalid_request     | Malformed or missing request parameters.  |
    | 401  | (unauthorized)      | Missing or invalid vendor API key.        |
    | 403  | license_invalid     | License is not valid for this operation.  |
    | 403  | expired             | License has expired.                      |
    | 403  | domain_not_activated| Domain is not activated for this license. |
    | 404  | not_found           | License or resource does not exist.       |
    | 409  | seat_limit_exceeded | Activation would exceed the seat limit.   |
    | 429  | rate_limited        | Too many requests; retry later.           |
  contact:
    name: Code Heaven Developer Support
    url: https://code-heaven.com/developers
  license:
    name: Proprietary
    url: https://code-heaven.com/terms

servers:
  - url: https://api.code-heaven.com/v1
    description: Production

security:
  - VendorApiKey: []

tags:
  - name: Licenses
    description: License validation and lifecycle.
  - name: Activations
    description: Per-domain seat activation management.
  - name: Updates
    description: Product update checks and package downloads.

paths:
  /licenses/validate:
    post:
      tags:
        - Licenses
      operationId: validateLicense
      summary: Validate a license key
      description: |
        Validates a license key against an optional product and the calling domain.
        Returns the current status, expiry, seat limit, and the list of active
        domain activations.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ValidateRequest'
            examples:
              valid:
                summary: Validate for a specific product and domain
                value:
                  licenseKey: CH-9F2A-7C41-DD88-1B30
                  domain: shop.example.com
                  product: booknetic-pro
      responses:
        '200':
          description: Validation result. Inspect `valid` and `status` for the outcome.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidateResponse'
              examples:
                valid:
                  summary: Active and activated for the domain
                  value:
                    valid: true
                    status: valid
                    product: booknetic-pro
                    expiresAt: '2027-01-01T00:00:00Z'
                    activations:
                      - domain: shop.example.com
                        activatedAt: '2026-01-15T10:22:00Z'
                    seatLimit: 3
                domainNotActivated:
                  summary: Valid key but the domain is not activated
                  value:
                    valid: false
                    status: domain_not_activated
                    product: booknetic-pro
                    expiresAt: '2027-01-01T00:00:00Z'
                    activations:
                      - domain: other.example.com
                        activatedAt: '2026-01-15T10:22:00Z'
                    seatLimit: 3
                expired:
                  summary: License has expired
                  value:
                    valid: false
                    status: expired
                    product: booknetic-pro
                    expiresAt: '2025-12-31T23:59:59Z'
                    activations: []
                    seatLimit: 3
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'

  /licenses/{licenseKey}/domains:
    post:
      tags:
        - Activations
      operationId: activateDomain
      summary: Activate a domain for a license
      description: |
        Activates the given domain against the license, consuming one seat.
        Returns the updated activation list. Fails with `409` when the seat limit
        is already reached.
      parameters:
        - $ref: '#/components/parameters/LicenseKeyPath'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ActivateDomainRequest'
            examples:
              activate:
                value:
                  domain: shop.example.com
      responses:
        '201':
          description: Domain activated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ActivateDomainResponse'
              examples:
                activated:
                  value:
                    activated: true
                    domain: shop.example.com
                    activations:
                      - domain: shop.example.com
                        activatedAt: '2026-06-04T09:00:00Z'
                      - domain: staging.example.com
                        activatedAt: '2026-05-01T12:00:00Z'
                    seatLimit: 3
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/SeatLimitExceeded'
        '429':
          $ref: '#/components/responses/RateLimited'

  /licenses/{licenseKey}/domains/{domain}:
    delete:
      tags:
        - Activations
      operationId: deactivateDomain
      summary: Deactivate a domain for a license
      description: |
        Releases the seat held by the given domain, freeing it for reuse.
      parameters:
        - $ref: '#/components/parameters/LicenseKeyPath'
        - $ref: '#/components/parameters/DomainPath'
      responses:
        '200':
          description: Domain deactivated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeactivateDomainResponse'
              examples:
                deactivated:
                  value:
                    deactivated: true
                    domain: shop.example.com
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'

  /licenses/{licenseKey}/updates:
    get:
      tags:
        - Updates
      operationId: checkUpdates
      summary: Check for product updates
      description: |
        Returns the latest available version for a product and whether an update
        is available relative to the installed version, including a changelog.
      parameters:
        - $ref: '#/components/parameters/LicenseKeyPath'
        - name: product
          in: query
          required: true
          description: Product slug to check updates for.
          schema:
            type: string
          example: booknetic-pro
        - name: installedVersion
          in: query
          required: true
          description: Semantic version currently installed on the domain.
          schema:
            type: string
          example: 1.4.2
        - name: domain
          in: query
          required: true
          description: Domain requesting the update check (must be activated).
          schema:
            type: string
            format: hostname
          example: shop.example.com
      responses:
        '200':
          description: Update information for the product.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UpdateInfo'
              examples:
                hasUpdate:
                  value:
                    product: booknetic-pro
                    latestVersion: 1.5.0
                    hasUpdate: true
                    changelog:
                      - version: 1.5.0
                        date: '2026-05-20'
                        notes: Added recurring appointments and bug fixes.
                      - version: 1.4.3
                        date: '2026-04-10'
                        notes: Security hardening.
                upToDate:
                  value:
                    product: booknetic-pro
                    latestVersion: 1.4.2
                    hasUpdate: false
                    changelog: []
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'

  /licenses/{licenseKey}/download:
    get:
      tags:
        - Updates
      operationId: getDownload
      summary: Get a signed package download URL
      description: |
        Returns a short-lived signed URL for downloading the requested product
        package version. The `url` expires at `expiresAt`; request a fresh one
        once it lapses.
      parameters:
        - $ref: '#/components/parameters/LicenseKeyPath'
        - name: product
          in: query
          required: true
          description: Product slug to download.
          schema:
            type: string
          example: booknetic-pro
        - name: version
          in: query
          required: true
          description: Exact version to download.
          schema:
            type: string
          example: 1.5.0
        - name: domain
          in: query
          required: true
          description: Domain requesting the download (must be activated).
          schema:
            type: string
            format: hostname
          example: shop.example.com
      responses:
        '200':
          description: Signed download URL.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DownloadResponse'
              examples:
                signed:
                  value:
                    url: https://dl.code-heaven.com/pkg/booknetic-pro-1.5.0.zip?sig=abc123&exp=1749031200
                    version: 1.5.0
                    expiresAt: '2026-06-04T10:00:00Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'

components:
  securitySchemes:
    VendorApiKey:
      type: apiKey
      in: header
      name: X-CH-Vendor-Key
      description: Vendor server-to-server API key. Distinct from the buyer OAuth flow.

  parameters:
    LicenseKeyPath:
      name: licenseKey
      in: path
      required: true
      description: The license key.
      schema:
        type: string
      example: CH-9F2A-7C41-DD88-1B30
    DomainPath:
      name: domain
      in: path
      required: true
      description: The domain to operate on.
      schema:
        type: string
        format: hostname
      example: shop.example.com

  schemas:
    LicenseStatus:
      type: string
      description: Current license status.
      enum:
        - valid
        - invalid
        - expired
        - domain_not_activated
        - revoked

    Activation:
      type: object
      description: A single domain activation occupying one seat.
      required:
        - domain
        - activatedAt
      properties:
        domain:
          type: string
          format: hostname
          description: Activated domain.
          example: shop.example.com
        activatedAt:
          type: string
          format: date-time
          description: When the domain was activated.
          example: '2026-01-15T10:22:00Z'

    License:
      type: object
      description: A developer license and its current state.
      required:
        - licenseKey
        - status
        - product
        - seatLimit
        - activations
      properties:
        licenseKey:
          type: string
          description: The license key.
          example: CH-9F2A-7C41-DD88-1B30
        status:
          $ref: '#/components/schemas/LicenseStatus'
        product:
          type: string
          description: Product slug the license is for.
          example: booknetic-pro
        expiresAt:
          type:
            - string
            - 'null'
          format: date-time
          description: Expiry timestamp, or null for a perpetual license.
          example: '2027-01-01T00:00:00Z'
        seatLimit:
          type: integer
          description: Maximum number of concurrent domain activations.
          minimum: 0
          example: 3
        activations:
          type: array
          description: Currently active domains.
          items:
            $ref: '#/components/schemas/Activation'

    ValidateRequest:
      type: object
      description: Request body for license validation.
      required:
        - licenseKey
        - domain
      properties:
        licenseKey:
          type: string
          description: The license key to validate.
          example: CH-9F2A-7C41-DD88-1B30
        domain:
          type: string
          format: hostname
          description: Domain the license is being validated for.
          example: shop.example.com
        product:
          type: string
          description: Optional product slug to scope the validation.
          example: booknetic-pro

    ValidateResponse:
      type: object
      description: Result of a license validation.
      required:
        - valid
        - status
        - product
        - expiresAt
        - activations
        - seatLimit
      properties:
        valid:
          type: boolean
          description: True when the license is valid and activated for the domain.
          example: true
        status:
          $ref: '#/components/schemas/LicenseStatus'
        product:
          type: string
          description: Product slug the license applies to.
          example: booknetic-pro
        expiresAt:
          type:
            - string
            - 'null'
          format: date-time
          description: Expiry timestamp, or null for a perpetual license.
          example: '2027-01-01T00:00:00Z'
        activations:
          type: array
          description: Currently active domains for this license.
          items:
            $ref: '#/components/schemas/Activation'
        seatLimit:
          type: integer
          description: Maximum number of concurrent domain activations.
          minimum: 0
          example: 3

    ActivateDomainRequest:
      type: object
      description: Request body to activate a domain.
      required:
        - domain
      properties:
        domain:
          type: string
          format: hostname
          description: Domain to activate.
          example: shop.example.com

    ActivateDomainResponse:
      type: object
      description: Result of a domain activation.
      required:
        - activated
        - domain
        - activations
        - seatLimit
      properties:
        activated:
          type: boolean
          const: true
          description: Always true on success.
          example: true
        domain:
          type: string
          format: hostname
          description: The domain that was activated.
          example: shop.example.com
        activations:
          type: array
          description: Updated list of active domains.
          items:
            $ref: '#/components/schemas/Activation'
        seatLimit:
          type: integer
          description: Maximum number of concurrent domain activations.
          minimum: 0
          example: 3

    DeactivateDomainResponse:
      type: object
      description: Result of a domain deactivation.
      required:
        - deactivated
        - domain
      properties:
        deactivated:
          type: boolean
          const: true
          description: Always true on success.
          example: true
        domain:
          type: string
          format: hostname
          description: The domain that was deactivated.
          example: shop.example.com

    ChangelogEntry:
      type: object
      description: A single changelog entry for a product version.
      required:
        - version
        - date
        - notes
      properties:
        version:
          type: string
          description: Version this entry describes.
          example: 1.5.0
        date:
          type: string
          format: date
          description: Release date.
          example: '2026-05-20'
        notes:
          type: string
          description: Human-readable release notes.
          example: Added recurring appointments and bug fixes.

    UpdateInfo:
      type: object
      description: Update availability information for a product.
      required:
        - product
        - latestVersion
        - hasUpdate
        - changelog
      properties:
        product:
          type: string
          description: Product slug.
          example: booknetic-pro
        latestVersion:
          type: string
          description: Latest available version.
          example: 1.5.0
        hasUpdate:
          type: boolean
          description: True when latestVersion is newer than the installed version.
          example: true
        changelog:
          type: array
          description: Changelog entries newer than the installed version.
          items:
            $ref: '#/components/schemas/ChangelogEntry'

    DownloadResponse:
      type: object
      description: A short-lived signed package download URL.
      required:
        - url
        - version
        - expiresAt
      properties:
        url:
          type: string
          format: uri
          description: Short-lived signed URL to the package archive.
          example: https://dl.code-heaven.com/pkg/booknetic-pro-1.5.0.zip?sig=abc123&exp=1749031200
        version:
          type: string
          description: Version of the package the URL points to.
          example: 1.5.0
        expiresAt:
          type: string
          format: date-time
          description: When the signed URL expires.
          example: '2026-06-04T10:00:00Z'

    Error:
      type: object
      description: Standard error envelope.
      required:
        - error
      properties:
        error:
          type: object
          required:
            - code
            - message
          properties:
            code:
              type: string
              description: Machine-readable error code.
              enum:
                - invalid_request
                - license_invalid
                - expired
                - domain_not_activated
                - not_found
                - seat_limit_exceeded
                - rate_limited
              example: invalid_request
            message:
              type: string
              description: Human-readable error description.
              example: The request body is missing required fields.

  responses:
    BadRequest:
      description: The request was malformed or missing required parameters.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            invalidRequest:
              value:
                error:
                  code: invalid_request
                  message: The request body is missing required fields.
    Unauthorized:
      description: The vendor API key is missing or invalid.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            unauthorized:
              value:
                error:
                  code: invalid_request
                  message: Missing or invalid X-CH-Vendor-Key header.
    Forbidden:
      description: |
        The license is not permitted to perform this operation. The `code`
        distinguishes the cause: `license_invalid`, `expired`, or
        `domain_not_activated`.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            licenseInvalid:
              summary: License invalid
              value:
                error:
                  code: license_invalid
                  message: The license key is not valid.
            expired:
              summary: License expired
              value:
                error:
                  code: expired
                  message: The license has expired.
            domainNotActivated:
              summary: Domain not activated
              value:
                error:
                  code: domain_not_activated
                  message: The requesting domain is not activated for this license.
    NotFound:
      description: The license or resource does not exist.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            notFound:
              value:
                error:
                  code: not_found
                  message: No license found for the supplied key.
    SeatLimitExceeded:
      description: Activating the domain would exceed the license seat limit.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            seatLimit:
              value:
                error:
                  code: seat_limit_exceeded
                  message: All seats for this license are in use.
    RateLimited:
      description: Too many requests. Back off and retry later.
      headers:
        Retry-After:
          description: Seconds to wait before retrying.
          schema:
            type: integer
          example: 30
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            rateLimited:
              value:
                error:
                  code: rate_limited
                  message: Rate limit exceeded. Retry after the indicated delay.
