openapi: 3.0.3
info:
  title: noBSredir API
  description: |
    URL shortener API. No SDK - just HTTP.

    All endpoints are under `/api/`. Authenticate with an API key via the `X-API-Key` header.
    API keys are workspace-scoped and grant owner-level access.
  version: "1.0"
  contact:
    url: https://nobsredir.com

servers:
  - url: https://nobsredir.com/api

security:
  - ApiKeyAuth: []

tags:
  - name: Links
    description: Create, list, update, and delete short links
  - name: Tags
    description: Manage workspace tags and metadata
  - name: Workspaces
    description: View and update workspace settings
  - name: Domains
    description: Add, verify, and remove custom domains
  - name: Members
    description: List and manage workspace members
  - name: Invites
    description: Invite users to a workspace
  - name: API Keys
    description: Create and manage API keys (max 10 per workspace)
  - name: Stats
    description: Click analytics and statistics
  - name: Audit Log
    description: Workspace activity log
  - name: Export
    description: Export links and clicks as CSV
  - name: Import
    description: Import links from CSV
  - name: Monitoring
    description: Broken link monitoring (Pro+ plans)
  - name: Billing
    description: Plan usage and limits

paths:
  # ── Links ──────────────────────────────────────────────
  /workspaces/{wsId}/links:
    post:
      operationId: createLink
      tags: [Links]
      summary: Create a link
      description: |
        Rate limit: 30 requests per 10 seconds per workspace.
      parameters:
        - $ref: "#/components/parameters/wsId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateLinkRequest"
      responses:
        "201":
          description: Link created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LinkResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/TooManyRequests"

    get:
      operationId: listLinks
      tags: [Links]
      summary: List links
      parameters:
        - $ref: "#/components/parameters/wsId"
        - name: page
          in: query
          schema:
            type: integer
            default: 1
            minimum: 1
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
            minimum: 1
            maximum: 100
        - name: domain
          in: query
          schema:
            type: string
          description: Filter by domain
        - name: prefix
          in: query
          schema:
            type: string
          description: Filter by slug prefix (defaults to fnl.sh domain)
        - name: tag
          in: query
          schema:
            type: string
          description: Filter by tag name
      responses:
        "200":
          description: Paginated link list
          content:
            application/json:
              schema:
                type: object
                properties:
                  links:
                    type: array
                    items:
                      $ref: "#/components/schemas/Link"
                  total:
                    type: integer
                  page:
                    type: integer
                  limit:
                    type: integer
        "401":
          $ref: "#/components/responses/Unauthorized"

  /workspaces/{wsId}/links/{linkId}:
    get:
      operationId: getLink
      tags: [Links]
      summary: Get a link
      parameters:
        - $ref: "#/components/parameters/wsId"
        - $ref: "#/components/parameters/linkId"
      responses:
        "200":
          description: Link details
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Link"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

    patch:
      operationId: updateLink
      tags: [Links]
      summary: Update a link
      description: |
        All fields are optional. Send `null` to clear optional fields.
        Set `password` to a new string to change it, or `null` to remove it.
        Changes take effect immediately.
      parameters:
        - $ref: "#/components/parameters/wsId"
        - $ref: "#/components/parameters/linkId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateLinkRequest"
      responses:
        "200":
          $ref: "#/components/responses/Ok"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

    delete:
      operationId: deleteLink
      tags: [Links]
      summary: Delete a link
      parameters:
        - $ref: "#/components/parameters/wsId"
        - $ref: "#/components/parameters/linkId"
      responses:
        "200":
          $ref: "#/components/responses/Ok"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /workspaces/{wsId}/links/bulk:
    post:
      operationId: bulkCreateLinks
      tags: [Links]
      summary: Bulk create links
      description: |
        Create up to 100 links in a single request. Links with invalid data or duplicate slugs are silently skipped.

        Rate limit: 5 requests per 10 seconds per workspace.
      parameters:
        - $ref: "#/components/parameters/wsId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [links]
              properties:
                links:
                  type: array
                  maxItems: 100
                  items:
                    $ref: "#/components/schemas/CreateLinkRequest"
      responses:
        "201":
          description: Bulk create result
          content:
            application/json:
              schema:
                type: object
                properties:
                  created:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                        domain:
                          type: string
                        slug:
                          type: string
                        target:
                          type: string
                        title:
                          type: string
                          nullable: true
                  count:
                    type: integer
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "429":
          $ref: "#/components/responses/TooManyRequests"

    delete:
      operationId: bulkDeleteLinks
      tags: [Links]
      summary: Bulk delete links
      description: Delete up to 100 links in a single request.
      parameters:
        - $ref: "#/components/parameters/wsId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [ids]
              properties:
                ids:
                  type: array
                  maxItems: 100
                  items:
                    type: string
      responses:
        "200":
          description: Bulk delete result
          content:
            application/json:
              schema:
                type: object
                properties:
                  deleted:
                    type: integer
        "401":
          $ref: "#/components/responses/Unauthorized"

  # ── Tags ─────────────────────────────────────────────
  /workspaces/{wsId}/links/tags:
    get:
      operationId: listTags
      tags: [Tags]
      summary: List tags
      description: Returns all tags in the workspace, sorted alphabetically.
      parameters:
        - $ref: "#/components/parameters/wsId"
      responses:
        "200":
          description: Tag list
          content:
            application/json:
              schema:
                type: object
                properties:
                  tags:
                    type: array
                    items:
                      $ref: "#/components/schemas/Tag"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /workspaces/{wsId}/links/tags/{tagName}:
    put:
      operationId: upsertTag
      tags: [Tags]
      summary: Create or update tag metadata
      description: |
        Max 100 tags per workspace. Tag names must be lowercase letters, numbers, hyphens, and underscores.
      parameters:
        - $ref: "#/components/parameters/wsId"
        - name: tagName
          in: path
          required: true
          schema:
            type: string
            pattern: "^[a-z0-9_-]+$"
            maxLength: 32
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                color:
                  type: string
                  description: "Hex color (e.g. #ff0000) or empty string to clear"
                description:
                  type: string
                  maxLength: 500
                date_from:
                  type: string
                  format: date
                  nullable: true
                  description: Campaign start date (YYYY-MM-DD)
                date_to:
                  type: string
                  format: date
                  nullable: true
                  description: Campaign end date (YYYY-MM-DD)
      responses:
        "200":
          $ref: "#/components/responses/Ok"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          description: Tag limit reached (max 100 per workspace)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

    delete:
      operationId: deleteTag
      tags: [Tags]
      summary: Delete a tag
      description: Removes tag metadata. Does not untag links.
      parameters:
        - $ref: "#/components/parameters/wsId"
        - name: tagName
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          $ref: "#/components/responses/Ok"
        "401":
          $ref: "#/components/responses/Unauthorized"

  # ── Workspaces ─────────────────────────────────────────
  /workspaces/{wsId}:
    get:
      operationId: getWorkspace
      tags: [Workspaces]
      summary: Get workspace details
      parameters:
        - $ref: "#/components/parameters/wsId"
      responses:
        "200":
          description: Workspace details
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Workspace"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

    patch:
      operationId: updateWorkspace
      tags: [Workspaces]
      summary: Update workspace settings
      parameters:
        - $ref: "#/components/parameters/wsId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateWorkspaceRequest"
      responses:
        "200":
          $ref: "#/components/responses/Ok"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"

  # ── Domains ────────────────────────────────────────────
  /workspaces/{wsId}/domains:
    post:
      operationId: addDomain
      tags: [Domains]
      summary: Add a custom domain
      parameters:
        - $ref: "#/components/parameters/wsId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [domain]
              properties:
                domain:
                  type: string
                  description: "Lowercase hostname (e.g. go.example.com)"
      responses:
        "201":
          description: Domain added
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                  domain:
                    type: string
                  verified:
                    type: boolean
                  txt_record:
                    type: string
                    description: TXT record value to add for verification
                  cname_target:
                    type: string
                    description: CNAME target to point your domain at
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "409":
          $ref: "#/components/responses/Conflict"

    get:
      operationId: listDomains
      tags: [Domains]
      summary: List domains
      parameters:
        - $ref: "#/components/parameters/wsId"
      responses:
        "200":
          description: Domain list
          content:
            application/json:
              schema:
                type: object
                properties:
                  domains:
                    type: array
                    items:
                      $ref: "#/components/schemas/Domain"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /workspaces/{wsId}/domains/{domainId}/verify:
    post:
      operationId: verifyDomain
      tags: [Domains]
      summary: Verify a domain
      description: |
        Check if the required DNS records have been added.

        Rate limit: 1 request per 10 seconds per domain.
      parameters:
        - $ref: "#/components/parameters/wsId"
        - name: domainId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Verification result
          content:
            application/json:
              schema:
                type: object
                properties:
                  verified:
                    type: boolean
                  txt_verified:
                    type: boolean
                  cname_verified:
                    type: boolean
                  expected_txt:
                    type: string
                    description: Present when not yet verified
                  cname_target:
                    type: string
                    description: Present when not yet verified
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /workspaces/{wsId}/domains/{domainId}/link-count:
    get:
      operationId: getDomainLinkCount
      tags: [Domains]
      summary: Get link count for a domain
      parameters:
        - $ref: "#/components/parameters/wsId"
        - name: domainId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Link count
          content:
            application/json:
              schema:
                type: object
                properties:
                  count:
                    type: integer
        "401":
          $ref: "#/components/responses/Unauthorized"

  /workspaces/{wsId}/domains/{domainId}:
    delete:
      operationId: deleteDomain
      tags: [Domains]
      summary: Remove a domain
      description: Deletes the domain and all links on it.
      parameters:
        - $ref: "#/components/parameters/wsId"
        - name: domainId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          $ref: "#/components/responses/Ok"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  # ── Members ────────────────────────────────────────────
  /workspaces/{wsId}/members:
    get:
      operationId: listMembers
      tags: [Members]
      summary: List members
      parameters:
        - $ref: "#/components/parameters/wsId"
      responses:
        "200":
          description: Member list with pending invites
          content:
            application/json:
              schema:
                type: object
                properties:
                  members:
                    type: array
                    items:
                      $ref: "#/components/schemas/Member"
                  pendingInvites:
                    type: array
                    items:
                      type: object
                      properties:
                        email:
                          type: string
                        role:
                          $ref: "#/components/schemas/Role"
                        createdAt:
                          type: string
                          format: date-time
        "401":
          $ref: "#/components/responses/Unauthorized"

  /workspaces/{wsId}/members/{userId}:
    patch:
      operationId: updateMemberRole
      tags: [Members]
      summary: Update a member's role
      parameters:
        - $ref: "#/components/parameters/wsId"
        - name: userId
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [role]
              properties:
                role:
                  $ref: "#/components/schemas/Role"
      responses:
        "200":
          $ref: "#/components/responses/Ok"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

    delete:
      operationId: removeMember
      tags: [Members]
      summary: Remove a member
      parameters:
        - $ref: "#/components/parameters/wsId"
        - name: userId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          $ref: "#/components/responses/Ok"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"

  # ── Invites ────────────────────────────────────────────
  /workspaces/{wsId}/invites:
    post:
      operationId: createInvite
      tags: [Invites]
      summary: Invite a user
      parameters:
        - $ref: "#/components/parameters/wsId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email:
                  type: string
                  format: email
                role:
                  $ref: "#/components/schemas/Role"
                  default: viewer
      responses:
        "201":
          description: Invite created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Invite"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "409":
          $ref: "#/components/responses/Conflict"

    get:
      operationId: listInvites
      tags: [Invites]
      summary: List invites
      parameters:
        - $ref: "#/components/parameters/wsId"
      responses:
        "200":
          description: Invite list
          content:
            application/json:
              schema:
                type: object
                properties:
                  invites:
                    type: array
                    items:
                      $ref: "#/components/schemas/Invite"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /workspaces/{wsId}/invites/{inviteId}:
    delete:
      operationId: deleteInvite
      tags: [Invites]
      summary: Revoke an invite
      parameters:
        - $ref: "#/components/parameters/wsId"
        - name: inviteId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          $ref: "#/components/responses/Ok"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  # ── API Keys ───────────────────────────────────────────
  /workspaces/{wsId}/api-keys:
    post:
      operationId: createApiKey
      tags: [API Keys]
      summary: Create an API key
      description: |
        The full key is returned only once. Store it immediately.
        Max 10 API keys per workspace.
      parameters:
        - $ref: "#/components/parameters/wsId"
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  default: API Key
                  description: Human-readable label
      responses:
        "201":
          description: API key created
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                  name:
                    type: string
                  key:
                    type: string
                    description: Full API key (nobs_...) - shown only once
                  key_prefix:
                    type: string
                    description: First 10 characters of the key
                  created_at:
                    type: string
                    format: date-time
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          description: API key limit reached (max 10)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

    get:
      operationId: listApiKeys
      tags: [API Keys]
      summary: List API keys
      description: Returns key prefixes only, never the full key.
      parameters:
        - $ref: "#/components/parameters/wsId"
      responses:
        "200":
          description: API key list
          content:
            application/json:
              schema:
                type: object
                properties:
                  api_keys:
                    type: array
                    items:
                      $ref: "#/components/schemas/ApiKey"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /workspaces/{wsId}/api-keys/{keyId}:
    delete:
      operationId: deleteApiKey
      tags: [API Keys]
      summary: Delete an API key
      description: Any requests using this key will immediately return 401.
      parameters:
        - $ref: "#/components/parameters/wsId"
        - name: keyId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          $ref: "#/components/responses/Ok"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  # ── Stats ──────────────────────────────────────────────
  /workspaces/{wsId}/stats/summary:
    get:
      operationId: statsSummary
      tags: [Stats]
      summary: Get summary stats
      parameters:
        - $ref: "#/components/parameters/wsId"
        - $ref: "#/components/parameters/statsLink"
        - $ref: "#/components/parameters/statsFrom"
        - $ref: "#/components/parameters/statsTo"
        - $ref: "#/components/parameters/statsIncludeBots"
      responses:
        "200":
          description: Summary statistics
          content:
            application/json:
              schema:
                type: object
                properties:
                  total_links:
                    type: integer
                  total_clicks:
                    type: integer
                  total_countries:
                    type: integer
                  today_clicks:
                    type: integer
        "401":
          $ref: "#/components/responses/Unauthorized"

  /workspaces/{wsId}/stats/daily:
    get:
      operationId: statsDaily
      tags: [Stats]
      summary: Get daily click counts
      parameters:
        - $ref: "#/components/parameters/wsId"
        - name: days
          in: query
          schema:
            type: integer
            default: 30
            minimum: 1
            maximum: 365
        - $ref: "#/components/parameters/statsLink"
        - $ref: "#/components/parameters/statsFrom"
        - $ref: "#/components/parameters/statsTo"
        - $ref: "#/components/parameters/statsCountry"
        - $ref: "#/components/parameters/statsBrowser"
        - $ref: "#/components/parameters/statsOS"
        - $ref: "#/components/parameters/statsDevice"
        - $ref: "#/components/parameters/statsReferer"
        - $ref: "#/components/parameters/statsIncludeBots"
      responses:
        "200":
          description: Daily click counts
          content:
            application/json:
              schema:
                type: object
                properties:
                  daily:
                    type: array
                    items:
                      type: object
                      properties:
                        date:
                          type: string
                          format: date
                        clicks:
                          type: integer
        "401":
          $ref: "#/components/responses/Unauthorized"

  /workspaces/{wsId}/stats/countries:
    get:
      operationId: statsCountries
      tags: [Stats]
      summary: Get clicks by country
      parameters:
        - $ref: "#/components/parameters/wsId"
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
            minimum: 1
            maximum: 100
        - $ref: "#/components/parameters/statsLink"
        - $ref: "#/components/parameters/statsFrom"
        - $ref: "#/components/parameters/statsTo"
        - $ref: "#/components/parameters/statsIncludeBots"
      responses:
        "200":
          description: Click counts by country
          content:
            application/json:
              schema:
                type: object
                properties:
                  countries:
                    type: array
                    items:
                      type: object
                      properties:
                        country:
                          type: string
                          description: ISO 3166-1 alpha-2 country code
                        clicks:
                          type: integer
        "401":
          $ref: "#/components/responses/Unauthorized"

  /workspaces/{wsId}/stats/referrers:
    get:
      operationId: statsReferrers
      tags: [Stats]
      summary: Get clicks by referrer
      parameters:
        - $ref: "#/components/parameters/wsId"
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
            minimum: 1
            maximum: 100
        - $ref: "#/components/parameters/statsLink"
        - $ref: "#/components/parameters/statsFrom"
        - $ref: "#/components/parameters/statsTo"
        - $ref: "#/components/parameters/statsIncludeBots"
      responses:
        "200":
          description: Click counts by referrer
          content:
            application/json:
              schema:
                type: object
                properties:
                  referrers:
                    type: array
                    items:
                      type: object
                      properties:
                        referer:
                          type: string
                        clicks:
                          type: integer
        "401":
          $ref: "#/components/responses/Unauthorized"

  /workspaces/{wsId}/stats/hourly:
    get:
      operationId: statsHourly
      tags: [Stats]
      summary: Get clicks by hour of day
      parameters:
        - $ref: "#/components/parameters/wsId"
        - $ref: "#/components/parameters/statsLink"
        - $ref: "#/components/parameters/statsFrom"
        - $ref: "#/components/parameters/statsTo"
        - $ref: "#/components/parameters/statsIncludeBots"
      responses:
        "200":
          description: Click counts by hour (0-23)
          content:
            application/json:
              schema:
                type: object
                properties:
                  hourly:
                    type: array
                    items:
                      type: object
                      properties:
                        hour:
                          type: integer
                          minimum: 0
                          maximum: 23
                        clicks:
                          type: integer
        "401":
          $ref: "#/components/responses/Unauthorized"

  /workspaces/{wsId}/stats/browsers:
    get:
      operationId: statsBrowsers
      tags: [Stats]
      summary: Get clicks by browser
      parameters:
        - $ref: "#/components/parameters/wsId"
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
        - $ref: "#/components/parameters/statsLink"
        - $ref: "#/components/parameters/statsFrom"
        - $ref: "#/components/parameters/statsTo"
        - $ref: "#/components/parameters/statsIncludeBots"
      responses:
        "200":
          description: Click counts by browser
          content:
            application/json:
              schema:
                type: object
                properties:
                  browsers:
                    type: array
                    items:
                      type: object
                      properties:
                        browser:
                          type: string
                        clicks:
                          type: integer
        "401":
          $ref: "#/components/responses/Unauthorized"

  /workspaces/{wsId}/stats/os:
    get:
      operationId: statsOS
      tags: [Stats]
      summary: Get clicks by operating system
      parameters:
        - $ref: "#/components/parameters/wsId"
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
        - $ref: "#/components/parameters/statsLink"
        - $ref: "#/components/parameters/statsFrom"
        - $ref: "#/components/parameters/statsTo"
        - $ref: "#/components/parameters/statsIncludeBots"
      responses:
        "200":
          description: Click counts by OS
          content:
            application/json:
              schema:
                type: object
                properties:
                  os:
                    type: array
                    items:
                      type: object
                      properties:
                        os:
                          type: string
                        clicks:
                          type: integer
        "401":
          $ref: "#/components/responses/Unauthorized"

  /workspaces/{wsId}/stats/devices:
    get:
      operationId: statsDevices
      tags: [Stats]
      summary: Get clicks by device type
      parameters:
        - $ref: "#/components/parameters/wsId"
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
        - $ref: "#/components/parameters/statsLink"
        - $ref: "#/components/parameters/statsFrom"
        - $ref: "#/components/parameters/statsTo"
        - $ref: "#/components/parameters/statsIncludeBots"
      responses:
        "200":
          description: Click counts by device type
          content:
            application/json:
              schema:
                type: object
                properties:
                  devices:
                    type: array
                    items:
                      type: object
                      properties:
                        device:
                          type: string
                          description: "Desktop, Mobile, or Tablet"
                        clicks:
                          type: integer
        "401":
          $ref: "#/components/responses/Unauthorized"

  /workspaces/{wsId}/stats/top-links:
    get:
      operationId: statsTopLinks
      tags: [Stats]
      summary: Get top links by clicks
      parameters:
        - $ref: "#/components/parameters/wsId"
        - name: days
          in: query
          schema:
            type: integer
            default: 7
            minimum: 1
            maximum: 365
        - name: limit
          in: query
          schema:
            type: integer
            default: 5
            minimum: 1
            maximum: 100
        - $ref: "#/components/parameters/statsIncludeBots"
      responses:
        "200":
          description: Top links by click count
          content:
            application/json:
              schema:
                type: object
                properties:
                  links:
                    type: array
                    items:
                      type: object
                      properties:
                        link_id:
                          type: string
                        domain:
                          type: string
                        slug:
                          type: string
                        clicks:
                          type: integer
        "401":
          $ref: "#/components/responses/Unauthorized"

  /workspaces/{wsId}/stats/links/{linkId}:
    get:
      operationId: statsLink
      tags: [Stats]
      summary: Get stats for a single link
      parameters:
        - $ref: "#/components/parameters/wsId"
        - $ref: "#/components/parameters/linkId"
        - $ref: "#/components/parameters/statsFrom"
        - $ref: "#/components/parameters/statsTo"
        - $ref: "#/components/parameters/statsIncludeBots"
      responses:
        "200":
          description: Per-link statistics
          content:
            application/json:
              schema:
                type: object
                properties:
                  total_clicks:
                    type: integer
                  daily:
                    type: array
                    items:
                      type: object
                      properties:
                        date:
                          type: string
                          format: date
                        clicks:
                          type: integer
                  countries:
                    type: array
                    items:
                      type: object
                      properties:
                        country:
                          type: string
                        clicks:
                          type: integer
                  browsers:
                    type: array
                    items:
                      type: object
                      properties:
                        browser:
                          type: string
                        clicks:
                          type: integer
                  devices:
                    type: array
                    items:
                      type: object
                      properties:
                        device:
                          type: string
                        clicks:
                          type: integer
                  os:
                    type: array
                    items:
                      type: object
                      properties:
                        os:
                          type: string
                        clicks:
                          type: integer
                  referrers:
                    type: array
                    items:
                      type: object
                      properties:
                        referer:
                          type: string
                        clicks:
                          type: integer
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /workspaces/{wsId}/stats/tags/{tagName}:
    get:
      operationId: statsTag
      tags: [Stats]
      summary: Get stats for a tag
      description: Requires Pro plan or higher.
      parameters:
        - $ref: "#/components/parameters/wsId"
        - name: tagName
          in: path
          required: true
          schema:
            type: string
        - $ref: "#/components/parameters/statsFrom"
        - $ref: "#/components/parameters/statsTo"
        - $ref: "#/components/parameters/statsIncludeBots"
      responses:
        "200":
          description: Tag analytics
          content:
            application/json:
              schema:
                type: object
                properties:
                  tag:
                    $ref: "#/components/schemas/Tag"
                  total_links:
                    type: integer
                  total_clicks:
                    type: integer
                  daily:
                    type: array
                    items:
                      type: object
                      properties:
                        date:
                          type: string
                          format: date
                        clicks:
                          type: integer
                  countries:
                    type: array
                    items:
                      type: object
                      properties:
                        country:
                          type: string
                        clicks:
                          type: integer
                  referrers:
                    type: array
                    items:
                      type: object
                      properties:
                        referer:
                          type: string
                        clicks:
                          type: integer
                  hourly:
                    type: array
                    items:
                      type: object
                      properties:
                        hour:
                          type: integer
                        clicks:
                          type: integer
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"

  /workspaces/{wsId}/stats/tags/compare:
    get:
      operationId: statsTagCompare
      tags: [Stats]
      summary: Compare two tags
      description: Requires Team plan or higher.
      parameters:
        - $ref: "#/components/parameters/wsId"
        - name: tags
          in: query
          required: true
          schema:
            type: string
          description: "Two tag names, comma-separated (e.g. campaign-a,campaign-b)"
        - name: days
          in: query
          schema:
            type: integer
            default: 30
        - $ref: "#/components/parameters/statsIncludeBots"
      responses:
        "200":
          description: Side-by-side tag comparison
          content:
            application/json:
              schema:
                type: object
                additionalProperties:
                  type: object
                  properties:
                    name:
                      type: string
                    daily:
                      type: array
                      items:
                        type: object
                        properties:
                          date:
                            type: string
                            format: date
                          clicks:
                            type: integer
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"

  /workspaces/{wsId}/stats/rollup:
    get:
      operationId: statsRollup
      tags: [Stats]
      summary: Get rollup stats across client workspaces
      description: Agency plan only.
      parameters:
        - $ref: "#/components/parameters/wsId"
      responses:
        "200":
          description: Client workspace rollup
          content:
            application/json:
              schema:
                type: object
                properties:
                  clients:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                        name:
                          type: string
                        slug:
                          type: string
                        total_links:
                          type: integer
                        total_clicks:
                          type: integer
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"

  # ── Audit Log ──────────────────────────────────────────
  /workspaces/{wsId}/audit-log:
    get:
      operationId: listAuditLog
      tags: [Audit Log]
      summary: List audit log entries
      parameters:
        - $ref: "#/components/parameters/wsId"
        - name: page
          in: query
          schema:
            type: integer
            default: 1
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
            maximum: 100
        - name: action
          in: query
          schema:
            type: string
          description: "Filter by action (e.g. link.create, member.remove)"
        - name: user_id
          in: query
          schema:
            type: string
          description: Filter by user ID
        - name: from
          in: query
          schema:
            type: string
            format: date-time
          description: Start date (ISO 8601)
        - name: to
          in: query
          schema:
            type: string
            format: date-time
          description: End date (ISO 8601)
      responses:
        "200":
          description: Paginated audit log
          content:
            application/json:
              schema:
                type: object
                properties:
                  logs:
                    type: array
                    items:
                      $ref: "#/components/schemas/AuditLogEntry"
                  total:
                    type: integer
                  page:
                    type: integer
                  limit:
                    type: integer
        "401":
          $ref: "#/components/responses/Unauthorized"

  # ── Export ─────────────────────────────────────────────
  /workspaces/{wsId}/export/links:
    get:
      operationId: exportLinks
      tags: [Export]
      summary: Export links as CSV
      parameters:
        - $ref: "#/components/parameters/wsId"
      responses:
        "200":
          description: CSV file
          content:
            text/csv:
              schema:
                type: string
        "401":
          $ref: "#/components/responses/Unauthorized"

  /workspaces/{wsId}/export/clicks:
    get:
      operationId: exportClicks
      tags: [Export]
      summary: Export clicks as CSV
      description: Returns the 10,000 most recent clicks.
      parameters:
        - $ref: "#/components/parameters/wsId"
      responses:
        "200":
          description: CSV file
          content:
            text/csv:
              schema:
                type: string
        "401":
          $ref: "#/components/responses/Unauthorized"

  # ── Import ─────────────────────────────────────────────
  /workspaces/{wsId}/import/links:
    post:
      operationId: importLinks
      tags: [Import]
      summary: Import links from CSV
      description: |
        Max 500 rows per import. Auto-detects Bitly export format.
        Required column: `target` (or `url`, `destination`, `long_url`).
        Optional columns: `title`, `slug`, `tags`, `note`.
      parameters:
        - $ref: "#/components/parameters/wsId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [csv]
              properties:
                csv:
                  type: string
                  description: RFC 4180 CSV content
                format:
                  type: string
                  enum: [generic, bitly, auto]
                  default: auto
                domain:
                  type: string
                  default: fnl.sh
                  description: Domain for imported links
      responses:
        "201":
          description: Import result
          content:
            application/json:
              schema:
                type: object
                properties:
                  created:
                    type: integer
                  skipped:
                    type: integer
                  errors:
                    type: array
                    items:
                      type: object
                      properties:
                        row:
                          type: integer
                        reason:
                          type: string
                  links:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                        domain:
                          type: string
                        slug:
                          type: string
                        target:
                          type: string
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"

  # ── Monitoring ─────────────────────────────────────────
  /workspaces/{wsId}/monitoring/broken:
    get:
      operationId: listBrokenLinks
      tags: [Monitoring]
      summary: List broken links
      description: Requires Pro plan or higher.
      parameters:
        - $ref: "#/components/parameters/wsId"
      responses:
        "200":
          description: Broken links
          content:
            application/json:
              schema:
                type: object
                properties:
                  links:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                        domain:
                          type: string
                        slug:
                          type: string
                        target:
                          type: string
                        lastStatus:
                          type: integer
                        lastCheckedAt:
                          type: string
                          format: date-time
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"

  /workspaces/{wsId}/monitoring/check:
    post:
      operationId: checkLinks
      tags: [Monitoring]
      summary: Run a link health check
      description: |
        Requires Pro plan or higher. Rate limit: 1 check per workspace per hour.
        Optionally check a single link by ID, or all links in the workspace.
      parameters:
        - $ref: "#/components/parameters/wsId"
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                linkId:
                  type: string
                  description: Check a single link (optional, checks all if omitted)
      responses:
        "200":
          description: Check result
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                  statusCode:
                    type: integer
                  isBroken:
                    type: boolean
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"
        "429":
          $ref: "#/components/responses/TooManyRequests"

  /workspaces/{wsId}/monitoring/alerts:
    get:
      operationId: listAlerts
      tags: [Monitoring]
      summary: List monitoring alerts
      description: Requires Pro plan or higher.
      parameters:
        - $ref: "#/components/parameters/wsId"
        - name: page
          in: query
          schema:
            type: integer
            default: 1
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
      responses:
        "200":
          description: Alert list
          content:
            application/json:
              schema:
                type: object
                properties:
                  alerts:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: integer
                        linkId:
                          type: string
                        domain:
                          type: string
                        slug:
                          type: string
                        target:
                          type: string
                        statusCode:
                          type: integer
                        alertedAt:
                          type: string
                          format: date-time
                        resolvedAt:
                          type: string
                          format: date-time
                          nullable: true
                  total:
                    type: integer
                  page:
                    type: integer
                  limit:
                    type: integer
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PaymentRequired"

  # ── Billing ────────────────────────────────────────────
  /billing/usage:
    get:
      operationId: getUsage
      tags: [Billing]
      summary: Get plan usage and limits
      parameters:
        - name: organization_id
          in: query
          schema:
            type: string
          description: Organization ID (preferred)
        - name: workspace_id
          in: query
          schema:
            type: string
          description: Workspace ID (legacy)
      responses:
        "200":
          description: Usage and limits
          content:
            application/json:
              schema:
                type: object
                properties:
                  plan:
                    type: string
                  limits:
                    type: object
                    properties:
                      max_links:
                        type: integer
                      max_clicks_per_month:
                        type: integer
                        description: "-1 for unlimited"
                      max_domains:
                        type: integer
                      max_members:
                        type: integer
                      max_client_workspaces:
                        type: integer
                  usage:
                    type: object
                    properties:
                      links_created:
                        type: integer
                      clicks_recorded:
                        type: integer
                      domains:
                        type: integer
                      members:
                        type: integer
                      client_workspaces:
                        type: integer
        "401":
          $ref: "#/components/responses/Unauthorized"

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: "API key (nobs_...) created in the dashboard or via the API Keys endpoint"

  parameters:
    wsId:
      name: wsId
      in: path
      required: true
      schema:
        type: string
      description: Workspace ID
    linkId:
      name: linkId
      in: path
      required: true
      schema:
        type: string
      description: Link ID
    statsLink:
      name: link
      in: query
      schema:
        type: string
      description: Filter by link ID
    statsFrom:
      name: from
      in: query
      schema:
        type: string
        format: date
      description: Start date (YYYY-MM-DD)
    statsTo:
      name: to
      in: query
      schema:
        type: string
        format: date
      description: End date (YYYY-MM-DD)
    statsCountry:
      name: country
      in: query
      schema:
        type: string
      description: Filter by country code (ISO 3166-1 alpha-2)
    statsBrowser:
      name: browser
      in: query
      schema:
        type: string
      description: Filter by browser name
    statsOS:
      name: os
      in: query
      schema:
        type: string
      description: Filter by operating system
    statsDevice:
      name: device
      in: query
      schema:
        type: string
        enum: [Desktop, Mobile, Tablet]
      description: Filter by device type
    statsReferer:
      name: referer
      in: query
      schema:
        type: string
      description: Filter by referrer
    statsIncludeBots:
      name: include_bots
      in: query
      schema:
        type: boolean
        default: false
      description: Include bot traffic in results

  responses:
    Ok:
      description: Success
      content:
        application/json:
          schema:
            type: object
            properties:
              ok:
                type: boolean
                example: true
    BadRequest:
      description: Bad request - missing or invalid fields
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Unauthorized:
      description: Unauthorized - missing or invalid API key
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Forbidden:
      description: Forbidden - insufficient role
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    PaymentRequired:
      description: Plan limit reached
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Conflict:
      description: Duplicate resource or state conflict
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    TooManyRequests:
      description: Rate limit exceeded
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"

  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: string

    Role:
      type: string
      enum: [owner, admin, editor, viewer]

    UTMParams:
      type: object
      properties:
        source:
          type: string
        medium:
          type: string
        campaign:
          type: string
        term:
          type: string
        content:
          type: string

    RoutingRules:
      type: object
      description: Requires Team or Agency plan.
      properties:
        geo:
          type: object
          additionalProperties:
            type: string
          description: "Country code to URL mapping (e.g. {\"US\": \"https://us.example.com\"})"
        device:
          type: object
          additionalProperties:
            type: string
          description: "Device type to URL mapping (Desktop, Mobile, Tablet)"
        os:
          type: object
          additionalProperties:
            type: string
          description: "OS to URL mapping (Windows, macOS, iOS, Android, Linux)"

    ABVariant:
      type: object
      required: [name, url, weight]
      properties:
        name:
          type: string
        url:
          type: string
          format: uri
        weight:
          type: integer
          minimum: 1
          maximum: 99
          description: Weights across all variants must sum to 100

    DeepLinkConfig:
      type: object
      description: Requires Pro plan or higher.
      properties:
        ios_app_url:
          type: string
          description: iOS app deep link URL (e.g. myapp://page)
        ios_store_url:
          type: string
          format: uri
          description: iOS App Store URL
        ios_fallback_url:
          type: string
          format: uri
          description: Fallback URL if iOS app is not installed
        android_app_url:
          type: string
          description: Android app deep link URL
        android_store_url:
          type: string
          format: uri
          description: Google Play Store URL
        android_fallback_url:
          type: string
          format: uri
          description: Fallback URL if Android app is not installed
        interstitial:
          type: boolean
          description: Show an interstitial page before redirecting

    CreateLinkRequest:
      type: object
      required: [target]
      properties:
        target:
          type: string
          format: uri
          description: Destination URL
        slug:
          type: string
          pattern: "^[a-zA-Z0-9_-]+$"
          description: Custom slug (requires a custom domain, not available on fnl.sh)
        domain:
          type: string
          description: Custom domain hostname. Defaults to fnl.sh.
        title:
          type: string
          description: Display name in the dashboard
        tags:
          type: array
          items:
            type: string
            pattern: "^[a-z0-9_-]+$"
            maxLength: 32
          maxItems: 10
          description: Tags for organizing links
        note:
          type: string
          description: Internal annotation (never exposed to visitors)
        utm_params:
          $ref: "#/components/schemas/UTMParams"
        expires_at:
          type: string
          format: date-time
          description: Auto-expire the link after this time (ISO 8601)
        fallback_url:
          type: string
          format: uri
          description: Redirect here instead of returning 410 when the link expires
        password:
          type: string
          minLength: 8
          description: Password-protect the link (min 8 characters)
        routing_rules:
          $ref: "#/components/schemas/RoutingRules"
        og_title:
          type: string
          description: Open Graph title for social previews
        og_description:
          type: string
          description: Open Graph description for social previews
        og_image:
          type: string
          format: uri
          description: Open Graph image URL for social previews
        ab_variants:
          type: array
          items:
            $ref: "#/components/schemas/ABVariant"
          minItems: 2
          maxItems: 4
          description: A/B test variants (Team+ plan, weights must sum to 100)
        deep_link:
          $ref: "#/components/schemas/DeepLinkConfig"

    UpdateLinkRequest:
      type: object
      description: All fields are optional. Send `null` to clear a field.
      properties:
        target:
          type: string
          format: uri
        slug:
          type: string
          nullable: true
        domain:
          type: string
        title:
          type: string
          nullable: true
        tags:
          type: array
          items:
            type: string
          maxItems: 10
          nullable: true
        note:
          type: string
          nullable: true
        utm_params:
          allOf:
            - $ref: "#/components/schemas/UTMParams"
          nullable: true
        expires_at:
          type: string
          format: date-time
          nullable: true
        fallback_url:
          type: string
          format: uri
          nullable: true
        password:
          type: string
          minLength: 8
          nullable: true
          description: Set to change, null to remove
        routing_rules:
          allOf:
            - $ref: "#/components/schemas/RoutingRules"
          nullable: true
        og_title:
          type: string
          nullable: true
        og_description:
          type: string
          nullable: true
        og_image:
          type: string
          format: uri
          nullable: true
        ab_variants:
          type: array
          items:
            $ref: "#/components/schemas/ABVariant"
          nullable: true
        deep_link:
          allOf:
            - $ref: "#/components/schemas/DeepLinkConfig"
          nullable: true

    LinkResponse:
      type: object
      description: Response after creating a link
      properties:
        id:
          type: string
        domain:
          type: string
        slug:
          type: string
        target:
          type: string
        title:
          type: string
          nullable: true
        tags:
          type: array
          items:
            type: string
        note:
          type: string
          nullable: true
        utm_params:
          allOf:
            - $ref: "#/components/schemas/UTMParams"
          nullable: true
        expires_at:
          type: string
          format: date-time
          nullable: true
        fallback_url:
          type: string
          nullable: true
        routing_rules:
          allOf:
            - $ref: "#/components/schemas/RoutingRules"
          nullable: true
        og_title:
          type: string
          nullable: true
        og_description:
          type: string
          nullable: true
        og_image:
          type: string
          nullable: true
        has_password:
          type: boolean
        ab_variants:
          type: array
          items:
            $ref: "#/components/schemas/ABVariant"
          nullable: true
        deep_link:
          allOf:
            - $ref: "#/components/schemas/DeepLinkConfig"
          nullable: true
        short_url:
          type: string
          format: uri

    Link:
      type: object
      properties:
        id:
          type: string
        workspace_id:
          type: string
        domain:
          type: string
        slug:
          type: string
        target:
          type: string
        title:
          type: string
          nullable: true
        tags:
          type: array
          items:
            type: string
        note:
          type: string
          nullable: true
        utm_params:
          type: string
          nullable: true
          description: JSON string
        expires_at:
          type: string
          format: date-time
          nullable: true
        fallback_url:
          type: string
          nullable: true
        routing_rules:
          type: string
          nullable: true
          description: JSON string
        og_title:
          type: string
          nullable: true
        og_description:
          type: string
          nullable: true
        og_image:
          type: string
          nullable: true
        has_password:
          type: boolean
        ab_variants:
          type: array
          items:
            $ref: "#/components/schemas/ABVariant"
          nullable: true
        deep_link:
          allOf:
            - $ref: "#/components/schemas/DeepLinkConfig"
          nullable: true
        click_count:
          type: integer
        created_by:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    Tag:
      type: object
      properties:
        name:
          type: string
        color:
          type: string
          description: Hex color (e.g. #ff0000)
        description:
          type: string
        date_from:
          type: string
          format: date
          nullable: true
        date_to:
          type: string
          format: date
          nullable: true

    Workspace:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        plan:
          type: string
        organization_id:
          type: string
          nullable: true
        parent_workspace_id:
          type: string
          nullable: true
        ga_id:
          type: string
          nullable: true
        fb_pixel_id:
          type: string
          nullable: true
        retargeting_scripts:
          type: string
          nullable: true
          description: JSON array of script URLs (Team+ plan)
        default_utm_params:
          type: string
          nullable: true
          description: JSON object of default UTM parameters
        expired_page_url:
          type: string
          nullable: true
          description: Custom expired link page URL
        not_found_page_url:
          type: string
          nullable: true
          description: Custom 404 page URL (custom domains only)
        member_count:
          type: integer
        created_at:
          type: string
          format: date-time

    UpdateWorkspaceRequest:
      type: object
      properties:
        name:
          type: string
        ga_id:
          type: string
          nullable: true
        ga_api_secret:
          type: string
          nullable: true
        fb_pixel_id:
          type: string
          nullable: true
        fb_access_token:
          type: string
          nullable: true
        retargeting_scripts:
          type: array
          items:
            type: string
          nullable: true
          description: Script URLs (Team+ plan). Send null to clear.
        default_utm_params:
          allOf:
            - $ref: "#/components/schemas/UTMParams"
          nullable: true
        expired_page_url:
          type: string
          format: uri
          nullable: true
        not_found_page_url:
          type: string
          format: uri
          nullable: true

    Domain:
      type: object
      properties:
        id:
          type: string
        workspace_id:
          type: string
        domain:
          type: string
        verified:
          type: integer
          description: "0 = unverified, 1 = verified"
        txt_record:
          type: string
        created_at:
          type: string
          format: date-time

    Member:
      type: object
      properties:
        id:
          type: string
        email:
          type: string
        name:
          type: string
          nullable: true
        role:
          $ref: "#/components/schemas/Role"
        joined_at:
          type: string
          format: date-time

    Invite:
      type: object
      properties:
        id:
          type: string
        workspace_id:
          type: string
        email:
          type: string
        role:
          $ref: "#/components/schemas/Role"
        token:
          type: string
        invited_by:
          type: string
        invited_by_email:
          type: string
        expires_at:
          type: string
          format: date-time
        accepted_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time

    ApiKey:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        key_prefix:
          type: string
          description: First 10 characters of the key
        created_at:
          type: string
          format: date-time
        created_by_email:
          type: string

    AuditLogEntry:
      type: object
      properties:
        id:
          type: integer
        workspace_id:
          type: string
        user_id:
          type: string
        action:
          type: string
          description: "Action type (e.g. link.create, member.remove, domain.verify)"
        details:
          type: string
          nullable: true
          description: JSON string with action-specific data
        created_at:
          type: string
          format: date-time
