openapi: 3.0.3
info:
  title: CompactSaaS External API
  description: |
    External API for customer integrations with CompactSaaS services.
    
    ## Authentication
    
    All endpoints except `/v1/health` require an API key in the `x-api-key` header.
    
    API keys can be created and managed from the CompactSaaS dashboard under 
    **Team Settings > API Keys**.
    
    ### API Key Format
    
    API keys follow the format: `pk_live_<32 random characters>` (production) or 
    `pk_test_<32 random characters>` (staging/test).
    
    ### Example Request
    
    ```bash
    curl -X POST https://api.compactsaas.com/v1/track \
      -H "x-api-key: pk_live_your_key_here" \
      -H "Content-Type: application/json" \
      -d '{"event": "page_view", "properties": {"page": "/home"}}'
    ```
    
    ## Tenant Isolation
    
    Your API key determines your team identity. All resources are automatically
    scoped to your team — you cannot access other teams' data.
    
    ## Rate Limiting
    
    Rate limits are applied for DDoS protection:
    - **100 requests per second** (burst: 200)
    - No monthly quotas - pay-as-you-go billing via Stripe
    
    When rate limited, the API returns `429 Too Many Requests` with a `Retry-After` header.
    
    ## Billing
    
    All API calls are billed via Stripe Billing Meters. Usage appears on your monthly invoice.
    View current usage in the CompactSaaS dashboard under **Billing > Usage**.
    
    ## Links
    
    The Links API lets you create redirects and short links programmatically.
    Links are served at the edge with sub-millisecond latency.
    
    **Click Analytics:** Set `analyticsEnabled: true` on any redirect or short link
    to track every click as a pageview in your analytics dashboard. Click analytics
    are billed as `advanced_analytics_pageviews` on your monthly invoice — the same
    rate as regular website pageviews.
    
    When analytics is enabled, an `analyticsWebsiteId` is returned. Use this ID to
    view click metrics (visitors, geo, referrer, device breakdowns) in the analytics
    dashboard under the **Links** tab.
    
  version: 1.1.0
  contact:
    name: CompactSaaS Support
    email: support@compactsaas.com
  license:
    name: Proprietary
    url: https://compactsaas.com/terms

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

tags:
  - name: Analytics
    description: Analytics tracking endpoints
  - name: Redirects
    description: Manage URL redirects on custom domains
  - name: Short Links
    description: Create and manage shortened URLs
  - name: Domains
    description: View custom domains and manage fallback URLs
  - name: Health
    description: Health check endpoints

security:
  - ApiKeyAuth: []

