openapi: 3.1.0
info:
  title: HookSense API
  version: 1.0.0
  description: |
    HookSense is a webhook inspection SaaS that lets you capture, inspect, replay,
    and monitor incoming webhooks in real time. This specification covers all public
    and authenticated API endpoints.

    ## Authentication
    Most endpoints require a JWT token stored in an HTTP-only cookie named `token`.
    The cookie is set automatically on signup, login, or GitHub OAuth callback.

    ## Plans
    HookSense offers three plans: **Free**, **Hook**, and **Sense**. Many features
    are gated by plan. Endpoints that require a paid plan return `403` with
    `{ "error": "...", "upgrade": true }`.

    ## Rate Limiting
    Rate-limited endpoints return `429` when the limit is exceeded. Rate limit
    metadata is conveyed via standard headers.
  contact:
    name: HookSense
    url: https://hooksense.com
  license:
    name: Proprietary

servers:
  - url: https://hooksense.com
    description: Production
  - url: http://localhost:3001
    description: Development

tags:
  - name: Auth
    description: Signup, login, logout, email verification, password reset, OAuth
  - name: Endpoints
    description: Create, list, update, claim, and configure webhook endpoints
  - name: Requests
    description: View captured webhook requests, replay, diagnose
  - name: Billing
    description: Checkout sessions, customer portal, promo codes, LemonSqueezy webhook
  - name: Notifications
    description: Notification channels (email, Slack, Discord)
  - name: Teams
    description: Team management, invites, members
  - name: Admin
    description: Admin-only routes for promos, stats, feature flags
  - name: Webhooks
    description: Webhook capture endpoint (the URL providers send webhooks to)
  - name: Health
    description: Server health check

