openapi: 3.1.0
info:
  title: Markdown Blog REST API
  description: "REST API for Markdown Blog (markdownblog.dev): create and manage blog posts, query generated articles, and inspect workspace blogs."
  version: 1.0.0
  x-api-version: 1.0.0
  x-api-versioning-policy:
    scheme: URI path versioning (/v1/)
    currentVersion: v1
    noticeTimeline: 90 days minimum advance notice before retirement of any API version
    deprecationHeaders:
      - Deprecation
      - Sunset
      - Link
    documentationUrl: https://markdownblog.dev/docs#versioning
  x-api-deprecation-policy: Markdown Blog guarantees at least 90 days notice before retiring any API version. Deprecation is signaled via RFC 8594 Deprecation header, Sunset header, and Link rel='deprecation' pointing to migration documentation at https://markdownblog.dev/docs#versioning.
  x-cli-manifest:
    name: markdownblog
    package: "@merginit/markdownblog"
    repository: https://github.com/merginit/markdownblog.dev
    install: npm install -g @merginit/markdownblog
    run: npx @merginit/markdownblog <command>
    commands:
      - command: markdownblog generate <title>
        summary: Create or draft a Markdown post with research sources and frontmatter
      - command: markdownblog convert <file>
        summary: Convert HTML or raw text to clean Markdown
      - command: markdownblog audit <file>
        summary: Audit frontmatter, headings, reading time, and citations
      - command: markdownblog posts list
        summary: List posts belonging to the authenticated workspace
      - command: markdownblog whoami
        summary: Verify API key and workspace authentication
  contact:
    name: Markdown Blog Developer Support
    url: https://markdownblog.dev/contact
    email: jonas@merginit.com
  license:
    name: Proprietary
    url: https://markdownblog.dev/terms
servers:
  - url: https://markdownblog.dev
    description: Production API Server
