openapi: 3.0.3
info:
  title: awsys.co URL Shortener API
  description: |
    The awsys.co API allows you to programmatically create and manage short URLs,
    retrieve click analytics, and generate QR codes.

    ## Authentication

    API requests require an API key passed in the `Authorization` header:
    ```
    Authorization: Bearer awsys_your_api_key_here
    ```

    Generate your API key from the [Settings page](https://awsys.co/settings.html).

    ## Rate Limits

    API access requires a **Pro or Builder** subscription. Free accounts cannot use the API.

    | Tier | Hourly | Monthly | Tracked Clicks |
    |------|--------|---------|----------------|
    | Free | — | No API access | 1,000/mo |
    | Pro | 50 | 1,000 | 50,000/mo + $0.15/1K overage |
    | Builder | 500 | 10,000 | 500,000/mo + $0.15/1K overage |

    Rate limit headers are included in responses:
    - `X-RateLimit-Limit`: Your hourly limit
    - `X-RateLimit-Remaining`: Requests remaining this hour
    - `X-RateLimit-Reset`: Unix timestamp when limit resets

    ## Base URL

    All API requests should be made to: `https://awsys.co/api`

  version: 1.0.0
  contact:
    name: awsys.co Support
    email: support@alphawavesystems.com
    url: https://awsys.co
  license:
    name: Proprietary
    url: https://awsys.co/terms.html

servers:
  - url: https://awsys.co/api
    description: Production server

tags:
  - name: Links
    description: Create and manage short URLs
  - name: Statistics
    description: Click analytics and statistics
  - name: QR Codes
    description: Generate QR codes for short URLs
  - name: Bulk Operations
    description: Batch operations (Builder tier and above)
  - name: Webhooks
    description: Manage webhooks for link events (Pro and Builder)
  - name: Folders
    description: Organize links into folders (all tiers)

security:
  - ApiKeyAuth: []

paths:
  /v1/links:
    post:
      tags:
        - Links
      summary: Create a short URL
      description: |
        Create a new shortened URL. Optionally specify a custom slug (premium tiers),
        expiration date, or maximum click limit.
      operationId: createLink
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateLinkRequest"
            examples:
              basic:
                summary: Basic short URL
                value:
                  url: "https://example.com/my-long-url"
              withExpiration:
                summary: With expiration
                value:
                  url: "https://example.com/campaign"
                  expiresAt: "2025-12-31T23:59:59Z"
              withCustomSlug:
                summary: With custom slug (Pro+)
                value:
                  url: "https://example.com/product"
                  customSlug: "my-product"
              withMaxClicks:
                summary: With click limit
                value:
                  url: "https://example.com/limited-offer"
                  maxClicks: 1000
      responses:
        "201":
          description: Short URL created successfully
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LinkResponse"
              example:
                success: true
                shortUrl: "https://awsys.co/abc123"
                shortCode: "abc123"
                fullPath: null
                namespace: null
                long: "https://example.com/my-long-url"
                created: "2025-01-15T10:30:00Z"
                expiresAt: null
                maxClicks: null
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              examples:
                missingUrl:
                  value:
                    error: true
                    message: "URL is required"
                    code: "MISSING_URL"
                invalidUrl:
                  value:
                    error: true
                    message: "Invalid URL format"
                    code: "INVALID_URL"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          description: Premium feature required
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error: true
                message: "Custom slugs require a Pro subscription or higher"
                code: "PREMIUM_REQUIRED"
        "409":
          description: Slug already taken
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error: true
                message: "This custom slug is already in use"
                code: "SLUG_TAKEN"
        "429":
          $ref: "#/components/responses/RateLimited"

    get:
      tags:
        - Links
      summary: List your short URLs
      description: Retrieve a paginated list of all short URLs you've created.
      operationId: listLinks
      parameters:
        - name: limit
          in: query
          description: Number of links to return (max 100)
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: offset
          in: query
          description: Number of links to skip for pagination
          schema:
            type: integer
            minimum: 0
            default: 0
      responses:
        "200":
          description: List of short URLs
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LinkListResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"

  /v1/links/{shortPath}:
    get:
      tags:
        - Links
      summary: Get link details
      description: |
        Retrieve details for a specific short URL.

        The `shortPath` can be either:
        - A 6-character code (e.g., `abc123`)
        - A namespaced path (e.g., `myprefix/my-slug`)
      operationId: getLink
      parameters:
        - $ref: "#/components/parameters/shortPath"
      responses:
        "200":
          description: Link details
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LinkDetails"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          description: Not authorized to access this link
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

    delete:
      tags:
        - Links
      summary: Delete a short URL
      description: Permanently delete a short URL. This action cannot be undone.
      operationId: deleteLink
      parameters:
        - $ref: "#/components/parameters/shortPath"
      responses:
        "200":
          description: Link deleted successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  message:
                    type: string
                    example: "Link deleted"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          description: Not authorized to delete this link
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

  /v1/links/{shortPath}/stats:
    get:
      tags:
        - Statistics
      summary: Get link statistics
      description: |
        Retrieve detailed click statistics for a short URL, including
        geographic data, device information, and referrers.

        Returns the last 100 clicks.
      operationId: getLinkStats
      parameters:
        - $ref: "#/components/parameters/shortPath"
      responses:
        "200":
          description: Link statistics
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LinkStats"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          description: Not authorized to access statistics for this link
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

  /v1/bulk:
    post:
      tags:
        - Bulk Operations
      summary: Create multiple short URLs
      description: |
        Create up to 100 short URLs in a single request.

        **Requires Builder tier or higher.**

        Each URL in the batch can have its own configuration (custom slug, expiration, etc.).
        The response includes success/failure status for each URL.
      operationId: bulkCreate
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/BulkCreateRequest"
            example:
              urls:
                - url: "https://example.com/page1"
                - url: "https://example.com/page2"
                  customSlug: "page-two"
                - url: "https://example.com/campaign"
                  expiresAt: "2025-06-30T23:59:59Z"
                  maxClicks: 5000
      responses:
        "200":
          description: Bulk creation results
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BulkCreateResponse"
        "400":
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              examples:
                missingUrls:
                  value:
                    error: true
                    message: "urls array is required"
                    code: "MISSING_URLS"
                tooMany:
                  value:
                    error: true
                    message: "Maximum 100 URLs per request"
                    code: "TOO_MANY_URLS"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          description: Builder tier required
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error: true
                message: "Bulk operations require Builder subscription or higher"
                code: "TIER_REQUIRED"
        "429":
          $ref: "#/components/responses/RateLimited"

  /stats/{shortCode}:
    get:
      tags:
        - Statistics
      summary: Get detailed click statistics
      description: |
        Retrieve comprehensive click analytics for a short URL.

        **Authentication optional** - without auth, only basic stats are returned.
        With auth, full click details including location and device data are included.

        Analytics events captured during an overage grace period (up to 7 days after the monthly limit is exceeded) are excluded from results until the subscription is upgraded or the billing cycle resets.
      operationId: getDetailedStats
      security:
        - ApiKeyAuth: []
        - {}
      parameters:
        - name: shortCode
          in: path
          required: true
          description: The 6-character short code
          schema:
            type: string
            pattern: "^[A-Za-z0-9]{6}$"
            example: "abc123"
      responses:
        "200":
          description: Detailed statistics
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DetailedStats"
        "404":
          $ref: "#/components/responses/NotFound"

  /stats/{shortCode}/aggregate:
    get:
      tags:
        - Statistics
      summary: Get aggregated statistics
      description: |
        Retrieve aggregated analytics including daily trends, device breakdown,
        geographic distribution, and more.

        **History limits by tier:**
        - Free: 30 days
        - Pro: 90 days
        - Builder+: 365 days (+ $0.15/1K overage above monthly tracked click limit)

        Analytics events captured during an overage grace period (up to 7 days after the monthly limit is exceeded) are excluded from results until the subscription is upgraded or the billing cycle resets.
      operationId: getAggregateStats
      parameters:
        - name: shortCode
          in: path
          required: true
          description: The 6-character short code
          schema:
            type: string
            pattern: "^[A-Za-z0-9]{6}$"
        - name: period
          in: query
          description: Time period for aggregation
          schema:
            type: string
            enum: [7d, 30d, 90d, 365d]
            default: 7d
      responses:
        "200":
          description: Aggregated statistics
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AggregatedStats"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          description: Period exceeds tier limit
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          $ref: "#/components/responses/NotFound"

  /qr/{shortCode}:
    get:
      tags:
        - QR Codes
      summary: Generate QR code
      description: |
        Generate a QR code image for a short URL.

        **No authentication required.**

        Customize the QR code with query parameters for size and colors.
      operationId: getQrCode
      security: []
      parameters:
        - name: shortCode
          in: path
          required: true
          description: The 6-character short code
          schema:
            type: string
            pattern: "^[A-Za-z0-9]{6}$"
        - name: size
          in: query
          description: QR code size in pixels (100-1000)
          schema:
            type: integer
            minimum: 100
            maximum: 1000
            default: 300
        - name: color
          in: query
          description: Foreground color (hex without #)
          schema:
            type: string
            pattern: "^[0-9A-Fa-f]{6}$"
            default: "000000"
          example: "1a73e8"
        - name: bgColor
          in: query
          description: Background color (hex without #)
          schema:
            type: string
            pattern: "^[0-9A-Fa-f]{6}$"
            default: "FFFFFF"
          example: "f5f5f5"
      responses:
        "200":
          description: QR code PNG image
          content:
            image/png:
              schema:
                type: string
                format: binary
        "404":
          description: Short URL not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /qr/{prefix}/{slug}:
    get:
      tags:
        - QR Codes
      summary: Generate QR code for namespaced URL
      description: |
        Generate a QR code for a namespaced (custom prefix) short URL.

        **No authentication required.**
      operationId: getNamespacedQrCode
      security: []
      parameters:
        - name: prefix
          in: path
          required: true
          description: The namespace prefix
          schema:
            type: string
          example: "mycompany"
        - name: slug
          in: path
          required: true
          description: The custom slug
          schema:
            type: string
          example: "product-launch"
        - name: size
          in: query
          description: QR code size in pixels (100-1000)
          schema:
            type: integer
            minimum: 100
            maximum: 1000
            default: 300
        - name: color
          in: query
          description: Foreground color (hex without #)
          schema:
            type: string
            pattern: "^[0-9A-Fa-f]{6}$"
            default: "000000"
        - name: bgColor
          in: query
          description: Background color (hex without #)
          schema:
            type: string
            pattern: "^[0-9A-Fa-f]{6}$"
            default: "FFFFFF"
      responses:
        "200":
          description: QR code PNG image
          content:
            image/png:
              schema:
                type: string
                format: binary
        "404":
          description: Short URL not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /ns/stats/{prefix}/{slug}:
    get:
      tags:
        - Statistics
      summary: Get namespaced link statistics
      description: Get detailed click statistics for a namespaced short URL.
      operationId: getNamespacedStats
      security:
        - ApiKeyAuth: []
        - {}
      parameters:
        - name: prefix
          in: path
          required: true
          schema:
            type: string
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Detailed statistics
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DetailedStats"
        "404":
          $ref: "#/components/responses/NotFound"

  /ns/stats/{prefix}/{slug}/aggregate:
    get:
      tags:
        - Statistics
      summary: Get namespaced aggregated statistics
      description: Get aggregated analytics for a namespaced short URL.
      operationId: getNamespacedAggregateStats
      parameters:
        - name: prefix
          in: path
          required: true
          schema:
            type: string
        - name: slug
          in: path
          required: true
          schema:
            type: string
        - name: period
          in: query
          schema:
            type: string
            enum: [7d, 30d, 90d, 365d]
            default: 7d
      responses:
        "200":
          description: Aggregated statistics
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AggregatedStats"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /v1/webhooks:
    get:
      tags:
        - Webhooks
      summary: List webhooks
      description: Returns all webhooks configured for your account.
      responses:
        "200":
          description: List of webhooks
          content:
            application/json:
              schema:
                type: object
                properties:
                  webhooks:
                    type: array
                    items:
                      $ref: "#/components/schemas/WebhookObject"
                  limit:
                    type: integer
                    description: Max webhooks allowed for your tier
        "401":
          $ref: "#/components/responses/Unauthorized"
    post:
      tags:
        - Webhooks
      summary: Create a webhook
      description: |
        Create a new webhook endpoint.

        **Tier limits:**
        - Free: 0 (not available)
        - Pro: 2 webhooks
        - Builder: 10 webhooks
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - url
                - events
              properties:
                url:
                  type: string
                  format: uri
                  description: HTTPS URL to receive webhook POST requests
                events:
                  type: array
                  items:
                    type: string
                    enum:
                      - link.click
                      - link.created
                      - link.updated
                      - link.deleted
                      - link.expired
                      - link.limit_reached
                      - link.geo_blocked
                name:
                  type: string
                  description: Optional friendly name for the webhook
      responses:
        "201":
          description: Webhook created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WebhookObject"
        "400":
          description: Invalid request (bad URL, invalid events, duplicate)
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          description: Webhook limit reached for your tier

  /v1/webhooks/{webhookId}:
    delete:
      tags:
        - Webhooks
      summary: Delete a webhook
      parameters:
        - name: webhookId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Webhook deleted
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /v1/webhooks/{webhookId}/test:
    post:
      tags:
        - Webhooks
      summary: Test a webhook
      description: Sends a test payload to the webhook URL with HMAC-SHA256 signature.
      parameters:
        - name: webhookId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Test delivery result
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  statusCode:
                    type: integer
                  durationMs:
                    type: integer
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /v1/folders:
    get:
      tags:
        - Folders
      summary: List folders
      description: Returns all folders for your account along with your tier's folder limit.
      responses:
        "200":
          description: List of folders
          content:
            application/json:
              schema:
                type: object
                properties:
                  folders:
                    type: array
                    items:
                      $ref: "#/components/schemas/FolderObject"
                  limit:
                    type: integer
                    description: Max folders allowed for your tier (5 for Free, unlimited for Pro+)
        "401":
          $ref: "#/components/responses/Unauthorized"
    post:
      tags:
        - Folders
      summary: Create a folder
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
              properties:
                name:
                  type: string
                  maxLength: 50
                color:
                  type: string
                  description: Hex color code (e.g. "#3B82F6")
      responses:
        "201":
          description: Folder created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FolderObject"
        "400":
          description: Invalid name or folder limit reached
        "401":
          $ref: "#/components/responses/Unauthorized"

  /v1/folders/{folderId}:
    delete:
      tags:
        - Folders
      summary: Delete a folder
      description: Deletes the folder. Links inside are unassigned but not deleted.
      parameters:
        - name: folderId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Folder deleted
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /v1/links/{shortCode}/folder:
    post:
      tags:
        - Folders
      summary: Assign link to folder
      description: Assigns a link to a folder. Pass `null` as `folderId` to remove from folder.
      parameters:
        - name: shortCode
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                folderId:
                  type: string
                  nullable: true
                  description: Folder ID to assign, or null to remove from folder
      responses:
        "200":
          description: Assignment updated
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /subscription/overage-limit:
    post:
      summary: Set monthly overage spending cap
      description: |
        Sets a hard cap on overage click charges for the current billing cycle.
        Once the cap is reached, analytics recording pauses (links still redirect).
        Applies to Pro and Builder tiers only. Pro default: $10/month. Builder default: no cap.
      tags:
        - Subscription
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - limitCents
              properties:
                limitCents:
                  type: integer
                  minimum: 0
                  description: Hard monthly cap in USD cents. 0 = no cap.
                  example: 1000
      responses:
        '200':
          description: Cap updated successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  limitCents:
                    type: integer
        '400':
          description: Invalid limitCents value
        '403':
          description: Only available on Pro and Builder plans
        '401':
          $ref: '#/components/responses/Unauthorized'

components:
  securitySchemes:
    ApiKeyAuth:
      type: http
      scheme: bearer
      bearerFormat: API Key
      description: |
        API key authentication. Pass your API key in the Authorization header:
        ```
        Authorization: Bearer awsys_your_api_key_here
        ```

  parameters:
    shortPath:
      name: shortPath
      in: path
      required: true
      description: |
        The short URL identifier. Can be either:
        - A 6-character code (e.g., `abc123`)
        - A namespaced path using URL encoding (e.g., `myprefix%2Fmy-slug`)
      schema:
        type: string
      examples:
        shortCode:
          value: "abc123"
          summary: 6-character code
        namespaced:
          value: "myprefix/my-slug"
          summary: Namespaced path

  schemas:
    WebhookObject:
      type: object
      properties:
        id:
          type: string
          description: Webhook ID
        url:
          type: string
          format: uri
        name:
          type: string
        events:
          type: array
          items:
            type: string
        createdAt:
          type: string
          format: date-time
        lastDeliveryAt:
          type: string
          format: date-time
          nullable: true
        lastStatus:
          type: integer
          nullable: true

    FolderObject:
      type: object
      properties:
        id:
          type: string
          description: Folder ID
        name:
          type: string
        color:
          type: string
          description: Hex color code
          nullable: true
        linkCount:
          type: integer
          description: Number of links in this folder
        createdAt:
          type: string
          format: date-time

    CreateLinkRequest:
      type: object
      required:
        - url
      properties:
        url:
          type: string
          format: uri
          description: The destination URL to shorten
          example: "https://example.com/my-long-url"
        customSlug:
          type: string
          minLength: 3
          maxLength: 50
          pattern: "^[a-zA-Z0-9-]+$"
          description: |
            Custom slug for the short URL (Pro tier and above).
            Must be 3-50 characters, alphanumeric and hyphens only.
          example: "my-campaign"
        expiresAt:
          type: string
          format: date-time
          description: ISO 8601 date when the link should expire
          example: "2025-12-31T23:59:59Z"
        maxClicks:
          type: integer
          minimum: 1
          description: Maximum number of clicks before the link expires
          example: 1000
        activeFrom:
          type: string
          format: date-time
          description: ISO 8601 date when the link should become active (scheduling)
          example: "2025-02-01T00:00:00Z"

    LinkResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        shortUrl:
          type: string
          format: uri
          description: The full short URL
          example: "https://awsys.co/abc123"
        shortCode:
          type: string
          description: The 6-character short code
          example: "abc123"
        fullPath:
          type: string
          nullable: true
          description: Full path for namespaced URLs (prefix/slug)
          example: "mycompany/campaign"
        namespace:
          type: string
          nullable: true
          description: The namespace prefix if using custom slugs
          example: "mycompany"
        long:
          type: string
          format: uri
          description: The original destination URL
        created:
          type: string
          format: date-time
          description: When the link was created
        expiresAt:
          type: string
          format: date-time
          nullable: true
          description: When the link expires (if set)
        maxClicks:
          type: integer
          nullable: true
          description: Maximum clicks (if set)

    LinkDetails:
      type: object
      properties:
        id:
          type: string
          description: Unique document ID
        shortUrl:
          type: string
          format: uri
        shortCode:
          type: string
        fullPath:
          type: string
          nullable: true
        namespace:
          type: string
          nullable: true
        long:
          type: string
          format: uri
        clicks:
          type: integer
          description: Total click count
        created:
          type: string
          format: date-time
        expiresAt:
          type: string
          format: date-time
          nullable: true
        maxClicks:
          type: integer
          nullable: true
        isCustom:
          type: boolean
          description: Whether this is a custom slug

    LinkListResponse:
      type: object
      properties:
        links:
          type: array
          items:
            $ref: "#/components/schemas/LinkDetails"
        pagination:
          type: object
          properties:
            limit:
              type: integer
            offset:
              type: integer
            hasMore:
              type: boolean

    LinkStats:
      type: object
      properties:
        shortCode:
          type: string
        fullPath:
          type: string
          nullable: true
        totalClicks:
          type: integer
        clicks:
          type: array
          items:
            $ref: "#/components/schemas/ClickRecord"

    ClickRecord:
      type: object
      properties:
        timestamp:
          type: string
          format: date-time
        city:
          type: string
          nullable: true
        region:
          type: string
          nullable: true
        country:
          type: string
          nullable: true
        browser:
          type: string
          example: "Chrome"
        os:
          type: string
          example: "Windows"
        device:
          type: string
          enum: [desktop, mobile, tablet]
        referrer:
          type: string
          nullable: true
          example: "https://google.com"

    DetailedStats:
      type: object
      properties:
        short:
          type: string
        long:
          type: string
          format: uri
        opened:
          type: integer
          description: Total click count
        created:
          type: string
          format: date-time
        expiresAt:
          type: string
          format: date-time
          nullable: true
        maxClicks:
          type: integer
          nullable: true
        isCustom:
          type: boolean
        clicks:
          type: array
          items:
            $ref: "#/components/schemas/ClickRecord"

    AggregatedStats:
      type: object
      properties:
        short:
          type: string
        period:
          type: string
          example: "7d"
        totalClicks:
          type: integer
        clicksByDay:
          type: array
          items:
            type: object
            properties:
              date:
                type: string
                format: date
              clicks:
                type: integer
        deviceBreakdown:
          type: object
          properties:
            mobile:
              type: integer
            desktop:
              type: integer
            tablet:
              type: integer
        countryBreakdown:
          type: object
          additionalProperties:
            type: integer
          example:
            US: 150
            UK: 45
            DE: 30
        referrerBreakdown:
          type: object
          additionalProperties:
            type: integer
        browserBreakdown:
          type: object
          additionalProperties:
            type: integer
        osBreakdown:
          type: object
          additionalProperties:
            type: integer
        hourBreakdown:
          type: object
          additionalProperties:
            type: integer
          description: Clicks by hour (0-23)
        uniqueVisitors:
          type: integer
        tierLimit:
          type: integer
          description: Maximum days of history for your tier

    BulkCreateRequest:
      type: object
      required:
        - urls
      properties:
        urls:
          type: array
          minItems: 1
          maxItems: 100
          items:
            type: object
            required:
              - url
            properties:
              url:
                type: string
                format: uri
              customSlug:
                type: string
              expiresAt:
                type: string
                format: date-time
              activeFrom:
                type: string
                format: date-time
              maxClicks:
                type: integer
                minimum: 1

    BulkCreateResponse:
      type: object
      properties:
        success:
          type: boolean
        summary:
          type: object
          properties:
            total:
              type: integer
            created:
              type: integer
            failed:
              type: integer
        results:
          type: array
          items:
            type: object
            properties:
              index:
                type: integer
              url:
                type: string
              success:
                type: boolean
              shortUrl:
                type: string
                description: Only present if success is true
              shortCode:
                type: string
                description: Only present if success is true
              fullPath:
                type: string
                nullable: true
              error:
                type: string
                description: Only present if success is false

    Error:
      type: object
      properties:
        error:
          type: boolean
          example: true
        message:
          type: string
          description: Human-readable error message
        code:
          type: string
          description: Machine-readable error code
          example: "INVALID_URL"

  responses:
    Unauthorized:
      description: Invalid or missing API key
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          examples:
            missing:
              value:
                error: true
                message: "API key required"
                code: "API_KEY_REQUIRED"
            invalid:
              value:
                error: true
                message: "Invalid API key"
                code: "INVALID_API_KEY"

    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error: true
            message: "Short URL not found"
            code: "NOT_FOUND"

    RateLimited:
      description: Rate limit exceeded
      headers:
        X-RateLimit-Limit:
          schema:
            type: integer
          description: Your hourly rate limit
        X-RateLimit-Remaining:
          schema:
            type: integer
          description: Remaining requests this hour
        X-RateLimit-Reset:
          schema:
            type: integer
          description: Unix timestamp when limit resets
        Retry-After:
          schema:
            type: integer
          description: Seconds until you can retry
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error: true
            message: "Rate limit exceeded. Please try again later."
            code: "RATE_LIMITED"