components:
  securitySchemes:
    cookieAuth:
      type: apiKey
      in: cookie
      name: token
      description: JWT token stored in an HTTP-only cookie

  headers:
    X-RateLimit-Limit:
      description: Maximum number of requests allowed in the current window
      schema:
        type: integer
    X-RateLimit-Remaining:
      description: Number of requests remaining in the current window
      schema:
        type: integer
    X-RateLimit-Reset:
      description: Unix timestamp (seconds) when the rate limit window resets
      schema:
        type: integer

  schemas:
    Error:
      type: object
      required:
        - error
      properties:
        error:
          type: string
        upgrade:
          type: boolean
          description: Present and true when the error is due to plan limits

    Ok:
      type: object
      properties:
        ok:
          type: boolean
          example: true

    User:
      type: object
      properties:
        id:
          type: string
          format: uuid
        email:
          type: string
          format: email
        name:
          type: string
          nullable: true
        avatarUrl:
          type: string
          nullable: true
        plan:
          $ref: "#/components/schemas/PlanName"
        planExpiresAt:
          type: string
          format: date-time
          nullable: true
        emailVerified:
          type: boolean
        isAdmin:
          type: boolean

    UserAuth:
      type: object
      description: User object returned from signup and login (no isAdmin or planExpiresAt)
      properties:
        id:
          type: string
          format: uuid
        email:
          type: string
          format: email
        name:
          type: string
          nullable: true
        avatarUrl:
          type: string
          nullable: true
        plan:
          $ref: "#/components/schemas/PlanName"
        emailVerified:
          type: boolean

    PlanName:
      type: string
      enum:
        - free
        - hook
        - sense

    Endpoint:
      type: object
      properties:
        id:
          type: string
          format: uuid
        slug:
          type: string
        userId:
          type: string
          format: uuid
          nullable: true
        createdAt:
          type: string
          format: date-time
        expiresAt:
          type: string
          format: date-time
          nullable: true
        customResponseStatus:
          type: integer
          default: 200
        customResponseBody:
          type: string
          nullable: true
        customResponseHeaders:
          type: object
          additionalProperties:
            type: string
        customDomain:
          type: string
          nullable: true
        customDomainVerifiedAt:
          type: string
          format: date-time
          nullable: true
        webhookSecret:
          type: string
          nullable: true
        signatureProvider:
          type: string
          nullable: true
          enum:
            - stripe
            - github
            - shopify
            - custom
            - null
        inactivityThreshold:
          type: integer
          description: Inactivity threshold in minutes for monitoring alerts

    WebhookRequest:
      type: object
      properties:
        id:
          type: string
          format: uuid
        endpointId:
          type: string
          format: uuid
        method:
          type: string
        headers:
          type: object
          additionalProperties:
            type: string
        queryParams:
          type: object
          additionalProperties:
            type: string
        body:
          type: string
          nullable: true
        contentType:
          type: string
          nullable: true
        sourceIp:
          type: string
          nullable: true
        sizeBytes:
          type: integer
        detectedProvider:
          type: string
          nullable: true
          enum:
            - stripe
            - github
            - shopify
            - twilio
            - sendgrid
            - paddle
            - linear
            - slack
            - null
        receivedAt:
          type: string
          format: date-time

    SignatureVerification:
      type: object
      properties:
        valid:
          type: boolean
        provider:
          type: string
          enum:
            - stripe
            - github
            - shopify
            - custom
        algorithm:
          type: string
        headerUsed:
          type: string
        detectedProvider:
          type: string
          nullable: true

    ReplayResult:
      type: object
      properties:
        status:
          type: integer
        statusText:
          type: string
        headers:
          type: object
          additionalProperties:
            type: string
        body:
          type: string

    ReplayLog:
      type: object
      properties:
        id:
          type: string
          format: uuid
        requestId:
          type: string
          format: uuid
        targetUrl:
          type: string
        responseStatus:
          type: integer
          nullable: true
        responseDurationMs:
          type: integer
          nullable: true
        replayedAt:
          type: string
          format: date-time

    EndpointAnalytics:
      type: object
      properties:
        totalRequests:
          type: integer
        methodBreakdown:
          type: object
          additionalProperties:
            type: integer
        avgSizeBytes:
          type: number
        requestsOverTime:
          type: array
          items:
            type: object
            properties:
              timestamp:
                type: string
              count:
                type: integer
        topSourceIps:
          type: array
          items:
            type: object
            properties:
              ip:
                type: string
              count:
                type: integer
        peakHour:
          type: string
        avgRequestsPerHour:
          type: number

    HealthMetrics:
      type: object
      properties:
        status:
          type: string
          enum:
            - healthy
            - warning
            - down
        summary:
          $ref: "#/components/schemas/MetricsSummary"
        timeline:
          type: array
          items:
            $ref: "#/components/schemas/MetricsTimeline"
        period:
          type: string

    MetricsSummary:
      type: object
      properties:
        totalRequests:
          type: integer
        successCount:
          type: integer
        failCount:
          type: integer
        failRate:
          type: number
          description: Percentage with 2 decimal places
        lastRequestAt:
          type: string
          format: date-time
          nullable: true

    MetricsTimeline:
      type: object
      properties:
        id:
          type: string
        endpointId:
          type: string
          format: uuid
        period:
          type: string
        periodStart:
          type: string
          format: date-time
        totalRequests:
          type: integer
        successCount:
          type: integer
        failCount:
          type: integer
        avgLatencyMs:
          type: number
          nullable: true
        p95LatencyMs:
          type: number
          nullable: true
        lastRequestAt:
          type: string
          format: date-time
          nullable: true

    HealthSummary:
      type: object
      properties:
        status:
          type: string
          enum:
            - healthy
            - warning
            - down
        totalRequests:
          type: integer
        failRate:
          type: number
        lastRequestAt:
          type: string
          format: date-time
          nullable: true

    AlertState:
      type: object
      properties:
        id:
          type: string
          format: uuid
        endpointId:
          type: string
          format: uuid
        ruleType:
          type: string
          enum:
            - stopped
            - high_fail_rate
            - latency_spike
        state:
          type: string
          enum:
            - healthy
            - alerting
            - recovered
        triggeredAt:
          type: string
          format: date-time
          nullable: true
        resolvedAt:
          type: string
          format: date-time
          nullable: true
        lastNotifiedAt:
          type: string
          format: date-time
          nullable: true

    RetryJob:
      type: object
      properties:
        id:
          type: string
          format: uuid
        endpointId:
          type: string
          format: uuid
        requestId:
          type: string
          format: uuid
        targetUrl:
          type: string
        attemptNumber:
          type: integer
        maxAttempts:
          type: integer
        nextRetryAt:
          type: string
          format: date-time
        status:
          type: string
          enum:
            - pending
            - running
            - succeeded
            - failed
            - exhausted
        lastError:
          type: string
          nullable: true
        lastResponseStatus:
          type: integer
          nullable: true
        lastResponseDurationMs:
          type: integer
          nullable: true
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        diagnosis:
          $ref: "#/components/schemas/Diagnosis"

    Diagnosis:
      type: object
      properties:
        category:
          type: string
          enum:
            - auth
            - server
            - network
            - timeout
            - signature
            - unknown
        title:
          type: string
        suggestion:
          type: string
        severity:
          type: string
          enum:
            - info
            - warning
            - critical

    NotificationChannel:
      type: object
      properties:
        id:
          type: string
          format: uuid
        userId:
          type: string
          format: uuid
        endpointId:
          type: string
          format: uuid
          nullable: true
        type:
          type: string
          enum:
            - email
            - slack
            - discord
        target:
          type: string
        enabled:
          type: boolean
        createdAt:
          type: string
          format: date-time

    Team:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        slug:
          type: string
        ownerId:
          type: string
          format: uuid
          nullable: true
        role:
          type: string
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    TeamMember:
      type: object
      properties:
        id:
          type: string
          format: uuid
        userId:
          type: string
          format: uuid
        role:
          type: string
        joinedAt:
          type: string
          format: date-time
        email:
          type: string
          format: email
        name:
          type: string
          nullable: true
        avatarUrl:
          type: string
          nullable: true

    TeamDetail:
      allOf:
        - $ref: "#/components/schemas/Team"
        - type: object
          properties:
            members:
              type: array
              items:
                $ref: "#/components/schemas/TeamMember"
            endpoints:
              type: array
              items:
                $ref: "#/components/schemas/Endpoint"

    TeamInvite:
      type: object
      properties:
        id:
          type: string
          format: uuid
        teamId:
          type: string
          format: uuid
        email:
          type: string
          format: email
        role:
          type: string
        token:
          type: string
        expiresAt:
          type: string
          format: date-time
        createdAt:
          type: string
          format: date-time

    AdminStats:
      type: object
      properties:
        totalUsers:
          type: integer
        totalEndpoints:
          type: integer
        totalRequests:
          type: integer

    PromoCode:
      type: object
      properties:
        id:
          type: string
          format: uuid
        code:
          type: string
        plan:
          type: string
        durationDays:
          type: integer
        maxRedemptions:
          type: integer
          nullable: true
        currentRedemptions:
          type: integer
        active:
          type: boolean
        expiresAt:
          type: string
          format: date-time
          nullable: true
        createdAt:
          type: string
          format: date-time

    PromoRedemption:
      type: object
      properties:
        userId:
          type: string
          format: uuid
        userEmail:
          type: string
          format: email
        userName:
          type: string
          nullable: true
        redeemedAt:
          type: string
          format: date-time

    FeatureFlag:
      type: object
      properties:
        key:
          type: string
        enabled:
          type: boolean
        category:
          type: string
        description:
          type: string
          nullable: true

  responses:
    BadRequest:
      description: Invalid request parameters
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Unauthorized:
      description: Authentication required or invalid credentials
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Forbidden:
      description: Insufficient permissions or plan upgrade required
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Conflict:
      description: Resource already exists
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    TooManyRequests:
      description: Rate limit exceeded
      headers:
        X-RateLimit-Limit:
          $ref: "#/components/headers/X-RateLimit-Limit"
        X-RateLimit-Remaining:
          $ref: "#/components/headers/X-RateLimit-Remaining"
        X-RateLimit-Reset:
          $ref: "#/components/headers/X-RateLimit-Reset"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    ServerError:
      description: Internal server error
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"

