openapi: 3.1.0
info:
  title: melinks API
  description: |
    melinks is a fast, secure, privacy-focused link-in-bio tool designed for AI agents.
    Create link lists programmatically, then share the claim URL so users can take ownership.
  version: 1.0.0
  contact:
    url: https://melinks.co

servers:
  - url: https://melinks.co
    description: Production

paths:
  /api/v1/links:
    post:
      operationId: createLinkList
      summary: Create a new link list
      description: |
        Creates a new unclaimed link list. Returns a public URL and a claim token.
        The claim token allows the creator or intended owner to later claim the link list.
        No authentication required. Rate limited to 10 requests per hour per IP.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateLinkListRequest"
      responses:
        "201":
          description: Link list created successfully
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreateLinkListResponse"
        "400":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429":
          description: Rate limited

  /api/v1/links/{id}:
    get:
      operationId: getLinkList
      summary: Get a link list
      description: Fetch a link list by shortcode or username.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Shortcode or username
      responses:
        "200":
          description: Link list data
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LinkList"
        "404":
          description: Not found

    put:
      operationId: updateLinkList
      summary: Update a link list
      description: Update a link list. Requires a claim token (Bearer) or authenticated session.
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateLinkListRequest"
      responses:
        "200":
          description: Updated successfully
        "401":
          description: Unauthorized
        "404":
          description: Not found

    delete:
      operationId: deleteLinkList
      summary: Delete a link list
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Deleted successfully
        "401":
          description: Unauthorized
        "404":
          description: Not found

  /api/v1/click/{linkId}:
    get:
      operationId: trackClick
      summary: Track a click and redirect
      description: Records an anonymous click event and redirects to the link URL. Used as href in link pages.
      parameters:
        - name: linkId
          in: path
          required: true
          schema:
            type: string
      responses:
        "302":
          description: Redirects to link URL
        "404":
          description: Link not found

  /api/v1/links/{id}/versions:
    get:
      operationId: listVersions
      summary: List version history
      description: List version history for a link list. Requires authorization.
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Shortcode or username
        - name: limit
          in: query
          schema:
            type: integer
            default: 10
            maximum: 50
        - name: offset
          in: query
          schema:
            type: integer
            default: 0
      responses:
        "200":
          description: Version history
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VersionList"
        "401":
          description: Unauthorized
        "404":
          description: Not found

  /api/v1/links/{id}/versions/{version}:
    get:
      operationId: getVersion
      summary: Get a specific version
      description: Get details of a specific version including the full snapshot.
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - name: version
          in: path
          required: true
          schema:
            type: integer
      responses:
        "200":
          description: Version details
        "401":
          description: Unauthorized
        "404":
          description: Not found

  /api/v1/links/{id}/rollback:
    post:
      operationId: rollbackVersion
      summary: Rollback to a previous version
      description: Saves the current state as a new version, then restores a previous version.
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [version_number]
              properties:
                version_number:
                  type: integer
                  minimum: 1
      responses:
        "200":
          description: Rollback successful
          content:
            application/json:
              schema:
                type: object
                properties:
                  new_version_number:
                    type: integer
        "401":
          description: Unauthorized
        "404":
          description: Not found

  /api/v1/links/{id}/export:
    get:
      operationId: exportLinkList
      summary: Export a link list as JSON
      description: |
        Export a link list in melinks JSON format (v1). The response is a downloadable JSON file
        containing the full link list data. No authentication required for public export.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Shortcode or username
      responses:
        "200":
          description: JSON export file
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MelinksExportFormat"
          headers:
            Content-Disposition:
              schema:
                type: string
              description: 'attachment; filename="melinks-{identifier}.json"'
        "404":
          description: Not found

  /api/v1/links/import:
    post:
      operationId: importLinkList
      summary: Create a link list from JSON import
      description: |
        Create a new link list by importing a melinks JSON file (v1 format).
        Returns the same response as creating a link list, including a claim token.
        Rate limited to 10 requests per hour per IP.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/MelinksExportFormat"
      responses:
        "201":
          description: Link list created from import
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreateLinkListResponse"
        "400":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429":
          description: Rate limited

  /api/v1/links/{id}/import:
    put:
      operationId: importOverwriteLinkList
      summary: Overwrite a link list from JSON import
      description: |
        Overwrite an existing link list by importing a melinks JSON file (v1 format).
        Requires a claim token (Bearer) or API key for authorization.
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Shortcode or username of the list to overwrite
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/MelinksExportFormat"
      responses:
        "200":
          description: Link list overwritten successfully
        "400":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Unauthorized
        "404":
          description: Not found

  /api/v1/images:
    post:
      summary: Upload image
      description: Upload an image file for use as avatar, SEO image, or link thumbnail. Max 3MB, JPEG/PNG/WebP/GIF.
      operationId: uploadImage
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file, purpose]
              properties:
                file:
                  type: string
                  format: binary
                  description: Image file (JPEG, PNG, WebP, or GIF, max 3MB)
                purpose:
                  type: string
                  enum: [avatar, seo, thumbnail]
                  description: Image purpose
      responses:
        "201":
          description: Image uploaded
          content:
            application/json:
              schema:
                type: object
                properties:
                  url:
                    type: string
                    format: uri
                    description: URL of the uploaded image
        "400":
          description: Validation error
        "401":
          description: Unauthorized

  /api/v1/images/{path}:
    get:
      summary: Serve image
      description: Retrieve an uploaded image by path. Images served with long-lived cache headers.
      operationId: getImage
      parameters:
        - name: path
          in: path
          required: true
          schema:
            type: string
          description: Image path (e.g. avatars/abc123.jpg)
      responses:
        "200":
          description: Image file
          content:
            image/*:
              schema:
                type: string
                format: binary
        "404":
          description: Image not found

  /api/v1/links/{id}/analytics:
    get:
      operationId: getAnalytics
      summary: Get link list analytics (Pro)
      description: |
        Get detailed click analytics for a link list. Returns time series, top countries,
        top referrers, and per-link click breakdown. Requires Pro subscription.
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Link list ID
        - name: range
          in: query
          schema:
            type: string
            enum: [7d, 30d, 90d]
            default: 30d
          description: Time range for analytics data
      responses:
        "200":
          description: Analytics data
          content:
            application/json:
              schema:
                type: object
                properties:
                  range:
                    type: string
                  total_clicks:
                    type: integer
                  time_series:
                    type: array
                    items:
                      type: object
                      properties:
                        date:
                          type: string
                        clicks:
                          type: integer
                  countries:
                    type: array
                    items:
                      type: object
                      properties:
                        country:
                          type: string
                        clicks:
                          type: integer
                  referrers:
                    type: array
                    items:
                      type: object
                      properties:
                        referrer:
                          type: string
                        clicks:
                          type: integer
                  links:
                    type: array
                    items:
                      type: object
                      properties:
                        title:
                          type: string
                        clicks:
                          type: integer
        "401":
          description: Unauthorized
        "403":
          description: Pro subscription required

  /api/v1/links/{id}/ab-test:
    post:
      operationId: createAbTest
      summary: Create A/B test (Pro)
      description: |
        Start an A/B test. Variant A is the current page. Variant B is provided as a snapshot.
        Requires Pro subscription. Only one active test per link list.
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Link list ID
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [variant_b]
              properties:
                variant_b:
                  $ref: "#/components/schemas/Snapshot"
                traffic_split:
                  type: integer
                  minimum: 0
                  maximum: 100
                  default: 50
                  description: Percentage of traffic to variant B
      responses:
        "201":
          description: A/B test created
          content:
            application/json:
              schema:
                type: object
                properties:
                  test_id:
                    type: string
        "401":
          description: Unauthorized
        "403":
          description: Pro subscription required
    get:
      operationId: getAbTest
      summary: Get A/B test status (Pro)
      description: Get the current A/B test and per-variant click stats.
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Link list ID
      responses:
        "200":
          description: A/B test data
          content:
            application/json:
              schema:
                type: object
                properties:
                  active:
                    type: boolean
                  test_id:
                    type: string
                  status:
                    type: string
                    enum: [running, paused, completed]
                  traffic_split:
                    type: integer
                  winner:
                    type: string
                    nullable: true
                  stats:
                    type: object
                    properties:
                      a:
                        type: object
                        properties:
                          clicks:
                            type: integer
                      b:
                        type: object
                        properties:
                          clicks:
                            type: integer
        "401":
          description: Unauthorized
    put:
      operationId: updateAbTest
      summary: Update A/B test (Pro)
      description: Update traffic split, variant B content, or pause/resume a test.
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Link list ID
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                traffic_split:
                  type: integer
                  minimum: 0
                  maximum: 100
                variant_b:
                  $ref: "#/components/schemas/Snapshot"
                status:
                  type: string
                  enum: [running, paused]
      responses:
        "200":
          description: Test updated
        "401":
          description: Unauthorized
    delete:
      operationId: cancelAbTest
      summary: Cancel A/B test (Pro)
      description: Cancel and delete an A/B test without applying changes.
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Link list ID
      responses:
        "200":
          description: Test cancelled
        "401":
          description: Unauthorized

  /api/v1/links/{id}/ab-test/conclude:
    post:
      operationId: concludeAbTest
      summary: Conclude A/B test (Pro)
      description: End a test and apply the winning variant to the live page.
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Link list ID
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [winner]
              properties:
                winner:
                  type: string
                  enum: [a, b]
      responses:
        "200":
          description: Test concluded, winner applied
        "401":
          description: Unauthorized

  /api/v1/links/{id}/qr:
    get:
      operationId: getQrCode
      summary: Download QR code
      description: Generate and download a QR code for the link page as SVG.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Shortcode or username
        - name: size
          in: query
          schema:
            type: integer
            minimum: 128
            maximum: 1024
            default: 256
          description: QR code size in pixels
      responses:
        "200":
          description: SVG QR code
          content:
            image/svg+xml:
              schema:
                type: string
          headers:
            Content-Disposition:
              schema:
                type: string
              description: 'attachment; filename="melinks-{id}-qr.svg"'
        "404":
          description: Not found

  /api/v1/domains:
    post:
      operationId: registerCustomDomain
      summary: Register a custom domain (Pro)
      description: Register a custom domain for a link page. Requires Pro subscription.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [domain, link_list_id]
              properties:
                domain:
                  type: string
                  description: Domain name (e.g. links.example.com)
                link_list_id:
                  type: string
                  description: Link list ID to associate the domain with
      responses:
        "201":
          description: Domain registered
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                  domain:
                    type: string
                  status:
                    type: string
                    enum: [pending, active]
                  cname_target:
                    type: string
                    description: CNAME record target for DNS configuration
        "401":
          description: Unauthorized
        "403":
          description: Pro subscription required
    get:
      operationId: listCustomDomains
      summary: List custom domains (Pro)
      description: List all custom domains for the authenticated user.
      security:
        - bearerAuth: []
      responses:
        "200":
          description: List of custom domains
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  properties:
                    id:
                      type: string
                    domain:
                      type: string
                    status:
                      type: string
                    link_list_id:
                      type: string
        "401":
          description: Unauthorized

  /api/v1/domains/{id}:
    get:
      operationId: refreshCustomDomain
      summary: Refresh custom domain status (Pro)
      description: Check and refresh the status of a custom domain.
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Domain status
        "401":
          description: Unauthorized
        "404":
          description: Not found
    delete:
      operationId: removeCustomDomain
      summary: Remove a custom domain (Pro)
      description: Remove a custom domain and its Cloudflare Custom Hostname.
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Domain removed
        "401":
          description: Unauthorized
        "404":
          description: Not found

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: Claim token or API key

  schemas:
    CreateLinkListRequest:
      type: object
      required: [display_name, links]
      properties:
        display_name:
          type: string
          minLength: 1
          maxLength: 100
          description: Name displayed at the top of the link page
        bio:
          type: string
          maxLength: 500
          description: Short bio or description
        avatar_url:
          type: string
          format: uri
          description: Avatar image URL (must be https)
        theme:
          type: string
          enum: [default, dark, minimal, bold, pastel, custom]
          description: Page theme (default if omitted)
        custom_theme:
          $ref: "#/components/schemas/CustomTheme"
        button_style:
          type: string
          enum: [rounded, pill, sharp, soft]
          description: Button shape (rounded if omitted)
        seo_title:
          type: string
          maxLength: 100
          description: Custom Open Graph title (falls back to display_name)
        seo_description:
          type: string
          maxLength: 200
          description: Custom Open Graph description (falls back to bio)
        seo_image:
          type: string
          format: uri
          description: Custom Open Graph image URL (falls back to avatar_url)
        links:
          type: array
          minItems: 1
          maxItems: 50
          items:
            type: object
            required: [title]
            properties:
              type:
                type: string
                enum: [link, header, text]
                default: link
                description: "Content type: link (clickable URL), header (section heading), or text (paragraph)"
              title:
                type: string
                minLength: 1
                maxLength: 500
                description: Link title, heading text, or paragraph content
              url:
                type: string
                format: uri
                description: Link URL (required when type is link, ignored for header/text)
              icon:
                type: string
                maxLength: 50
                description: Optional platform icon identifier (auto-detected from URL if omitted)
              thumbnail_url:
                type: string
                format: uri
                description: Thumbnail image URL for the link
              visible_from:
                type: integer
                description: Unix timestamp (ms) when link becomes visible
              visible_until:
                type: integer
                description: Unix timestamp (ms) when link stops being visible
        page_pin:
          type: string
          pattern: "^\\d{4,8}$"
          description: 4-8 digit PIN to protect the page

    CreateLinkListResponse:
      type: object
      properties:
        url:
          type: string
          format: uri
          description: Public URL of the link page
        shortcode:
          type: string
          description: Unique shortcode identifier
        claim_token:
          type: string
          description: Secret token to claim ownership. Store securely.
        claim_url:
          type: string
          format: uri
          description: URL where the owner can claim this link list

    UpdateLinkListRequest:
      type: object
      properties:
        display_name:
          type: string
          minLength: 1
          maxLength: 100
        bio:
          type: string
          maxLength: 500
        avatar_url:
          type: string
          format: uri
          nullable: true
        theme:
          type: string
          enum: [default, dark, minimal, bold, pastel, custom]
        custom_theme:
          $ref: "#/components/schemas/CustomTheme"
        button_style:
          type: string
          enum: [rounded, pill, sharp, soft]
        seo_title:
          type: string
          maxLength: 100
          nullable: true
          description: Custom Open Graph title (null to clear)
        seo_description:
          type: string
          maxLength: 200
          nullable: true
          description: Custom Open Graph description (null to clear)
        seo_image:
          type: string
          format: uri
          nullable: true
          description: Custom Open Graph image URL (null to clear)
        links:
          type: array
          minItems: 1
          maxItems: 50
          items:
            type: object
            required: [title]
            properties:
              type:
                type: string
                enum: [link, header, text]
                default: link
                description: "Content type: link, header, or text"
              title:
                type: string
                minLength: 1
                maxLength: 500
              url:
                type: string
                format: uri
                description: Required when type is link
              icon:
                type: string
                maxLength: 50
              thumbnail_url:
                type: string
                format: uri
                description: Thumbnail image URL for the link
              visible_from:
                type: integer
                description: Unix timestamp (ms) when link becomes visible
              visible_until:
                type: integer
                description: Unix timestamp (ms) when link stops being visible
        page_pin:
          type: string
          pattern: "^\\d{4,8}$"
          nullable: true
          description: 4-8 digit PIN to protect the page (null to remove)

    LinkList:
      type: object
      properties:
        shortcode:
          type: string
        username:
          type: string
          nullable: true
        display_name:
          type: string
        bio:
          type: string
          nullable: true
        avatar_url:
          type: string
          nullable: true
        theme:
          type: string
          enum: [default, dark, minimal, bold, pastel, custom]
        custom_theme:
          $ref: "#/components/schemas/CustomTheme"
        button_style:
          type: string
          enum: [rounded, pill, sharp, soft]
        pin_protected:
          type: boolean
          description: Whether the page requires a PIN to view
        seo_title:
          type: string
          nullable: true
          description: Custom Open Graph title
        seo_description:
          type: string
          nullable: true
          description: Custom Open Graph description
        seo_image:
          type: string
          nullable: true
          description: Custom Open Graph image URL
        links:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
              type:
                type: string
                enum: [link, header, text]
                description: "Content type: link, header, or text"
              title:
                type: string
              url:
                type: string
              icon:
                type: string
                nullable: true
              thumbnail_url:
                type: string
                nullable: true
                description: Thumbnail image URL for the link
              sort_order:
                type: integer
              visible_from:
                type: integer
                nullable: true
                description: Unix timestamp (ms) when link becomes visible
              visible_until:
                type: integer
                nullable: true
                description: Unix timestamp (ms) when link stops being visible
        created_at:
          type: integer
          description: Unix timestamp
        updated_at:
          type: integer
          description: Unix timestamp
        claimed:
          type: boolean

    Error:
      type: object
      properties:
        error:
          type: string
        details:
          type: object

    CustomTheme:
      type: object
      description: Custom color overrides (hex format). Only used when theme is "custom".
      properties:
        bg:
          type: string
          pattern: "^#[0-9a-fA-F]{6}$"
          description: Page background color
        text:
          type: string
          pattern: "^#[0-9a-fA-F]{6}$"
          description: Primary text color
        muted:
          type: string
          pattern: "^#[0-9a-fA-F]{6}$"
          description: Muted/secondary text color
        card:
          type: string
          pattern: "^#[0-9a-fA-F]{6}$"
          description: Card background color
        cardText:
          type: string
          pattern: "^#[0-9a-fA-F]{6}$"
          description: Card text color
        border:
          type: string
          pattern: "^#[0-9a-fA-F]{6}$"
          description: Border color
        cardHover:
          type: string
          pattern: "^#[0-9a-fA-F]{6}$"
          description: Card hover background color

    MelinksExportFormat:
      type: object
      required: [melinks_version, link_list]
      properties:
        melinks_version:
          type: string
          enum: ["1.0"]
          description: Format version, must be "1.0"
        exported_at:
          type: string
          format: date-time
          description: ISO 8601 timestamp of when the export was created
        link_list:
          type: object
          required: [display_name, links]
          properties:
            display_name:
              type: string
              minLength: 1
              maxLength: 100
            bio:
              type: string
              maxLength: 500
            avatar_url:
              type: string
              format: uri
            theme:
              type: string
              enum: [default, dark, minimal, bold, pastel, custom]
            custom_theme:
              $ref: "#/components/schemas/CustomTheme"
            button_style:
              type: string
              enum: [rounded, pill, sharp, soft]
            seo_title:
              type: string
              maxLength: 100
              description: Custom Open Graph title
            seo_description:
              type: string
              maxLength: 200
              description: Custom Open Graph description
            seo_image:
              type: string
              format: uri
              description: Custom Open Graph image URL
            links:
              type: array
              minItems: 1
              maxItems: 50
              items:
                type: object
                required: [title]
                properties:
                  type:
                    type: string
                    enum: [link, header, text]
                    default: link
                    description: "Content type: link, header, or text"
                  title:
                    type: string
                    minLength: 1
                    maxLength: 500
                  url:
                    type: string
                    format: uri
                    description: Link URL (present for link type)
                  icon:
                    type: string
                    maxLength: 50
                  thumbnail_url:
                    type: string
                    format: uri
                    description: Thumbnail image URL for the link
                  sort_order:
                    type: integer
                    minimum: 0

    VersionList:
      type: object
      properties:
        total:
          type: integer
        versions:
          type: array
          items:
            type: object
            properties:
              version_number:
                type: integer
              change_summary:
                type: string
                nullable: true
              created_by:
                type: string
                nullable: true
              created_at:
                type: integer
                description: Unix timestamp

    Snapshot:
      type: object
      required: [display_name, theme, button_style, links]
      properties:
        display_name:
          type: string
        bio:
          type: string
          nullable: true
        avatar_url:
          type: string
          nullable: true
        theme:
          type: string
        custom_theme:
          nullable: true
        button_style:
          type: string
        seo_title:
          type: string
          nullable: true
        seo_description:
          type: string
          nullable: true
        seo_image:
          type: string
          nullable: true
        links:
          type: array
          items:
            type: object
            required: [title, url, sort_order]
            properties:
              type:
                type: string
                default: link
              title:
                type: string
              url:
                type: string
              icon:
                type: string
                nullable: true
              thumbnail_url:
                type: string
                nullable: true
              sort_order:
                type: integer
              visible_from:
                type: integer
                nullable: true
              visible_until:
                type: integer
                nullable: true