paths:
  # ===========================================================================
  # Analytics
  # ===========================================================================
  /track:
    post:
      summary: Track an analytics event
      description: |
        Records an analytics event for your application.
        Events are processed asynchronously — the endpoint returns immediately 
        with a `202 Accepted` response.
      operationId: trackEvent
      tags:
        - Analytics
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TrackEventRequest'
            examples:
              pageView:
                summary: Track a page view
                value:
                  event: page_view
                  properties:
                    page: /pricing
                    referrer: https://google.com
              customEvent:
                summary: Track a custom event
                value:
                  event: purchase_completed
                  properties:
                    product_id: prod_123
                    amount: 99.99
      responses:
        '202':
          description: Event accepted for processing
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TrackEventResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'

  # ===========================================================================
  # Redirects
  # ===========================================================================
  /links/redirects:
    post:
      summary: Create a redirect
      description: |
        Creates a URL redirect on a verified custom domain. The redirect is
        synced to our edge routing layer, typically active within seconds.
      operationId: createRedirect
      tags:
        - Redirects
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateRedirectRequest'
            example:
              sourceDomain: links.example.com
              sourcePath: /blog
              targetUrl: https://example.com/blog
              statusCode: 301
      responses:
        '201':
          description: Redirect created
          content:
            application/json:
              schema:
                type: object
                properties:
                  redirect:
                    $ref: '#/components/schemas/Redirect'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '409':
          description: Redirect already exists for this domain + path
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

    get:
      summary: List redirects
      description: Returns all redirects for your team.
      operationId: listRedirects
      tags:
        - Redirects
      responses:
        '200':
          description: List of redirects
          content:
            application/json:
              schema:
                type: object
                properties:
                  redirects:
                    type: array
                    items:
                      $ref: '#/components/schemas/Redirect'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /links/redirects/{redirectId}:
    put:
      summary: Update a redirect
      description: Updates an existing redirect. Only provided fields are changed.
      operationId: updateRedirect
      tags:
        - Redirects
      parameters:
        - $ref: '#/components/parameters/RedirectId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateRedirectRequest'
            example:
              targetUrl: https://example.com/new-destination
              statusCode: 302
      responses:
        '200':
          description: Redirect updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  redirect:
                    $ref: '#/components/schemas/Redirect'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

    delete:
      summary: Delete a redirect
      description: Deletes a redirect and removes it from edge routing.
      operationId: deleteRedirect
      tags:
        - Redirects
      parameters:
        - $ref: '#/components/parameters/RedirectId'
      responses:
        '204':
          description: Redirect deleted
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  # ===========================================================================
  # Short Links
  # ===========================================================================
  /links/shorten:
    post:
      summary: Create a short link
      description: |
        Creates a shortened URL. If `customCode` is not provided, a random
        short code is generated.
      operationId: createShortLink
      tags:
        - Short Links
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateShortLinkRequest'
            examples:
              random:
                summary: Auto-generated short code
                value:
                  targetUrl: https://example.com/very/long/url/to/shorten
              custom:
                summary: Custom short code
                value:
                  targetUrl: https://example.com/launch
                  customCode: launch2026
      responses:
        '201':
          description: Short link created
          content:
            application/json:
              schema:
                type: object
                properties:
                  link:
                    $ref: '#/components/schemas/ShortLink'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '409':
          description: Custom code already taken
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /links/short:
    get:
      summary: List short links
      description: Returns all short links for your team.
      operationId: listShortLinks
      tags:
        - Short Links
      responses:
        '200':
          description: List of short links
          content:
            application/json:
              schema:
                type: object
                properties:
                  links:
                    type: array
                    items:
                      $ref: '#/components/schemas/ShortLink'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /links/short/{linkId}:
    get:
      summary: Get a short link
      description: Returns a single short link by ID, including the hasClicks flag.
      operationId: getShortLink
      tags:
        - Short Links
      parameters:
        - $ref: '#/components/parameters/LinkId'
      responses:
        '200':
          description: Short link details
          content:
            application/json:
              schema:
                type: object
                properties:
                  link:
                    $ref: '#/components/schemas/ShortLink'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    put:
      summary: Update a short link
      description: Updates an existing short link. Only provided fields are changed.
      operationId: updateShortLink
      tags:
        - Short Links
      parameters:
        - $ref: '#/components/parameters/LinkId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateShortLinkRequest'
      responses:
        '200':
          description: Short link updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  link:
                    $ref: '#/components/schemas/ShortLink'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    delete:
      summary: Delete a short link
      description: Deletes a short link. The short code becomes available for reuse.
      operationId: deleteShortLink
      tags:
        - Short Links
      parameters:
        - $ref: '#/components/parameters/LinkId'
      responses:
        '204':
          description: Short link deleted
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /links/short/{linkId}/stats:
    get:
      summary: Get click statistics for a short link
      description: |
        Returns daily click timeseries data for a short link.
        Data comes from analytics rollups which are preserved permanently.
        Requires analyticsEnabled on the link.
      operationId: getShortLinkStats
      tags:
        - Short Links
      parameters:
        - $ref: '#/components/parameters/LinkId'
        - name: days
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 730
            default: 30
          description: Number of trailing days to include (1-730, default 30)
      responses:
        '200':
          description: Click statistics
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ShortLinkStats'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /links/short/{linkId}/qr:
    get:
      summary: Get QR code for a short link
      description: |
        Generates a QR code image for a short link. Returns the image directly
        (not JSON). Every short link has a QR code endpoint — no creation limits,
        no branding restrictions.

        When the short link has `analyticsEnabled: true`, every QR scan is
        automatically tracked in your analytics dashboard as "Direct / QR" traffic.
      operationId: getShortLinkQr
      tags:
        - Short Links
      parameters:
        - $ref: '#/components/parameters/LinkId'
        - name: format
          in: query
          schema:
            type: string
            enum: [svg, png]
            default: svg
          description: Image format. SVG is ~1.5KB, PNG is ~2KB at 300px.
        - name: size
          in: query
          schema:
            type: integer
            minimum: 100
            maximum: 1000
            default: 300
          description: Image dimensions in pixels.
        - name: color
          in: query
          schema:
            type: string
            default: '#000000'
          description: QR code foreground color (hex).
        - name: bgColor
          in: query
          schema:
            type: string
            default: '#ffffff'
          description: QR code background color (hex).
      responses:
        '200':
          description: QR code image
          headers:
            Cache-Control:
              schema:
                type: string
                example: 'public, max-age=86400'
          content:
            image/svg+xml:
              schema:
                type: string
            image/png:
              schema:
                type: string
                format: binary
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  # ===========================================================================
  # Domains
  # ===========================================================================
  /links/domains:
    get:
      summary: List custom domains
      description: |
        Returns custom domains configured for your team (read-only).
        Domain setup, verification, and TLS provisioning are managed via the dashboard.
      operationId: listDomains
      tags:
        - Domains
      responses:
        '200':
          description: List of domains
          content:
            application/json:
              schema:
                type: object
                properties:
                  domains:
                    type: array
                    items:
                      $ref: '#/components/schemas/Domain'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /links/domains/fallback:
    post:
      summary: Set or clear domain fallback URL
      description: |
        Sets a fallback URL for a custom domain. When a visitor hits a path
        with no matching redirect, they are sent to this URL instead of getting a 404.
        
        Send an empty string or omit `fallbackUrl` to clear the fallback.
      operationId: setDomainFallback
      tags:
        - Domains
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SetFallbackRequest'
            examples:
              set:
                summary: Set a fallback URL
                value:
                  domain: links.example.com
                  fallbackUrl: https://example.com
              clear:
                summary: Clear the fallback
                value:
                  domain: links.example.com
                  fallbackUrl: ""
      responses:
        '200':
          description: Fallback updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  domain:
                    $ref: '#/components/schemas/Domain'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Domain not found or not verified
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  # ===========================================================================
  # Health
  # ===========================================================================
  /health:
    get:
      summary: Health check
      description: |
        Returns the health status of the API.
        This endpoint does **not** require authentication.
      operationId: healthCheck
      tags:
        - Health
      security: []
      responses:
        '200':
          description: API is healthy
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HealthResponse'
              example:
                status: ok
                timestamp: "2026-03-09T10:30:00.000Z"
                version: v1

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: x-api-key
      description: |
        API key for authentication. Obtain from the CompactSaaS dashboard 
        under **Team Settings > API Keys**.
        
        Format: `pk_live_<32 characters>` (production) or `pk_test_<32 characters>` (staging)

  parameters:
    RedirectId:
      name: redirectId
      in: path
      required: true
      schema:
        type: string
      description: Redirect ID (ULID format)

    LinkId:
      name: linkId
      in: path
      required: true
      schema:
        type: string
      description: Short link ID (ULID format)

  responses:
    ValidationError:
      description: Invalid request body or parameters
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            errors:
              - param: targetUrl
                type: Required

    Unauthorized:
      description: Invalid or missing API key
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            message: Unauthorized

    Forbidden:
      description: Account disabled or suspended
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            message: User is not authorized to access this resource

    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            message: Not found

    RateLimited:
      description: Rate limit exceeded
      headers:
        Retry-After:
          schema:
            type: integer
          description: Seconds to wait before retrying
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'

  schemas:
    # --- Analytics ---
    TrackEventRequest:
      type: object
      required:
        - event
      properties:
        event:
          type: string
          minLength: 1
          maxLength: 255
          description: Event name (e.g. `page_view`, `button_click`)
        properties:
          type: object
          additionalProperties: true
          description: Optional key-value pairs with event data
        timestamp:
          type: string
          format: date-time
          description: ISO 8601 timestamp. Defaults to server time if omitted.

    TrackEventResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true

    # --- Redirects ---
    CreateRedirectRequest:
      type: object
      required:
        - sourceDomain
        - targetUrl
      properties:
        sourceDomain:
          type: string
          minLength: 1
          maxLength: 253
          description: Custom domain to redirect from (must be verified)
          example: links.example.com
        sourcePath:
          type: string
          minLength: 1
          maxLength: 2048
          default: /
          description: Path to redirect from
          example: /blog
        targetUrl:
          type: string
          format: uri
          maxLength: 2048
          description: Destination URL
          example: https://example.com/blog
        statusCode:
          type: integer
          enum: [301, 302]
          default: 301
          description: HTTP redirect status code
        ttl:
          type: integer
          minimum: 0
          maximum: 31536000
          description: Cache TTL in seconds (0 = no cache, max 1 year)
        analyticsEnabled:
          type: boolean
          description: Track clicks as pageviews in your analytics dashboard. Billed as advanced_analytics_pageviews.

    UpdateRedirectRequest:
      type: object
      properties:
        targetUrl:
          type: string
          format: uri
          maxLength: 2048
        statusCode:
          type: integer
          enum: [301, 302]
        ttl:
          type: integer
          minimum: 0
          maximum: 31536000
        enabled:
          type: boolean
        analyticsEnabled:
          type: boolean
          description: Track clicks as pageviews in your analytics dashboard.

    Redirect:
      type: object
      properties:
        id:
          type: string
          description: Redirect ID (ULID)
          example: 01KK96EEVFSZF2CFB4BPA20F09
        sourceDomain:
          type: string
          example: links.example.com
        sourcePath:
          type: string
          example: /blog
        targetUrl:
          type: string
          example: https://example.com/blog
        statusCode:
          type: integer
          example: 301
        ttl:
          type: integer
          example: 86400
        enabled:
          type: boolean
          example: true
        analyticsEnabled:
          type: boolean
          description: Whether click analytics is enabled for this redirect
          example: false
        analyticsWebsiteId:
          type: string
          nullable: true
          description: ID for viewing click analytics in the dashboard (Links tab). Set automatically when analytics is first enabled.
          example: null
        createdAt:
          type: integer
          description: Unix timestamp in milliseconds
          example: 1773056506735
        updatedAt:
          type: integer
          description: Unix timestamp in milliseconds
          example: 1773056506735

    # --- Short Links ---
    CreateShortLinkRequest:
      type: object
      required:
        - targetUrl
      properties:
        targetUrl:
          type: string
          format: uri
          maxLength: 2048
          description: URL to shorten
          example: https://example.com/very/long/url
        customCode:
          type: string
          minLength: 3
          maxLength: 32
          pattern: '^[a-zA-Z0-9_-]+$'
          description: Optional custom short code (alphanumeric, hyphens, underscores)
          example: my-link
        analyticsEnabled:
          type: boolean
          description: Track clicks as pageviews in your analytics dashboard. Billed as advanced_analytics_pageviews.

    UpdateShortLinkRequest:
      type: object
      properties:
        targetUrl:
          type: string
          format: uri
          maxLength: 2048
          description: New destination URL
        analyticsEnabled:
          type: boolean
          description: Track clicks as pageviews in your analytics dashboard.

    ShortLink:
      type: object
      properties:
        id:
          type: string
          description: Short link ID (ULID)
          example: 01KK96EWWVMWVR7MGPTRKQR7FS
        shortCode:
          type: string
          example: sJqrCIz
        shortUrl:
          type: string
          description: Full shortened URL — share this with your audience
          example: https://urlz.zip/sJqrCIz
        targetUrl:
          type: string
          example: https://example.com/very/long/url
        domain:
          type: string
          example: urlz.zip
        enabled:
          type: boolean
          example: true
        analyticsEnabled:
          type: boolean
          description: Whether click analytics is enabled for this short link
          example: false
        analyticsWebsiteId:
          type: string
          nullable: true
          description: ID for viewing click analytics in the dashboard (Links tab). Set automatically when analytics is first enabled.
          example: null
        hasClicks:
          type: boolean
          description: Whether this short link has been clicked at least once. Set permanently to true on first click.
          example: false
        createdAt:
          type: integer
          example: 1773056521115
        updatedAt:
          type: integer
          example: 1773056521115

    ShortLinkStats:
      type: object
      properties:
        linkId:
          type: string
          example: 01KK96EWWVMWVR7MGPTRKQR7FS
        totalClicks:
          type: integer
          description: Sum of clicks across all days in the requested period
          example: 42
        days:
          type: integer
          description: Trailing days included in the response
          example: 30
        timeseries:
          type: array
          description: Daily click counts from analytics rollups (preserved permanently)
          items:
            type: object
            properties:
              date:
                type: string
                format: date
                description: Date (ISO format)
                example: "2026-03-20"
              clicks:
                type: integer
                example: 15

    # --- Domains ---
    Domain:
      type: object
      properties:
        domain:
          type: string
          example: links.example.com
        status:
          type: string
          enum: [pending, verified, failed]
          example: verified
        tenantStatus:
          type: string
          enum: [pending, active, inactive]
          nullable: true
          example: active
        fallbackUrl:
          type: string
          nullable: true
          description: URL visitors are sent to when no redirect matches
          example: https://example.com
        verifiedAt:
          type: integer
          nullable: true
          example: 1772836089453
        createdAt:
          type: integer
          example: 1772836027575

    SetFallbackRequest:
      type: object
      required:
        - domain
      properties:
        domain:
          type: string
          description: Custom domain (must be verified)
          example: links.example.com
        fallbackUrl:
          type: string
          description: Fallback URL. Empty string or omit to clear.
          example: https://example.com

    # --- Common ---
    HealthResponse:
      type: object
      properties:
        status:
          type: string
          enum: [ok]
        timestamp:
          type: string
          format: date-time
        version:
          type: string
          example: v1

    ErrorResponse:
      type: object
      properties:
        message:
          type: string
        errors:
          type: array
          items:
            type: object
            properties:
              param:
                type: string
              type:
                type: string