paths:
  # ─── Auth ───────────────────────────────────────────────────────────────

  /api/auth/signup:
    post:
      operationId: signup
      tags:
        - Auth
      summary: Create a new account
      description: |
        Register a new user with email and password. Sets a JWT cookie on success
        and sends a verification email. Rate limited to 5 attempts per IP per 15 minutes.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - email
                - password
              properties:
                email:
                  type: string
                  format: email
                  maxLength: 254
                password:
                  type: string
                  minLength: 8
                  maxLength: 128
      responses:
        "201":
          description: Account created successfully
          headers:
            Set-Cookie:
              description: JWT token cookie
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UserAuth"
        "400":
          $ref: "#/components/responses/BadRequest"
        "409":
          description: Email already registered
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429":
          $ref: "#/components/responses/TooManyRequests"

  /api/auth/login:
    post:
      operationId: login
      tags:
        - Auth
      summary: Log in with email and password
      description: |
        Authenticate with email and password. Sets a JWT cookie on success.
        Progressive lockout applies after repeated failed attempts: delay after 5
        failures, longer delay after 10, and full block after 15 within a 15-minute window.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - email
                - password
              properties:
                email:
                  type: string
                  format: email
                password:
                  type: string
      responses:
        "200":
          description: Login successful
          headers:
            Set-Cookie:
              description: JWT token cookie
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UserAuth"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          description: Invalid email or password
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429":
          $ref: "#/components/responses/TooManyRequests"

  /api/auth/logout:
    post:
      operationId: logout
      tags:
        - Auth
      summary: Log out
      description: Clears the JWT cookie.
      responses:
        "200":
          description: Logged out
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Ok"

  /api/auth/me:
    get:
      operationId: getMe
      tags:
        - Auth
      summary: Get current authenticated user
      description: |
        Returns the currently authenticated user or `{ user: null }` if not logged in.
        Also performs silent token refresh when the JWT is past its half-life (12 hours)
        and checks for plan expiry with automatic downgrade.
      security:
        - cookieAuth: []
        - {}
      responses:
        "200":
          description: Current user (or null if unauthenticated)
          content:
            application/json:
              schema:
                type: object
                properties:
                  user:
                    oneOf:
                      - $ref: "#/components/schemas/User"
                      - type: "null"

  /api/auth/verify-email:
    post:
      operationId: verifyEmail
      tags:
        - Auth
      summary: Verify email address
      description: Verify a user's email using the token sent via email.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - token
              properties:
                token:
                  type: string
      responses:
        "200":
          description: Email verified (or already verified)
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  emailVerified:
                    type: boolean
                  error:
                    type: string
        "400":
          $ref: "#/components/responses/BadRequest"

  /api/auth/resend-verification:
    post:
      operationId: resendVerification
      tags:
        - Auth
      summary: Resend email verification
      description: Regenerates the verification token and re-sends the verification email. Requires authentication.
      security:
        - cookieAuth: []
      responses:
        "200":
          description: Verification email sent
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Ok"
        "400":
          description: Email is already verified
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /api/auth/forgot-password:
    post:
      operationId: forgotPassword
      tags:
        - Auth
      summary: Request password reset
      description: |
        Sends a password reset email if the account exists. Always returns success
        to prevent email enumeration. Rate limited to 5 per IP per 15 minutes.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - email
              properties:
                email:
                  type: string
                  format: email
      responses:
        "200":
          description: Reset email sent (always returns success)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Ok"
        "400":
          $ref: "#/components/responses/BadRequest"
        "429":
          $ref: "#/components/responses/TooManyRequests"

  /api/auth/reset-password:
    post:
      operationId: resetPassword
      tags:
        - Auth
      summary: Reset password with token
      description: Set a new password using a reset token received via email.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - token
                - password
              properties:
                token:
                  type: string
                password:
                  type: string
                  minLength: 8
                  maxLength: 128
      responses:
        "200":
          description: Password reset successfully
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Ok"
        "400":
          description: Invalid or expired token, or password too short
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/auth/github:
    get:
      operationId: githubOAuthStart
      tags:
        - Auth
      summary: Start GitHub OAuth flow
      description: |
        Redirects the user to GitHub for OAuth authorization. Sets a `github_oauth_state`
        cookie for CSRF protection. Returns 503 if GitHub OAuth is not configured.
      responses:
        "302":
          description: Redirect to GitHub authorization URL
        "503":
          description: GitHub OAuth not configured
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/auth/github/callback:
    get:
      operationId: githubOAuthCallback
      tags:
        - Auth
      summary: GitHub OAuth callback
      description: |
        Handles the OAuth callback from GitHub. Validates the state parameter,
        exchanges the authorization code for an access token, creates or links the
        user account, and redirects to the app with a JWT cookie set.
      parameters:
        - name: code
          in: query
          required: true
          schema:
            type: string
        - name: state
          in: query
          required: true
          schema:
            type: string
      responses:
        "302":
          description: Redirect to app on success, or to login page with error parameter on failure

  /api/auth/cli:
    get:
      operationId: cliLogin
      tags:
        - Auth
      summary: Browser-based login for CLI
      description: |
        The CLI opens this URL in the user's browser. If the user is already
        authenticated, it generates a fresh JWT and POSTs it to the CLI's local
        callback server. If not logged in, redirects to the login page with a
        return URL. Port must be in range 1024-65535.
      security:
        - cookieAuth: []
        - {}
      parameters:
        - name: port
          in: query
          required: true
          description: The local port where the CLI is listening for the callback
          schema:
            type: integer
            minimum: 1024
            maximum: 65535
      responses:
        "200":
          description: HTML page that auto-submits the token to the CLI's local server
          content:
            text/html:
              schema:
                type: string
        "302":
          description: Redirect to login page if not authenticated
        "400":
          description: Missing or invalid port parameter
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  # ─── Endpoints ──────────────────────────────────────────────────────────

  /api/features:
    get:
      operationId: getFeatureFlags
      tags:
        - Endpoints
      summary: Get public feature flags
      description: Returns a map of feature flag keys to their enabled status.
      responses:
        "200":
          description: Feature flags map
          content:
            application/json:
              schema:
                type: object
                additionalProperties:
                  type: boolean

  /api/endpoints:
    get:
      operationId: listEndpoints
      tags:
        - Endpoints
      summary: List authenticated user's endpoints
      description: Returns all webhook endpoints owned by the authenticated user.
      security:
        - cookieAuth: []
      responses:
        "200":
          description: List of endpoints
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Endpoint"
        "401":
          $ref: "#/components/responses/Unauthorized"

    post:
      operationId: createEndpoint
      tags:
        - Endpoints
      summary: Create a new webhook endpoint
      description: |
        Creates a new webhook capture endpoint. Anonymous users can create endpoints
        without authentication. Authenticated users are subject to plan-based endpoint
        limits. Rate limited to 5 per IP per 15 minutes.
      security:
        - cookieAuth: []
        - {}
      responses:
        "201":
          description: Endpoint created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Endpoint"
        "403":
          description: Endpoint limit reached for current plan
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Error"
                  - type: object
                    properties:
                      current:
                        type: integer
                      max:
                        type: integer
        "429":
          $ref: "#/components/responses/TooManyRequests"

  /api/endpoints/{slug}:
    get:
      operationId: getEndpoint
      tags:
        - Endpoints
      summary: Get endpoint by slug
      description: Returns a single endpoint by its slug. No authentication required.
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Endpoint details
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Endpoint"
        "404":
          $ref: "#/components/responses/NotFound"

    patch:
      operationId: updateEndpoint
      tags:
        - Endpoints
      summary: Update endpoint configuration
      description: |
        Update endpoint settings such as custom response, webhook secret, signature
        provider, custom slug, or inactivity threshold. Only the endpoint owner can
        modify it. Some fields require specific plans:
        - Custom response body/headers/status: Hook plan or above
        - Custom slug: Hook plan or above
        - Webhook secret / signature provider: any paid plan
        - Inactivity threshold: Hook plan or above
      security:
        - cookieAuth: []
        - {}
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                customResponseStatus:
                  type: integer
                customResponseBody:
                  type: string
                  nullable: true
                customResponseHeaders:
                  type: object
                  additionalProperties:
                    type: string
                webhookSecret:
                  type: string
                  nullable: true
                signatureProvider:
                  type: string
                  nullable: true
                  enum:
                    - stripe
                    - github
                    - shopify
                    - custom
                    - null
                slug:
                  type: string
                  description: Custom slug (3-32 chars, lowercase alphanumeric and hyphens)
                  minLength: 3
                  maxLength: 32
                inactivityThreshold:
                  type: integer
                  description: Minutes before an inactivity alert fires
                  enum:
                    - 30
                    - 60
                    - 120
                    - 360
                    - 720
                    - 1440
      responses:
        "200":
          description: Updated endpoint
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Endpoint"
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: Slug already in use
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/endpoints/{slug}/claim:
    post:
      operationId: claimEndpoint
      tags:
        - Endpoints
      summary: Claim an anonymous endpoint
      description: |
        Claim ownership of an anonymous endpoint after logging in. The endpoint
        must not already be owned. Sets the expiry based on the user's plan retention.
      security:
        - cookieAuth: []
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Endpoint claimed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Endpoint"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: Endpoint already owned
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/endpoints/{slug}/domain:
    patch:
      operationId: setCustomDomain
      tags:
        - Endpoints
      summary: Set or clear a custom domain
      description: |
        Assign a custom domain to an endpoint or clear it by sending `null`.
        Requires the Sense plan. Returns a DNS verification token when setting a domain.
      security:
        - cookieAuth: []
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - domain
              properties:
                domain:
                  type: string
                  nullable: true
                  description: The custom domain to set, or null to clear
      responses:
        "200":
          description: Domain updated. When setting a domain, includes verificationToken.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Endpoint"
                  - type: object
                    properties:
                      verificationToken:
                        type: string
                        description: DNS TXT verification token (only present when setting a domain)
        "400":
          description: Invalid domain format
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          description: Requires Sense plan or not authorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          $ref: "#/components/responses/NotFound"

  /api/endpoints/{slug}/domain/verify:
    post:
      operationId: verifyCustomDomain
      tags:
        - Endpoints
      summary: Verify custom domain DNS records
      description: |
        Checks TXT and CNAME DNS records for the custom domain. Rate limited to
        3 per minute per IP. Returns the verified endpoint on success.
      security:
        - cookieAuth: []
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Domain verified
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Endpoint"
        "400":
          description: DNS verification failed or no custom domain set
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Error"
                  - type: object
                    properties:
                      txtVerified:
                        type: boolean
                      cnameVerified:
                        type: boolean
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/TooManyRequests"

  # ─── Requests ───────────────────────────────────────────────────────────

  /api/endpoints/{slug}/requests:
    get:
      operationId: listRequests
      tags:
        - Requests
      summary: List captured requests for an endpoint
      description: Returns captured webhook requests for the given endpoint, with pagination and optional text search.
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
        - name: offset
          in: query
          schema:
            type: integer
            default: 0
        - name: search
          in: query
          description: Free-text search filter
          schema:
            type: string
      responses:
        "200":
          description: List of captured requests
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/WebhookRequest"
        "404":
          $ref: "#/components/responses/NotFound"

  /api/endpoints/{slug}/export:
    get:
      operationId: exportRequests
      tags:
        - Requests
      summary: Export requests as JSON or CSV
      description: |
        Export all captured requests for an endpoint. Requires Hook plan or above.
        Supports JSON and CSV formats.
      security:
        - cookieAuth: []
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
        - name: format
          in: query
          schema:
            type: string
            enum:
              - json
              - csv
            default: json
      responses:
        "200":
          description: Exported data
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/WebhookRequest"
            text/csv:
              schema:
                type: string
        "400":
          description: Invalid format
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          description: Requires Hook plan or above
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          $ref: "#/components/responses/NotFound"

  /api/requests/{id}:
    get:
      operationId: getRequest
      tags:
        - Requests
      summary: Get a single captured request
      description: Returns full details of a specific captured webhook request by ID.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Request detail
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WebhookRequest"
        "404":
          $ref: "#/components/responses/NotFound"

  /api/requests/{id}/replay:
    post:
      operationId: replayRequest
      tags:
        - Requests
      summary: Replay a captured request
      description: |
        Re-send a captured webhook request to a target URL. Optionally override
        the HTTP method, headers, or body. Subject to replay limits based on plan.
      security:
        - cookieAuth: []
        - {}
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - targetUrl
              properties:
                targetUrl:
                  type: string
                  format: uri
                method:
                  type: string
                  description: Override the original HTTP method
                headers:
                  type: object
                  additionalProperties:
                    type: string
                  description: Override the original headers
                body:
                  type: string
                  description: Override the original request body
      responses:
        "200":
          description: Replay result
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ReplayResult"
        "400":
          description: Missing targetUrl or replay failed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Replay limit reached or not authorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          $ref: "#/components/responses/NotFound"

  /api/endpoints/{slug}/replays:
    get:
      operationId: listReplayLogs
      tags:
        - Requests
      summary: List replay logs for an endpoint
      description: Returns the last 50 replay logs for all requests belonging to this endpoint.
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Replay log entries
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/ReplayLog"
        "404":
          $ref: "#/components/responses/NotFound"

  /api/endpoints/{slug}/verify/{requestId}:
    get:
      operationId: verifyRequestSignature
      tags:
        - Requests
      summary: Verify HMAC signature for a request
      description: |
        Verify the HMAC signature of a captured request against the endpoint's
        webhook secret. Requires a paid plan and a configured webhook secret.
      security:
        - cookieAuth: []
        - {}
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
        - name: requestId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Signature verification result
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SignatureVerification"
        "400":
          description: No webhook secret configured
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Requires a paid plan
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          $ref: "#/components/responses/NotFound"

  /api/requests/diagnose:
    post:
      operationId: diagnoseRequest
      tags:
        - Requests
      summary: Auto-diagnose a request
      description: |
        Analyze request parameters (status code, error, latency, HMAC validity)
        and return a diagnosis with category, severity, and actionable suggestion.
        Requires the Sense plan and the auto_diagnosis feature flag.
      security:
        - cookieAuth: []
        - {}
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                statusCode:
                  type: integer
                error:
                  type: string
                durationMs:
                  type: number
                hmacValid:
                  type: boolean
      responses:
        "200":
          description: Diagnosis result
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Diagnosis"
        "403":
          description: Requires Sense plan
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Feature not available
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/endpoints/{slug}/analytics:
    get:
      operationId: getEndpointAnalytics
      tags:
        - Endpoints
      summary: Get endpoint analytics
      description: |
        Returns analytics data for a webhook endpoint including method breakdown,
        request volume over time, top source IPs, and peak hour. The available
        time period depends on the user's plan retention limits.
      security:
        - cookieAuth: []
        - {}
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
        - name: period
          in: query
          schema:
            type: string
            enum:
              - 24h
              - 7d
              - 30d
            default: "24h"
      responses:
        "200":
          description: Analytics data
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EndpointAnalytics"
        "400":
          description: Invalid period
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Period exceeds plan retention limit
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          $ref: "#/components/responses/NotFound"

  /api/endpoints/{slug}/metrics:
    get:
      operationId: getEndpointMetrics
      tags:
        - Endpoints
      summary: Get health dashboard metrics
      description: |
        Returns health metrics with timeline data for the specified period.
        Includes status (healthy/warning/down), summary stats, and time-series data.
        Requires Hook plan or above and the health_dashboard feature flag.
      security:
        - cookieAuth: []
        - {}
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
        - name: period
          in: query
          schema:
            type: string
            enum:
              - 24h
              - 7d
              - 30d
            default: "24h"
      responses:
        "200":
          description: Health metrics with timeline
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HealthMetrics"
        "400":
          description: Invalid period
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Requires Hook plan or above, or period exceeds retention
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Endpoint not found or feature not available
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/endpoints/{slug}/metrics/summary:
    get:
      operationId: getEndpointMetricsSummary
      tags:
        - Endpoints
      summary: Get lightweight health summary
      description: |
        Returns a quick health check for the last 24 hours. Lighter weight than
        the full metrics endpoint. Requires Hook plan or above and the
        health_dashboard feature flag.
      security:
        - cookieAuth: []
        - {}
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Health summary
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HealthSummary"
        "403":
          description: Requires Hook plan or above
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Endpoint not found or feature not available
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/endpoints/{slug}/alerts:
    get:
      operationId: getEndpointAlerts
      tags:
        - Endpoints
      summary: Get alert history for an endpoint
      description: |
        Returns all alert states for an endpoint. Requires Hook plan or above
        and the email_alerts feature flag.
      security:
        - cookieAuth: []
        - {}
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Alert states
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/AlertState"
        "403":
          description: Requires Hook plan or above
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Endpoint not found or feature not available
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/endpoints/{slug}/retries:
    get:
      operationId: listRetryJobs
      tags:
        - Requests
      summary: List retry jobs for an endpoint
      description: |
        Returns retry job history for an endpoint. Requires the Sense plan
        and the auto_retry feature flag.
      security:
        - cookieAuth: []
        - {}
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Retry jobs
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/RetryJob"
        "403":
          description: Requires Sense plan
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Endpoint not found or feature not available
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

    post:
      operationId: createRetryJob
      tags:
        - Requests
      summary: Create a retry job for a failed request
      description: |
        Schedule automatic retries for a failed webhook delivery. Requires the
        Sense plan, endpoint ownership, and the auto_retry feature flag.
      security:
        - cookieAuth: []
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - requestId
                - targetUrl
              properties:
                requestId:
                  type: string
                  format: uuid
                targetUrl:
                  type: string
                  format: uri
      responses:
        "201":
          description: Retry job created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RetryJob"
        "400":
          description: Missing fields or could not create retry job
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          description: Requires Sense plan or not authorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Endpoint or request not found, or feature not available
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  # ─── Billing ────────────────────────────────────────────────────────────

  /api/billing/checkout:
    post:
      operationId: createCheckout
      tags:
        - Billing
      summary: Create a checkout session
      description: |
        Creates a LemonSqueezy checkout URL for upgrading to a paid plan.
        Returns a URL to redirect the user to the payment page.
      security:
        - cookieAuth: []
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                plan:
                  type: string
                  enum:
                    - hook
                    - sense
                  default: hook
                yearly:
                  type: boolean
                  default: false
      responses:
        "200":
          description: Checkout URL
          content:
            application/json:
              schema:
                type: object
                properties:
                  url:
                    type: string
                    format: uri
        "400":
          description: Invalid plan selection
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          description: Billing not configured or checkout creation failed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/billing/portal:
    post:
      operationId: createPortalSession
      tags:
        - Billing
      summary: Get customer portal URL
      description: |
        Returns the LemonSqueezy customer portal URL where the user can manage
        their subscription. Requires an active subscription.
      security:
        - cookieAuth: []
      responses:
        "200":
          description: Portal URL
          content:
            application/json:
              schema:
                type: object
                properties:
                  url:
                    type: string
                    format: uri
        "400":
          description: No active subscription
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/ServerError"

  /api/billing/redeem:
    post:
      operationId: redeemPromo
      tags:
        - Billing
      summary: Redeem a promo code
      description: Apply a promotional code to upgrade the user's plan for a limited duration.
      security:
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - code
              properties:
                code:
                  type: string
      responses:
        "200":
          description: Promo redeemed
          content:
            application/json:
              schema:
                type: object
                properties:
                  plan:
                    type: string
                  expiresAt:
                    type: string
                    format: date-time
        "400":
          description: Invalid, expired, or already redeemed promo code
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /api/lemonsqueezy/webhook:
    post:
      operationId: lemonSqueezyWebhook
      tags:
        - Billing
      summary: LemonSqueezy webhook handler
      description: |
        Receives subscription lifecycle events from LemonSqueezy. Verified via
        HMAC-SHA256 signature in the `x-signature` header. Handles subscription
        creation, updates, cancellations, expirations, and payment failures.
      parameters:
        - name: x-signature
          in: header
          required: true
          description: HMAC-SHA256 hex signature of the request body
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: LemonSqueezy webhook event payload
      responses:
        "200":
          description: Webhook processed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Ok"
        "401":
          description: Missing or invalid signature
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  # ─── Notifications ─────────────────────────────────────────────────────

  /api/notifications:
    get:
      operationId: listNotificationChannels
      tags:
        - Notifications
      summary: List notification channels
      description: Returns all notification channels owned by the authenticated user.
      security:
        - cookieAuth: []
      responses:
        "200":
          description: Notification channels
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/NotificationChannel"
        "401":
          $ref: "#/components/responses/Unauthorized"

    post:
      operationId: createNotificationChannel
      tags:
        - Notifications
      summary: Create a notification channel
      description: |
        Create a new notification channel (email, Slack webhook, or Discord webhook).
        Can optionally be scoped to a specific endpoint. Subject to plan-based
        channel limits.
      security:
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - type
                - target
              properties:
                type:
                  type: string
                  enum:
                    - email
                    - slack
                    - discord
                target:
                  type: string
                  description: Email address or webhook URL (must be https:// for slack/discord)
                endpointId:
                  type: string
                  format: uuid
                  nullable: true
                  description: Scope notifications to a specific endpoint
      responses:
        "201":
          description: Channel created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/NotificationChannel"
        "400":
          description: Invalid channel type, target, or email format
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          description: Channel limit reached or endpoint not owned
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Error"
                  - type: object
                    properties:
                      current:
                        type: integer
                      max:
                        type: integer

  /api/notifications/{id}:
    delete:
      operationId: deleteNotificationChannel
      tags:
        - Notifications
      summary: Delete a notification channel
      description: Delete a notification channel owned by the authenticated user.
      security:
        - cookieAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Channel deleted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Ok"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

    patch:
      operationId: toggleNotificationChannel
      tags:
        - Notifications
      summary: Toggle notification channel enabled state
      description: Enable or disable a notification channel.
      security:
        - cookieAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - enabled
              properties:
                enabled:
                  type: boolean
      responses:
        "200":
          description: Channel updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/NotificationChannel"
        "400":
          description: enabled must be a boolean
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"

  /api/notifications/{id}/test:
    post:
      operationId: testNotificationChannel
      tags:
        - Notifications
      summary: Send a test notification
      description: Send a test message through the specified notification channel to verify it works.
      security:
        - cookieAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Test notification sent
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Ok"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          description: Test notification failed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  # ─── Teams ──────────────────────────────────────────────────────────────

  /api/teams:
    get:
      operationId: listTeams
      tags:
        - Teams
      summary: List user's teams
      description: Returns all teams the authenticated user is a member of.
      security:
        - cookieAuth: []
      responses:
        "200":
          description: Teams list
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Team"
        "401":
          $ref: "#/components/responses/Unauthorized"

    post:
      operationId: createTeam
      tags:
        - Teams
      summary: Create a new team
      description: Create a team with a unique slug. The creator becomes the owner.
      security:
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
                - slug
              properties:
                name:
                  type: string
                slug:
                  type: string
                  description: URL-friendly identifier (3-32 chars, lowercase alphanumeric and hyphens)
                  minLength: 3
                  maxLength: 32
                  pattern: "^[a-z0-9-]+$"
      responses:
        "201":
          description: Team created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Team"
        "400":
          description: Invalid name or slug
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "409":
          description: Team slug already taken
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/teams/{slug}:
    get:
      operationId: getTeamDetail
      tags:
        - Teams
      summary: Get team detail with members and endpoints
      description: |
        Returns full team details including members and endpoints.
        Only accessible to team members.
      security:
        - cookieAuth: []
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Team detail
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TeamDetail"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          description: Not a team member
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          $ref: "#/components/responses/NotFound"

  /api/teams/{slug}/invite:
    post:
      operationId: inviteToTeam
      tags:
        - Teams
      summary: Invite a user to the team by email
      description: |
        Send an email invitation to join the team. Only team owners and admins
        can invite new members. The invite includes a token link.
      security:
        - cookieAuth: []
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - email
              properties:
                email:
                  type: string
                  format: email
                role:
                  type: string
                  enum:
                    - admin
                    - member
                    - viewer
                  default: member
      responses:
        "201":
          description: Invite sent
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TeamInvite"
        "400":
          description: Invalid email or role
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          description: Only owners and admins can invite
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          $ref: "#/components/responses/NotFound"

  /api/teams/{slug}/join:
    post:
      operationId: joinTeam
      tags:
        - Teams
      summary: Accept a team invite
      description: Accept an invitation to join a team using the invite token.
      security:
        - cookieAuth: []
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - token
              properties:
                token:
                  type: string
      responses:
        "200":
          description: Joined team
          content:
            application/json:
              schema:
                type: object
                properties:
                  team:
                    $ref: "#/components/schemas/Team"
                  member:
                    $ref: "#/components/schemas/TeamMember"
        "400":
          description: Invalid, expired, or already accepted invite
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /api/teams/{slug}/members/{userId}:
    delete:
      operationId: removeTeamMember
      tags:
        - Teams
      summary: Remove a team member
      description: |
        Remove a member from the team. Owners and admins can remove other members.
        Users can always remove themselves (leave the team). The team owner cannot be removed.
      security:
        - cookieAuth: []
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
        - name: userId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Member removed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Ok"
        "400":
          description: Cannot remove the team owner
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          description: Not authorized to remove members
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Team or member not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

    patch:
      operationId: updateTeamMemberRole
      tags:
        - Teams
      summary: Change a team member's role
      description: |
        Update the role of a team member. Only the team owner can change roles.
        The owner's own role cannot be changed.
      security:
        - cookieAuth: []
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
        - name: userId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - role
              properties:
                role:
                  type: string
                  enum:
                    - admin
                    - member
                    - viewer
      responses:
        "200":
          description: Role updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TeamMember"
        "400":
          description: Invalid role or cannot change owner's role
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          description: Only the team owner can change roles
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Team or member not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  # ─── Admin ──────────────────────────────────────────────────────────────

  /api/admin/stats:
    get:
      operationId: getAdminStats
      tags:
        - Admin
      summary: Get aggregate statistics
      description: Returns total counts of users, endpoints, and requests. Admin only.
      security:
        - cookieAuth: []
      responses:
        "200":
          description: Aggregate stats
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AdminStats"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /api/admin/promos:
    get:
      operationId: listAdminPromos
      tags:
        - Admin
      summary: List all promo codes
      description: Returns all promo codes with their redemption counts. Admin only.
      security:
        - cookieAuth: []
      responses:
        "200":
          description: Promo codes
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/PromoCode"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

    post:
      operationId: createAdminPromo
      tags:
        - Admin
      summary: Create a promo code
      description: Create a new promotional code. Admin only.
      security:
        - cookieAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - code
                - plan
                - durationDays
              properties:
                code:
                  type: string
                plan:
                  type: string
                  enum:
                    - hook
                    - sense
                durationDays:
                  type: integer
                  minimum: 1
                maxRedemptions:
                  type: integer
                  nullable: true
                expiresAt:
                  type: string
                  format: date-time
                  nullable: true
      responses:
        "201":
          description: Promo code created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PromoCode"
        "400":
          description: Missing or invalid fields
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          description: Promo code already exists
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          $ref: "#/components/responses/ServerError"

  /api/admin/promos/{id}:
    patch:
      operationId: updateAdminPromo
      tags:
        - Admin
      summary: Update or toggle a promo code
      description: |
        Update promo code fields or toggle its active status. Send `{ active: boolean }`
        to toggle, or send other fields to update. Admin only.
      security:
        - cookieAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                active:
                  type: boolean
                plan:
                  type: string
                  enum:
                    - hook
                    - sense
                durationDays:
                  type: integer
                  minimum: 1
                maxRedemptions:
                  type: integer
                  nullable: true
                expiresAt:
                  type: string
                  format: date-time
                  nullable: true
      responses:
        "200":
          description: Promo code updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PromoCode"
        "400":
          description: Invalid fields
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"

  /api/admin/promos/{id}/redemptions:
    get:
      operationId: getAdminPromoRedemptions
      tags:
        - Admin
      summary: List redemptions for a promo code
      description: Returns which users redeemed a specific promo code. Admin only.
      security:
        - cookieAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Redemption list
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/PromoRedemption"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /api/admin/features:
    get:
      operationId: listAdminFeatureFlags
      tags:
        - Admin
      summary: List all feature flags
      description: Returns all feature flags with their metadata. Admin only.
      security:
        - cookieAuth: []
      responses:
        "200":
          description: Feature flags
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/FeatureFlag"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /api/admin/features/{key}:
    patch:
      operationId: toggleAdminFeatureFlag
      tags:
        - Admin
      summary: Toggle a feature flag
      description: Enable or disable a feature flag by its key. Admin only.
      security:
        - cookieAuth: []
      parameters:
        - name: key
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - enabled
              properties:
                enabled:
                  type: boolean
      responses:
        "200":
          description: Flag updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FeatureFlag"
        "400":
          description: enabled (boolean) is required
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"

  # ─── Webhooks (Capture) ────────────────────────────────────────────────

  /w/{slug}:
    get:
      operationId: captureWebhookGet
      tags:
        - Webhooks
      summary: Capture a webhook (GET)
      description: |
        Capture an incoming webhook request. This is the URL you give to external
        services. Supports all HTTP methods. The captured request is stored and
        broadcast to connected WebSocket clients in real time.

        Rate limited to 60 requests per minute per IP per endpoint. Payload size
        is limited based on the endpoint owner's plan (1MB free, 10MB paid).
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Webhook captured
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Ok"
        "404":
          $ref: "#/components/responses/NotFound"
        "410":
          description: Endpoint has expired
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "413":
          description: Payload too large
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                  limit:
                    type: string
        "429":
          description: Rate limit or daily request limit exceeded
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

    post:
      operationId: captureWebhookPost
      tags:
        - Webhooks
      summary: Capture a webhook (POST)
      description: Capture an incoming POST webhook request. See GET for full documentation.
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      requestBody:
        description: Any webhook payload
        content:
          application/json:
            schema:
              type: object
          application/x-www-form-urlencoded:
            schema:
              type: object
          text/plain:
            schema:
              type: string
          "*/*":
            schema:
              type: string
      responses:
        "200":
          description: Webhook captured
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Ok"
        "404":
          $ref: "#/components/responses/NotFound"
        "410":
          description: Endpoint has expired
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "413":
          description: Payload too large
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                  limit:
                    type: string
        "429":
          description: Rate limit or daily request limit exceeded
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

    put:
      operationId: captureWebhookPut
      tags:
        - Webhooks
      summary: Capture a webhook (PUT)
      description: Capture an incoming PUT webhook request. See GET for full documentation.
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      requestBody:
        description: Any webhook payload
        content:
          "*/*":
            schema:
              type: string
      responses:
        "200":
          description: Webhook captured
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Ok"
        "404":
          $ref: "#/components/responses/NotFound"
        "410":
          description: Endpoint has expired
        "413":
          description: Payload too large
        "429":
          description: Rate limit exceeded

    patch:
      operationId: captureWebhookPatch
      tags:
        - Webhooks
      summary: Capture a webhook (PATCH)
      description: Capture an incoming PATCH webhook request. See GET for full documentation.
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      requestBody:
        description: Any webhook payload
        content:
          "*/*":
            schema:
              type: string
      responses:
        "200":
          description: Webhook captured
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Ok"
        "404":
          $ref: "#/components/responses/NotFound"
        "410":
          description: Endpoint has expired
        "413":
          description: Payload too large
        "429":
          description: Rate limit exceeded

    delete:
      operationId: captureWebhookDelete
      tags:
        - Webhooks
      summary: Capture a webhook (DELETE)
      description: Capture an incoming DELETE webhook request. See GET for full documentation.
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Webhook captured
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Ok"
        "404":
          $ref: "#/components/responses/NotFound"
        "410":
          description: Endpoint has expired
        "413":
          description: Payload too large
        "429":
          description: Rate limit exceeded

  # ─── Health ─────────────────────────────────────────────────────────────

  /api/health:
    get:
      operationId: healthCheck
      tags:
        - Health
      summary: Server health check
      description: Returns server status and current timestamp. No authentication required.
      responses:
        "200":
          description: Server is healthy
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: ok
                  timestamp:
                    type: string
                    format: date-time
