openapi: 3.1.0
info:
  title: Linktrap API
  version: 1.0.0
  description: |
    Linktrap short links with bot-facing Open Graph previews, optional URL cloaking,
    buffer pages, and click analytics.

    **Authentication**
    - **Pro API v1** — `Authorization: Bearer lt_<secret>` (workspace API key)
    - **App API** — Clerk session cookie (browser / same-origin fetch with credentials)
    - **Guest API** — anonymous; sets `lt_guest_session` cookie on first use

    See [REST API overview](/docs/rest-api) and [API v1 reference](/docs/api-v1).
  contact:
    name: Linktrap
  license:
    name: Proprietary

servers:
  - url: /
    description: Current deployment origin (replace with your app URL in production)

tags:
  - name: Links v1
    description: Pro programmatic API (Bearer key)
  - name: Links session
    description: Authenticated app API (Clerk session)
  - name: Links guest
    description: Public quick shorten (no sign-in)
  - name: Workspace
    description: Pro workspace management (Clerk session)
  - name: Wizard
    description: Create-flow helpers (Clerk session)

components:
  securitySchemes:
    bearerApiKey:
      type: http
      scheme: bearer
      bearerFormat: lt_
      description: Pro workspace API key from Dashboard or POST /api/workspace/api-keys
    clerkSession:
      type: apiKey
      in: cookie
      name: __session
      description: Clerk session cookie (browser or credentials include)

  parameters:
    LinkId:
      name: id
      in: path
      required: true
      schema:
        type: string
        format: uuid
      description: Link UUID
    ClicksLimit:
      name: limit
      in: query
      schema:
        type: integer
        minimum: 1
        maximum: 200
        default: 50
      description: Max click rows returned (pagination)
    ClicksOffset:
      name: offset
      in: query
      schema:
        type: integer
        minimum: 0
        default: 0
      description: Pagination offset for click rows

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

    LinkCreateBody:
      type: object
      required: [destinationUrl]
      properties:
        destinationUrl:
          type: string
          format: uri
          example: https://example.com/page
        quickShorten:
          type: boolean
          default: false
          description: Auto-fill safe bot preview (no destination URL in og:description)
        botTitle:
          type: string
          description: Required when quickShorten is false
        botDescription:
          type: string
          description: Required when quickShorten is false
        ogImageUrl:
          type: string
          format: uri
          description: HTTPS image URL for preview cards
        campaign:
          type: string
        notes:
          type: string
        utmSource:
          type: string
        utmMedium:
          type: string
        utmCampaign:
          type: string
        expiresAt:
          type: string
          format: date-time
        isActive:
          type: boolean
          default: true
        humanBufferPage:
          type: boolean
          default: false
          description: Countdown interstitial for human visitors
        urlCloaking:
          type: boolean
          default: false
          description: Full-page iframe; address bar stays on short URL

    LinkPatchBody:
      type: object
      properties:
        destinationUrl:
          type: string
          format: uri
        botTitle:
          type: string
        botDescription:
          type: string
        ogImageUrl:
          type: string
          format: uri
        campaign:
          type: string
        notes:
          type: string
        utmSource:
          type: string
        utmMedium:
          type: string
        utmCampaign:
          type: string
        expiresAt:
          type: string
          format: date-time
          nullable: true
        isActive:
          type: boolean
        humanBufferPage:
          type: boolean
        urlCloaking:
          type: boolean

    LinkRow:
      type: object
      properties:
        id:
          type: string
          format: uuid
        slug:
          type: string
        destinationUrl:
          type: string
          format: uri
        botTitle:
          type: string
        botDescription:
          type: string
        ogImageUrl:
          type: string
          nullable: true
        createdAt:
          type: string
          format: date-time
        workspaceId:
          type: string
          nullable: true
        guestSessionId:
          type: string
          nullable: true
        campaign:
          type: string
          nullable: true
        notes:
          type: string
          nullable: true
        utmSource:
          type: string
          nullable: true
        utmMedium:
          type: string
          nullable: true
        utmCampaign:
          type: string
          nullable: true
        expiresAt:
          type: string
          format: date-time
          nullable: true
        isActive:
          type: boolean
        humanBufferPage:
          type: boolean
        urlCloaking:
          type: boolean
        clickCount:
          type: integer
          description: Human click total (list/get responses)

    LinkCreateResponse:
      type: object
      properties:
        id:
          type: string
          format: uuid
        slug:
          type: string
        shortUrl:
          type: string
          format: uri
        botTitle:
          type: string
        botDescription:
          type: string
        ogImageUrl:
          type: string
          nullable: true
        humanBufferPage:
          type: boolean
        urlCloaking:
          type: boolean

    LinkAnalytics:
      type: object
      properties:
        linkId:
          type: string
          format: uuid
        clickCount:
          type: integer
        summary:
          type: object
          properties:
            byCountry:
              type: array
              items:
                type: object
                properties:
                  country:
                    type: string
                    nullable: true
                  count:
                    type: integer
            topReferrers:
              type: array
              items:
                type: object
                properties:
                  referrer:
                    type: string
                    nullable: true
                  count:
                    type: integer
        clicks:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
                format: uuid
              slug:
                type: string
              createdAt:
                type: string
                format: date-time
              referrer:
                type: string
                nullable: true
              country:
                type: string
                nullable: true
        pagination:
          type: object
          properties:
            limit:
              type: integer
            offset:
              type: integer
            hasMore:
              type: boolean