paths:
  /api/v1/health:
    get:
      summary: Check API health and agent readiness
      description: Returns the API service operational status, version, and links to machine-readable agent resources.
      operationId: getHealthStatus
      parameters: []
      responses:
        "200":
          description: API is operational and healthy
          headers:
            RateLimit-Limit:
              $ref: "#/components/headers/RateLimit-Limit"
            RateLimit-Remaining:
              $ref: "#/components/headers/RateLimit-Remaining"
            RateLimit-Reset:
              $ref: "#/components/headers/RateLimit-Reset"
            X-API-Version:
              $ref: "#/components/headers/X-API-Version"
            Deprecation:
              $ref: "#/components/headers/Deprecation"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HealthResponse"
        "500":
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /api/v1/blogs/posts:
    post:
      summary: Create a blog post
      description: Creates a new blog post in a blog using live web research and structured Markdown formatting.
      operationId: createBlogPost
      security:
        - apiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreatePostRequest"
      responses:
        "201":
          description: Post created successfully
          headers:
            RateLimit-Limit:
              $ref: "#/components/headers/RateLimit-Limit"
            RateLimit-Remaining:
              $ref: "#/components/headers/RateLimit-Remaining"
            RateLimit-Reset:
              $ref: "#/components/headers/RateLimit-Reset"
            X-API-Version:
              $ref: "#/components/headers/X-API-Version"
            Deprecation:
              $ref: "#/components/headers/Deprecation"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreatePostResponse"
        "400":
          description: Invalid input parameters
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: "Unauthorized: missing or invalid x-api-key header"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "429":
          description: Rate limit exceeded
          headers:
            Retry-After:
              $ref: "#/components/headers/Retry-After"
            RateLimit-Limit:
              $ref: "#/components/headers/RateLimit-Limit"
            RateLimit-Remaining:
              $ref: "#/components/headers/RateLimit-Remaining"
            RateLimit-Reset:
              $ref: "#/components/headers/RateLimit-Reset"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /api/v1/posts:
    get:
      summary: List blog posts
      description: Retrieves blog posts belonging to the authenticated account.
      operationId: listBlogPosts
      security:
        - apiKeyAuth: []
      parameters:
        - name: id
          in: query
          required: false
          description: Optional specific post identifier to retrieve
          schema:
            type: string
        - name: blogId
          in: query
          required: false
          description: Filter posts by associated blog identifier
          schema:
            type: string
        - name: status
          in: query
          required: false
          description: Filter posts by publication status
          schema:
            type: string
            enum:
              - draft
              - published
              - archived
        - name: limit
          in: query
          required: false
          description: Maximum number of posts to return
          schema:
            type: integer
            default: 20
      responses:
        "200":
          description: List of blog posts
          headers:
            RateLimit-Limit:
              $ref: "#/components/headers/RateLimit-Limit"
            RateLimit-Remaining:
              $ref: "#/components/headers/RateLimit-Remaining"
            RateLimit-Reset:
              $ref: "#/components/headers/RateLimit-Reset"
            X-API-Version:
              $ref: "#/components/headers/X-API-Version"
            Deprecation:
              $ref: "#/components/headers/Deprecation"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PostListResponse"
        "401":
          description: "Unauthorized: missing or invalid x-api-key header"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "429":
          description: Rate limit exceeded
          headers:
            Retry-After:
              $ref: "#/components/headers/Retry-After"
            RateLimit-Limit:
              $ref: "#/components/headers/RateLimit-Limit"
            RateLimit-Remaining:
              $ref: "#/components/headers/RateLimit-Remaining"
            RateLimit-Reset:
              $ref: "#/components/headers/RateLimit-Reset"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
    patch:
      summary: Update a blog post
      description: Updates fields on an existing post such as title, Markdown content, or status.
      operationId: updateBlogPost
      security:
        - apiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdatePostRequest"
      responses:
        "200":
          description: Post updated successfully
          headers:
            RateLimit-Limit:
              $ref: "#/components/headers/RateLimit-Limit"
            RateLimit-Remaining:
              $ref: "#/components/headers/RateLimit-Remaining"
            RateLimit-Reset:
              $ref: "#/components/headers/RateLimit-Reset"
            X-API-Version:
              $ref: "#/components/headers/X-API-Version"
            Deprecation:
              $ref: "#/components/headers/Deprecation"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UpdatePostResponse"
        "400":
          description: Invalid post ID or payload
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: "Unauthorized: missing or invalid x-api-key header"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Post not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "429":
          description: Rate limit exceeded
          headers:
            Retry-After:
              $ref: "#/components/headers/Retry-After"
            RateLimit-Limit:
              $ref: "#/components/headers/RateLimit-Limit"
            RateLimit-Remaining:
              $ref: "#/components/headers/RateLimit-Remaining"
            RateLimit-Reset:
              $ref: "#/components/headers/RateLimit-Reset"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
    delete:
      summary: Delete a blog post
      description: Permanently removes a blog post by its unique identifier.
      operationId: deleteBlogPost
      security:
        - apiKeyAuth: []
      parameters:
        - name: id
          in: query
          required: true
          description: Unique identifier of the post to delete
          schema:
            type: string
      responses:
        "200":
          description: Post deleted successfully
          headers:
            RateLimit-Limit:
              $ref: "#/components/headers/RateLimit-Limit"
            RateLimit-Remaining:
              $ref: "#/components/headers/RateLimit-Remaining"
            RateLimit-Reset:
              $ref: "#/components/headers/RateLimit-Reset"
            X-API-Version:
              $ref: "#/components/headers/X-API-Version"
            Deprecation:
              $ref: "#/components/headers/Deprecation"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DeletePostResponse"
        "400":
          description: Invalid or missing post ID
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: "Unauthorized: missing or invalid x-api-key header"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Post not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "429":
          description: Rate limit exceeded
          headers:
            Retry-After:
              $ref: "#/components/headers/Retry-After"
            RateLimit-Limit:
              $ref: "#/components/headers/RateLimit-Limit"
            RateLimit-Remaining:
              $ref: "#/components/headers/RateLimit-Remaining"
            RateLimit-Reset:
              $ref: "#/components/headers/RateLimit-Reset"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
  /api/v1/blogs:
    get:
      summary: List blogs
      description: Retrieves blogs associated with the authenticated account.
      operationId: listBlogs
      security:
        - apiKeyAuth: []
      parameters:
        - name: id
          in: query
          required: false
          description: Optional specific blog identifier
          schema:
            type: string
      responses:
        "200":
          description: List of blogs
          headers:
            RateLimit-Limit:
              $ref: "#/components/headers/RateLimit-Limit"
            RateLimit-Remaining:
              $ref: "#/components/headers/RateLimit-Remaining"
            RateLimit-Reset:
              $ref: "#/components/headers/RateLimit-Reset"
            X-API-Version:
              $ref: "#/components/headers/X-API-Version"
            Deprecation:
              $ref: "#/components/headers/Deprecation"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BlogListResponse"
        "401":
          description: "Unauthorized: missing or invalid x-api-key header"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "429":
          description: Rate limit exceeded
          headers:
            Retry-After:
              $ref: "#/components/headers/Retry-After"
            RateLimit-Limit:
              $ref: "#/components/headers/RateLimit-Limit"
            RateLimit-Remaining:
              $ref: "#/components/headers/RateLimit-Remaining"
            RateLimit-Reset:
              $ref: "#/components/headers/RateLimit-Reset"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "500":
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
components:
  securitySchemes:
    apiKeyAuth:
      type: apiKey
      name: x-api-key
      in: header
      description: API key passed in the x-api-key header.
  headers:
    RateLimit-Limit:
      description: The maximum number of allowed requests in the current time window.
      schema:
        type: integer
        example: 60
    RateLimit-Remaining:
      description: The number of remaining requests allowed in the current time window.
      schema:
        type: integer
        example: 59
    RateLimit-Reset:
      description: The number of seconds until the current rate limit window resets.
      schema:
        type: integer
        example: 60
    Retry-After:
      description: The number of seconds to wait before retrying after a 429 Rate Limited response.
      schema:
        type: integer
        example: 60
    X-API-Version:
      description: The semantic version of the REST API servicing the request.
      schema:
        type: string
        example: 1.0.0
    Deprecation:
      description: RFC 8594 deprecation signal indicating whether this API version is deprecated.
      schema:
        type: string
        example: "false"
  schemas:
    ErrorResponse:
      type: object
      required:
        - error
        - code
        - statusCode
        - message
      properties:
        error:
          type: string
          description: Machine-readable error category or code.
          example: UNAUTHORIZED
        code:
          type: string
          description: Machine-readable error code for automated agent handling.
          example: UNAUTHORIZED
        statusCode:
          type: integer
          description: HTTP status code matching the response header.
          example: 401
        message:
          type: string
          description: Human-readable explanation of the error condition.
          example: Missing or invalid API key. Please provide your API key in the 'x-api-key' header.
        resolutionHints:
          type: array
          description: Actionable instructions to assist agents and clients in resolving the error.
          items:
            type: string
          example:
            - Provide a valid API key in the 'x-api-key' header.
            - Generate an API key at https://markdownblog.dev/dashboard/settings/api-keys.
            - Read authentication guide at https://markdownblog.dev/docs#authentication.
        documentationUrl:
          type: string
          format: uri
          description: URL to the relevant documentation section for this error.
          example: https://markdownblog.dev/docs#authentication
        retryAfter:
          type: integer
          description: Seconds to wait before retrying (present on 429 Rate Limited responses).
          example: 60
    HealthResponse:
      type: object
      required:
        - status
        - version
        - service
        - agentReadiness
      properties:
        status:
          type: string
          example: ok
        version:
          type: string
          example: 1.0.0
        service:
          type: string
          example: Markdown Blog REST API
        timestamp:
          type: string
          format: date-time
        agentReadiness:
          type: object
          required:
            - llmsTxt
            - openApiSpec
            - developerPortal
            - versioningPolicy
          properties:
            llmsTxt:
              type: string
              format: uri
            openApiSpec:
              type: string
              format: uri
            developerPortal:
              type: string
              format: uri
            versioningPolicy:
              type: string
              format: uri
    CreatePostRequest:
      type: object
      required:
        - blogId
        - title
      properties:
        blogId:
          type: string
          description: Target blog identifier
        title:
          type: string
          description: Post title
        topicDescription:
          type: string
          description: Optional research prompt or description
        lengthInWords:
          type: integer
          description: Target word count
        toneOfVoice:
          type: string
          description: Editorial tone (for example, technical, conversational)
        language:
          type: string
          default: en
          description: Language code
    CreatePostResponse:
      type: object
      required:
        - postId
        - success
      properties:
        postId:
          type: string
          description: Unique identifier of the created post
        success:
          type: boolean
          description: Indicates whether the creation succeeded
        links:
          type: array
          description: Hypermedia navigation links
          items:
            type: object
            properties:
              rel:
                type: string
              href:
                type: string
    UpdatePostRequest:
      type: object
      required:
        - id
      properties:
        id:
          type: string
          description: Convex post identifier
        title:
          type: string
          description: Updated post title
        markdownContent:
          type: string
          description: Updated Markdown document body
        status:
          type: string
          enum:
            - draft
            - published
            - archived
          description: Updated post status
    UpdatePostResponse:
      type: object
      required:
        - success
        - postId
      properties:
        success:
          type: boolean
          description: Indicates whether the update succeeded
        postId:
          type: string
          description: Convex identifier of the updated blog post
        title:
          type: string
          description: Updated post title
        status:
          type: string
          enum:
            - draft
            - published
            - archived
          description: Current post status
        updatedAt:
          type: string
          format: date-time
          description: Timestamp when the post was updated
    DeletePostResponse:
      type: object
      required:
        - success
        - postId
      properties:
        success:
          type: boolean
          description: Indicates whether the post was deleted
          example: true
        postId:
          type: string
          description: Unique identifier of the deleted post
          example: post_abc123
        message:
          type: string
          description: Confirmation message
          example: Post successfully deleted.
    PostListResponse:
      type: object
      required:
        - posts
      properties:
        posts:
          type: array
          items:
            $ref: "#/components/schemas/BlogPostItem"
    BlogListResponse:
      type: object
      required:
        - blogs
      properties:
        blogs:
          type: array
          items:
            $ref: "#/components/schemas/BlogItem"
    BlogItem:
      type: object
      required:
        - _id
        - name
      properties:
        _id:
          type: string
          description: Unique blog identifier
        name:
          type: string
          description: Display name of the blog
        description:
          type: string
          description: Optional description of the blog
    BlogPostItem:
      type: object
      required:
        - _id
        - title
      properties:
        _id:
          type: string
          description: Unique post identifier
        title:
          type: string
          description: Post title
        markdownContent:
          type: string
          description: Rendered Markdown content
        status:
          type: string
          enum:
            - draft
            - published
            - archived
          description: Publication status
        publishedAt:
          type: string
          format: date-time
