openapi: 3.1.0
info:
  title: Produktly API
  version: 1.0.0
  description: >-
    The Produktly REST API gives you programmatic access to your changelogs,
    roadmaps, feedback widgets, NPS widgets, tags, and analytics. It exposes the
    same functionality as the [MCP
    server](https://produktly.com/docs/mcp-server/), over plain HTTP.


    ## Base URL


    ```

    https://api.produktly.com/api/v1

    ```


    ## Authentication


    All endpoints (except `GET /openapi.json`) require an API key passed as a
    Bearer token.


    1. Sign in to Produktly and open **Settings → [API
    keys](https://produktly.com/app/settings/private-keys)**.

    2. Generate a new key and copy it. The same key works for both REST and MCP.

    3. Send it on every request:


    ```http

    GET /api/v1/changelogs HTTP/1.1

    Host: api.produktly.com

    Authorization: Bearer <your-api-key>

    ```


    Missing or invalid keys return `401 unauthorized`. Treat the key like a
    password — anyone with it can read and write your Produktly data.


    ## Response format


    List endpoints return an envelope with a `data` array and `pagination`
    metadata:


    ```json

    {
      "data": [ { "id": 1, "name": "..." } ],
      "pagination": { "total": 42, "limit": 50, "offset": 0 }
    }

    ```


    Single-resource endpoints (e.g. `GET /roadmaps/{id}`) return the resource
    directly, no envelope.


    ## Pagination


    List endpoints accept `limit` (default 50, max 100) and `offset` (default 0)
    query parameters. Date filters (`startDate`, `endDate`) compose with
    pagination where supported.


    ## Errors


    Errors come back in a consistent shape:


    ```json

    {
      "error": {
        "code": "invalid_tag_names",
        "message": "Unknown tag names: foo. Use GET /v1/tags to see available tags.",
        "details": { "missing": ["foo"] }
      }
    }

    ```


    Common codes:


    - `unauthorized` (401) — missing or invalid API key

    - `not_found` (404) — resource doesn't exist or doesn't belong to your
    company

    - `validation_error` (400) — invalid request parameters

    - `rate_limit_exceeded` (429) — see Rate limits below

    - `internal_error` (500) — server-side problem; safe to retry


    ## Rate limits


    Every API key is limited to **60 requests per minute**. Every response
    includes:


    - `X-RateLimit-Limit`: total allowed in the window

    - `X-RateLimit-Remaining`: remaining quota

    - `X-RateLimit-Reset`: Unix seconds when the bucket refills


    When you exceed the limit you'll get `429 rate_limit_exceeded` with a
    `Retry-After` header. Back off and retry.
servers:
  - url: https://api.produktly.com/api/v1
security:
  - BearerAuth: []
components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      description: Use an API key from the Produktly dashboard → Settings → API keys.
  schemas:
    Changelog:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        active:
          type: boolean
        createdAt:
          type: string
          format: date-time
      required:
        - id
        - name
        - active
        - createdAt
    Pagination:
      type: object
      properties:
        total:
          type: integer
        limit:
          type: integer
        offset:
          type: integer
      required:
        - total
        - limit
        - offset
    ChangelogList:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Changelog'
        pagination:
          $ref: '#/components/schemas/Pagination'
      required:
        - data
        - pagination
    Error:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
            message:
              type: string
            details:
              type: object
              additionalProperties: {}
          required:
            - code
            - message
      required:
        - error
    Tag:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        backgroundColor:
          type:
            - string
            - 'null'
        textColor:
          type:
            - string
            - 'null'
      required:
        - id
        - name
        - backgroundColor
        - textColor
    ChangelogPost:
      type: object
      properties:
        id:
          type: integer
        title:
          type: string
        description:
          type:
            - string
            - 'null'
        date:
          type: string
          format: date-time
        active:
          type: boolean
        tags:
          type: array
          items:
            $ref: '#/components/schemas/Tag'
      required:
        - id
        - title
        - description
        - date
        - active
        - tags
    ChangelogPostList:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/ChangelogPost'
        pagination:
          $ref: '#/components/schemas/Pagination'
      required:
        - data
        - pagination
    CreateChangelogPostBody:
      type: object
      properties:
        title:
          type: string
          minLength: 1
        description:
          type: string
        date:
          type: string
        active:
          type: boolean
        tagNames:
          type: array
          items:
            type: string
      required:
        - title
    UpdateChangelogPostBody:
      type: object
      properties:
        title:
          type: string
        description:
          type: string
        date:
          type: string
        active:
          type: boolean
        tagNames:
          type: array
          items:
            type: string
    FeedbackWidget:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        active:
          type: boolean
        createdAt:
          type: string
          format: date-time
      required:
        - id
        - name
        - active
        - createdAt
    FeedbackWidgetList:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/FeedbackWidget'
        pagination:
          $ref: '#/components/schemas/Pagination'
      required:
        - data
        - pagination
    FeedbackResponse:
      type: object
      properties:
        id:
          type: integer
        type:
          type: string
        optionName:
          type:
            - string
            - 'null'
        emoji:
          type:
            - string
            - 'null'
        message:
          type:
            - string
            - 'null'
        email:
          type:
            - string
            - 'null'
        name:
          type:
            - string
            - 'null'
        fromUrl:
          type:
            - string
            - 'null'
        createdAt:
          type: string
          format: date-time
      required:
        - id
        - type
        - optionName
        - emoji
        - message
        - email
        - name
        - fromUrl
        - createdAt
    FeedbackResponseList:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/FeedbackResponse'
        pagination:
          $ref: '#/components/schemas/Pagination'
      required:
        - data
        - pagination
    RoadmapItem:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        description:
          type:
            - string
            - 'null'
        votesCount:
          type: integer
        updatedAt:
          type: string
          format: date-time
        tag:
          allOf:
            - $ref: '#/components/schemas/Tag'
            - type:
                - object
                - 'null'
      required:
        - id
        - name
        - description
        - votesCount
        - updatedAt
        - tag
    RoadmapSection:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        items:
          type: array
          items:
            $ref: '#/components/schemas/RoadmapItem'
      required:
        - id
        - name
    Roadmap:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        publicName:
          type:
            - string
            - 'null'
        active:
          type: boolean
        customDomain:
          type:
            - string
            - 'null'
        publicId:
          type: string
        sections:
          type: array
          items:
            $ref: '#/components/schemas/RoadmapSection'
      required:
        - id
        - name
        - publicName
        - active
        - customDomain
        - publicId
    RoadmapList:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Roadmap'
        pagination:
          $ref: '#/components/schemas/Pagination'
      required:
        - data
        - pagination
    NpsWidget:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        active:
          type: boolean
        createdAt:
          type: string
          format: date-time
      required:
        - id
        - name
        - active
        - createdAt
    NpsWidgetList:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/NpsWidget'
        pagination:
          $ref: '#/components/schemas/Pagination'
      required:
        - data
        - pagination
    NpsScore:
      type: object
      properties:
        npsScore:
          type: number
        totalResponses:
          type: integer
        promoters:
          type: object
          properties:
            count:
              type: integer
            percentage:
              type: number
          required:
            - count
            - percentage
        passives:
          type: object
          properties:
            count:
              type: integer
            percentage:
              type: number
          required:
            - count
            - percentage
        detractors:
          type: object
          properties:
            count:
              type: integer
            percentage:
              type: number
          required:
            - count
            - percentage
      required:
        - npsScore
        - totalResponses
        - promoters
        - passives
        - detractors
    NpsResponse:
      type: object
      properties:
        id:
          type: integer
        rating:
          type: string
        comment:
          type:
            - string
            - 'null'
        email:
          type:
            - string
            - 'null'
        name:
          type:
            - string
            - 'null'
        userId:
          type:
            - string
            - 'null'
        url:
          type:
            - string
            - 'null'
        language:
          type:
            - string
            - 'null'
        createdAt:
          type: string
          format: date-time
      required:
        - id
        - rating
        - comment
        - email
        - name
        - userId
        - url
        - language
        - createdAt
    NpsResponseList:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/NpsResponse'
        pagination:
          $ref: '#/components/schemas/Pagination'
      required:
        - data
        - pagination
    WidgetStats:
      type: object
      properties:
        entityType:
          type: string
        period:
          type: object
          properties:
            start:
              type: string
            end:
              type: string
          required:
            - start
            - end
        widgets:
          type: array
          items:
            type: object
            properties:
              entityId:
                type: integer
              name:
                type:
                  - string
                  - 'null'
              archivedAt:
                type:
                  - string
                  - 'null'
                format: date-time
              events:
                type: object
                additionalProperties:
                  type: object
                  properties:
                    count:
                      type: integer
                    uniqueUsers:
                      type: integer
                  required:
                    - count
                    - uniqueUsers
              totalEvents:
                type: integer
              totalUniqueUsers:
                type: integer
            required:
              - entityId
              - name
              - archivedAt
              - events
              - totalEvents
              - totalUniqueUsers
      required:
        - entityType
        - period
        - widgets
    StatsSummary:
      type: object
      properties:
        period:
          type: object
          properties:
            start:
              type: string
            end:
              type: string
            days:
              type: integer
            granularity:
              type: string
          required:
            - start
            - end
            - days
            - granularity
        previousPeriod:
          type: object
          properties:
            start:
              type: string
            end:
              type: string
          required:
            - start
            - end
        features:
          type: array
          items:
            type: object
            properties:
              type:
                type: string
              label:
                type: string
              primary:
                type: object
                properties:
                  label:
                    type: string
                  count:
                    type: integer
                  previousCount:
                    type: integer
                  changePct:
                    type:
                      - number
                      - 'null'
                required:
                  - label
                  - count
                  - previousCount
                  - changePct
              secondary:
                type:
                  - object
                  - 'null'
                properties:
                  label:
                    type: string
                  value:
                    type:
                      - number
                      - 'null'
                  previousValue:
                    type:
                      - number
                      - 'null'
                  changePp:
                    type:
                      - number
                      - 'null'
                required:
                  - label
                  - value
                  - previousValue
                  - changePp
              events:
                type: object
                additionalProperties:
                  type: object
                  properties:
                    count:
                      type: integer
                    uniqueUsers:
                      type: integer
                    previousCount:
                      type: integer
                    previousUniqueUsers:
                      type: integer
                  required:
                    - count
                    - uniqueUsers
                    - previousCount
                    - previousUniqueUsers
              trend:
                type: array
                items:
                  type: object
                  properties:
                    period:
                      type: string
                    count:
                      type: integer
                  required:
                    - period
                    - count
            required:
              - type
              - label
              - primary
              - secondary
              - events
              - trend
      required:
        - period
        - previousPeriod
        - features
    TagList:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Tag'
        pagination:
          $ref: '#/components/schemas/Pagination'
      required:
        - data
        - pagination
  parameters: {}
paths:
  /changelogs:
    get:
      summary: List changelogs
      tags:
        - Changelogs
      security:
        - BearerAuth: []
      responses:
        '200':
          description: List of changelogs
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChangelogList'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Rate limit exceeded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /changelogs/{changelogId}/posts:
    get:
      summary: List posts in a changelog
      tags:
        - Changelogs
      security:
        - BearerAuth: []
      parameters:
        - schema:
            type: integer
            exclusiveMinimum: 0
          required: true
          name: changelogId
          in: path
        - schema:
            type: integer
            exclusiveMinimum: 0
            maximum: 100
            default: 50
          required: false
          name: limit
          in: query
        - schema:
            type:
              - integer
              - 'null'
            minimum: 0
            default: 0
          required: false
          name: offset
          in: query
        - schema:
            type: string
          required: false
          name: startDate
          in: query
        - schema:
            type: string
          required: false
          name: endDate
          in: query
      responses:
        '200':
          description: List of posts
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChangelogPostList'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Rate limit exceeded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    post:
      summary: Create a changelog post
      tags:
        - Changelogs
      security:
        - BearerAuth: []
      parameters:
        - schema:
            type: integer
            exclusiveMinimum: 0
          required: true
          name: changelogId
          in: path
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateChangelogPostBody'
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChangelogPost'
        '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'
        '429':
          description: Rate limit exceeded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /changelogs/{changelogId}/posts/{postId}:
    patch:
      summary: Update a changelog post
      tags:
        - Changelogs
      security:
        - BearerAuth: []
      parameters:
        - schema:
            type: integer
            exclusiveMinimum: 0
          required: true
          name: changelogId
          in: path
        - schema:
            type: integer
            exclusiveMinimum: 0
          required: true
          name: postId
          in: path
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateChangelogPostBody'
      responses:
        '200':
          description: Updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChangelogPost'
        '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'
        '429':
          description: Rate limit exceeded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /feedback-widgets:
    get:
      summary: List feedback widgets
      tags:
        - Feedback Widgets
      security:
        - BearerAuth: []
      responses:
        '200':
          description: List of feedback widgets
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FeedbackWidgetList'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Rate limit exceeded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /feedback-widgets/{widgetId}/responses:
    get:
      summary: List feedback responses
      tags:
        - Feedback Widgets
      security:
        - BearerAuth: []
      parameters:
        - schema:
            type: integer
            exclusiveMinimum: 0
          required: true
          name: widgetId
          in: path
        - schema:
            type: integer
            exclusiveMinimum: 0
            maximum: 100
            default: 50
          required: false
          name: limit
          in: query
        - schema:
            type:
              - integer
              - 'null'
            minimum: 0
            default: 0
          required: false
          name: offset
          in: query
        - schema:
            type: string
          required: false
          name: startDate
          in: query
        - schema:
            type: string
          required: false
          name: endDate
          in: query
      responses:
        '200':
          description: List of responses
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FeedbackResponseList'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Rate limit exceeded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /roadmaps:
    get:
      summary: List roadmaps
      tags:
        - Roadmaps
      security:
        - BearerAuth: []
      responses:
        '200':
          description: List of roadmaps
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RoadmapList'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Rate limit exceeded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /roadmaps/{roadmapId}:
    get:
      summary: Get roadmap with sections and items
      tags:
        - Roadmaps
      security:
        - BearerAuth: []
      parameters:
        - schema:
            type: integer
            exclusiveMinimum: 0
          required: true
          name: roadmapId
          in: path
        - schema:
            type: string
          required: false
          name: startDate
          in: query
        - schema:
            type: string
          required: false
          name: endDate
          in: query
        - schema:
            type: integer
            exclusiveMinimum: 0
            maximum: 100
          required: false
          name: latestCount
          in: query
        - schema:
            type: integer
            exclusiveMinimum: 0
            maximum: 100
          required: false
          name: limit
          in: query
        - schema:
            type: string
          required: false
          name: query
          in: query
        - schema:
            type: array
            items:
              type: string
          required: false
          name: tagNames
          in: query
      responses:
        '200':
          description: Roadmap
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Roadmap'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Rate limit exceeded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /nps-widgets:
    get:
      summary: List NPS widgets
      tags:
        - NPS Widgets
      security:
        - BearerAuth: []
      responses:
        '200':
          description: List of NPS widgets
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NpsWidgetList'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Rate limit exceeded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /nps-widgets/{widgetId}/score:
    get:
      summary: Get NPS score breakdown
      tags:
        - NPS Widgets
      security:
        - BearerAuth: []
      parameters:
        - schema:
            type: integer
            exclusiveMinimum: 0
          required: true
          name: widgetId
          in: path
        - schema:
            type: string
          required: false
          name: startDate
          in: query
        - schema:
            type: string
          required: false
          name: endDate
          in: query
      responses:
        '200':
          description: NPS score
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NpsScore'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Rate limit exceeded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /nps-widgets/{widgetId}/responses:
    get:
      summary: List NPS responses
      tags:
        - NPS Widgets
      security:
        - BearerAuth: []
      parameters:
        - schema:
            type: integer
            exclusiveMinimum: 0
          required: true
          name: widgetId
          in: path
        - schema:
            type: integer
            exclusiveMinimum: 0
            maximum: 100
            default: 50
          required: false
          name: limit
          in: query
        - schema:
            type:
              - integer
              - 'null'
            minimum: 0
            default: 0
          required: false
          name: offset
          in: query
        - schema:
            type: string
          required: false
          name: startDate
          in: query
        - schema:
            type: string
          required: false
          name: endDate
          in: query
      responses:
        '200':
          description: List of NPS responses
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NpsResponseList'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Rate limit exceeded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /stats/widgets/{entityType}:
    get:
      summary: Widget stats by entity type
      tags:
        - Stats
      security:
        - BearerAuth: []
      parameters:
        - schema:
            type: string
            enum:
              - tour
              - checklist
              - smartTip
              - announcement
              - changelog
              - npsWidget
              - roadmap
          required: true
          name: entityType
          in: path
        - schema:
            type: string
          required: false
          name: startDate
          in: query
        - schema:
            type: string
          required: false
          name: endDate
          in: query
        - schema:
            type: integer
            exclusiveMinimum: 0
          required: false
          name: entityId
          in: query
      responses:
        '200':
          description: Widget stats
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WidgetStats'
        '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 exceeded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /stats/summary:
    get:
      summary: Company-wide stats summary
      tags:
        - Stats
      security:
        - BearerAuth: []
      parameters:
        - schema:
            type: string
          required: false
          name: startDate
          in: query
        - schema:
            type: string
          required: false
          name: endDate
          in: query
      responses:
        '200':
          description: Stats summary
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StatsSummary'
        '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 exceeded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /tags:
    get:
      summary: List tags
      tags:
        - Tags
      security:
        - BearerAuth: []
      responses:
        '200':
          description: List of tags
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TagList'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Rate limit exceeded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
webhooks: {}