paths:
  /api/v1/links:
    get:
      operationId: v1ListLinks
      tags: [Links v1]
      summary: List workspace links
      security:
        - bearerApiKey: []
      responses:
        "200":
          description: Links newest first
          content:
            application/json:
              schema:
                type: object
                properties:
                  links:
                    type: array
                    items:
                      $ref: "#/components/schemas/LinkRow"
        "401":
          description: Missing or invalid Bearer token
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    post:
      operationId: v1CreateLink
      tags: [Links v1]
      summary: Create a link
      security:
        - bearerApiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LinkCreateBody"
            examples:
              quickShorten:
                summary: Quick shorten
                value:
                  destinationUrl: https://example.com/page
                  quickShorten: true
              cloaked:
                summary: Quick shorten with URL cloaking
                value:
                  destinationUrl: https://example.com/page
                  quickShorten: true
                  urlCloaking: true
      responses:
        "200":
          description: Created link
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LinkCreateResponse"
        "400":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429":
          description: Rate limit
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/v1/links/{id}:
    get:
      operationId: v1GetLink
      tags: [Links v1]
      summary: Get one link
      security:
        - bearerApiKey: []
      parameters:
        - $ref: "#/components/parameters/LinkId"
      responses:
        "200":
          description: Link row with clickCount
          content:
            application/json:
              schema:
                type: object
                properties:
                  link:
                    $ref: "#/components/schemas/LinkRow"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    patch:
      operationId: v1UpdateLink
      tags: [Links v1]
      summary: Update a link
      security:
        - bearerApiKey: []
      parameters:
        - $ref: "#/components/parameters/LinkId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LinkPatchBody"
      responses:
        "200":
          description: Updated link
          content:
            application/json:
              schema:
                type: object
                properties:
                  link:
                    $ref: "#/components/schemas/LinkRow"
        "400":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    delete:
      operationId: v1DeleteLink
      tags: [Links v1]
      summary: Delete a link
      security:
        - bearerApiKey: []
      parameters:
        - $ref: "#/components/parameters/LinkId"
      responses:
        "200":
          description: Deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    example: true
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/v1/links/{id}/clicks:
    get:
      operationId: v1GetLinkClicks
      tags: [Links v1]
      summary: Click analytics for a link
      description: |
        Returns total click count, aggregated breakdowns (country, referrer),
        and paginated individual click rows.
      security:
        - bearerApiKey: []
      parameters:
        - $ref: "#/components/parameters/LinkId"
        - $ref: "#/components/parameters/ClicksLimit"
        - $ref: "#/components/parameters/ClicksOffset"
      responses:
        "200":
          description: Analytics payload
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LinkAnalytics"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/links:
    get:
      operationId: sessionListLinks
      tags: [Links session]
      summary: List workspace links (session)
      security:
        - clerkSession: []
      responses:
        "200":
          description: Links with clickCount
          content:
            application/json:
              schema:
                type: object
                properties:
                  links:
                    type: array
                    items:
                      $ref: "#/components/schemas/LinkRow"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    post:
      operationId: sessionCreateLink
      tags: [Links session]
      summary: Create a link (session)
      security:
        - clerkSession: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LinkCreateBody"
      responses:
        "200":
          description: Created link
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LinkCreateResponse"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429":
          description: Rate limit
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/links/{id}:
    get:
      operationId: sessionGetLink
      tags: [Links session]
      summary: Get one link (session)
      security:
        - clerkSession: []
      parameters:
        - $ref: "#/components/parameters/LinkId"
      responses:
        "200":
          description: Link with clickCount
          content:
            application/json:
              schema:
                type: object
                properties:
                  link:
                    $ref: "#/components/schemas/LinkRow"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    patch:
      operationId: sessionUpdateLink
      tags: [Links session]
      summary: Update a link (session)
      security:
        - clerkSession: []
      parameters:
        - $ref: "#/components/parameters/LinkId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LinkPatchBody"
      responses:
        "200":
          description: Updated link
          content:
            application/json:
              schema:
                type: object
                properties:
                  link:
                    $ref: "#/components/schemas/LinkRow"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    delete:
      operationId: sessionDeleteLink
      tags: [Links session]
      summary: Delete a link (session)
      security:
        - clerkSession: []
      parameters:
        - $ref: "#/components/parameters/LinkId"
      responses:
        "200":
          description: Deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/links/{id}/clicks:
    get:
      operationId: sessionGetLinkClicks
      tags: [Links session]
      summary: Click analytics for a link (session)
      security:
        - clerkSession: []
      parameters:
        - $ref: "#/components/parameters/LinkId"
        - $ref: "#/components/parameters/ClicksLimit"
        - $ref: "#/components/parameters/ClicksOffset"
      responses:
        "200":
          description: Analytics payload
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LinkAnalytics"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/guest/links:
    post:
      operationId: guestCreateQuickLink
      tags: [Links guest]
      summary: Quick shorten without sign-in
      description: |
        Guest links require `quickShorten: true`. Optional `urlCloaking`.
        Sets `lt_guest_session` cookie on first request. Links expire after 90 days.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [destinationUrl, quickShorten]
              properties:
                destinationUrl:
                  type: string
                  format: uri
                quickShorten:
                  type: boolean
                  enum: [true]
                urlCloaking:
                  type: boolean
                  default: false
      responses:
        "200":
          description: Created guest link
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LinkCreateResponse"
        "400":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Guest quick shorten only
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429":
          description: Rate limit
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/workspace/api-keys:
    get:
      operationId: listApiKeys
      tags: [Workspace]
      summary: List API keys (Pro)
      security:
        - clerkSession: []
      responses:
        "200":
          description: Key metadata (secrets not included)
          content:
            application/json:
              schema:
                type: object
                properties:
                  keys:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                        name:
                          type: string
                          nullable: true
                        createdAt:
                          type: string
                          format: date-time
                        lastUsedAt:
                          type: string
                          format: date-time
                          nullable: true
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    post:
      operationId: createApiKey
      tags: [Workspace]
      summary: Create API key (Pro)
      security:
        - clerkSession: []
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
      responses:
        "200":
          description: Raw key shown once
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                  key:
                    type: string
                  message:
                    type: string
        "402":
          description: Pro subscription required
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    delete:
      operationId: revokeApiKey
      tags: [Workspace]
      summary: Revoke API key
      security:
        - clerkSession: []
      parameters:
        - name: id
          in: query
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Revoked
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean

  /api/workspace/custom-domains:
    get:
      operationId: listCustomDomains
      tags: [Workspace]
      summary: List custom domains (Pro)
      security:
        - clerkSession: []
      responses:
        "200":
          description: Registered domains
          content:
            application/json:
              schema:
                type: object
                properties:
                  domains:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                        host:
                          type: string
                        verified:
                          type: boolean
                        createdAt:
                          type: string
                          format: date-time
    post:
      operationId: addCustomDomain
      tags: [Workspace]
      summary: Register custom domain (Pro)
      security:
        - clerkSession: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [host]
              properties:
                host:
                  type: string
                  example: links.example.com
      responses:
        "200":
          description: Domain registered (verification pending)
        "402":
          description: Pro required
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/generate-meta:
    post:
      operationId: generateMeta
      tags: [Wizard]
      summary: AI-generate bot preview fields
      security:
        - clerkSession: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [destinationUrl]
              properties:
                destinationUrl:
                  type: string
                  format: uri
                hint:
                  type: string
                includeImage:
                  type: boolean
                outputLanguage:
                  type: string
                  enum: [en, th]
      responses:
        "200":
          description: Generated title, description, optional image URL
        "503":
          description: OPENAI_API_KEY or Blob not configured
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/upload-preview-image:
    post:
      operationId: uploadPreviewImage
      tags: [Wizard]
      summary: Upload preview image to Blob
      security:
        - clerkSession: []
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file:
                  type: string
                  format: binary
      responses:
        "200":
          description: Public Blob URL
          content:
            application/json:
              schema:
                type: object
                properties:
                  url:
                    type: string
                    format: uri
