---
openapi: 3.1.0
info:
  title: nowreading.dev API
  summary: 'Agent-native reading list: read any user''s public list, or curate a user''s
    list on their behalf.'
  description: |
    The nowreading.dev HTTP API lets AI agents and developers:

    - **Read** any user's public reading list as JSON (no auth required).
    - **Write** (create, update, delete, refresh) to a user's list with a per-user bearer token.

    Agents should first call `GET /api/me` to verify the token and discover the handle it represents.
    Every reading list is also published as RSS, Atom, JSON Feed, Markdown, plain text, and llms.txt at `/:handle.*`.

    **Rate limits** are returned on every response via `X-RateLimit-Limit`, `X-RateLimit-Remaining`,
    and `X-RateLimit-Reset` headers. Agents receiving `429 Too Many Requests` should respect the
    `Retry-After` header.
  version: 1.0.0
  contact:
    name: nowreading.dev
    email: v@codenamev.com
    url: https://nowreading.dev
  license:
    name: Proprietary
  x-logo:
    url: https://nowreading.dev/icon.png
servers:
- url: https://nowreading.dev
  description: Production
externalDocs:
  description: Markdown API documentation (llms.txt)
  url: https://nowreading.dev/llms.txt
security:
- bearerAuth: []
tags:
- name: me
  description: Token-holder identity & discovery.
- name: articles
  description: Create, read, update, and delete the authenticated user's articles.
- name: users
  description: Public reads of other users' reading lists (no auth).
paths:
  "/api/me":
    get:
      operationId: getMe
      tags:
      - me
      summary: Verify bearer token and discover the handle it represents.
      description: Returns the handle, reading-list URL, feed URLs, and article count
        for the authenticated user. Call this first after a token handoff.
      security:
      - bearerAuth: []
      responses:
        '200':
          "$ref": "#/components/responses/MeResponse"
        '401':
          "$ref": "#/components/responses/UnauthorizedError"
  "/api/articles":
    get:
      operationId: listArticles
      tags:
      - articles
      summary: List the authenticated user's articles (including drafts).
      security:
      - bearerAuth: []
      parameters:
      - name: page
        in: query
        schema:
          type: integer
          minimum: 1
          default: 1
        description: 1-based page number. Page size is 50.
      responses:
        '200':
          "$ref": "#/components/responses/ArticleListResponse"
        '401':
          "$ref": "#/components/responses/UnauthorizedError"
    post:
      operationId: createArticle
      tags:
      - articles
      summary: Add an article to the authenticated user's reading list.
      description: 'If the URL already exists on the user''s list, returns 200 with
        `{ message: ''Already saved.'' }`. Otherwise returns 201. Metadata extraction
        runs asynchronously; the first response may have partial fields.'
      security:
      - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              "$ref": "#/components/schemas/ArticleCreateRequest"
            examples:
              basic:
                value:
                  url: https://example.com/post
              withNotes:
                value:
                  url: https://example.com/post
                  notes: Revisit before next demo.
      responses:
        '200':
          "$ref": "#/components/responses/ArticleResponse"
        '201':
          "$ref": "#/components/responses/ArticleResponse"
        '401':
          "$ref": "#/components/responses/UnauthorizedError"
        '422':
          "$ref": "#/components/responses/ValidationError"
  "/api/articles/{id}":
    parameters:
    - name: id
      in: path
      required: true
      schema:
        type: integer
        minimum: 1
    patch:
      operationId: updateArticle
      tags:
      - articles
      summary: Update one of the authenticated user's articles.
      security:
      - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              "$ref": "#/components/schemas/ArticleUpdateRequest"
      responses:
        '200':
          "$ref": "#/components/responses/ArticleResponse"
        '401':
          "$ref": "#/components/responses/UnauthorizedError"
        '404':
          "$ref": "#/components/responses/NotFoundError"
        '422':
          "$ref": "#/components/responses/ValidationError"
    delete:
      operationId: deleteArticle
      tags:
      - articles
      summary: Remove an article from the authenticated user's reading list.
      security:
      - bearerAuth: []
      responses:
        '204':
          description: Deleted.
        '401':
          "$ref": "#/components/responses/UnauthorizedError"
        '404':
          "$ref": "#/components/responses/NotFoundError"
  "/api/articles/{id}/refresh_metadata":
    parameters:
    - name: id
      in: path
      required: true
      schema:
        type: integer
        minimum: 1
    post:
      operationId: refreshArticleMetadata
      tags:
      - articles
      summary: Queue a metadata re-extraction for an article.
      description: Returns 202 immediately; the extraction runs asynchronously.
      security:
      - bearerAuth: []
      responses:
        '202':
          "$ref": "#/components/responses/ArticleResponse"
        '401':
          "$ref": "#/components/responses/UnauthorizedError"
        '404':
          "$ref": "#/components/responses/NotFoundError"
  "/api/users/{handle}/articles":
    get:
      operationId: listUserArticles
      tags:
      - users
      summary: List any user's published articles (public; no auth).
      description: Returns the 50 most recent published articles. Equivalent to `/:handle.json`
        (JSON Feed 1.1).
      security: []
      parameters:
      - name: handle
        in: path
        required: true
        schema:
          type: string
          pattern: "^[a-zA-Z0-9_-]+$"
      responses:
        '200':
          "$ref": "#/components/responses/PublicArticleListResponse"
        '404':
          "$ref": "#/components/responses/NotFoundError"
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: Opaque per-user token (has_secure_token)
      description: Per-user bearer token. The human user reveals it at /token and
        gives it to the agent.
  parameters: {}
  schemas:
    Article:
      type: object
      required:
      - id
      - url
      - added_at
      properties:
        id:
          type: integer
          example: 42
        url:
          type: string
          format: uri
          example: https://example.com/post
        title:
          type: string
          nullable: true
          example: Diffusion based LLMs are here
        summary:
          type: string
          nullable: true
        author:
          type: string
          nullable: true
        source:
          type: string
          nullable: true
          description: Canonical site or publisher.
        publication_date:
          type: string
          format: date-time
          nullable: true
          description: When the original source was published.
        content_type:
          type: string
          nullable: true
          example: article
        length:
          type: integer
          nullable: true
          description: Word count if known.
        keywords:
          type: array
          items:
            type: string
          nullable: true
        added_at:
          type: string
          format: date-time
          description: When the article entered the reading list.
        notes:
          type: string
          nullable: true
          description: Owner-only.
        draft:
          type: boolean
          description: Owner-only. Drafts are excluded from public feeds.
        updated_at:
          type: string
          format: date-time
          description: Owner-only.
    ArticleCreateRequest:
      type: object
      required:
      - url
      properties:
        url:
          type: string
          format: uri
        notes:
          type: string
    ArticleUpdateRequest:
      type: object
      properties:
        notes:
          type: string
        draft:
          type: boolean
        title:
          type: string
        summary:
          type: string
    ArticleListResponse:
      type: object
      required:
      - page
      - page_size
      - articles
      properties:
        page:
          type: integer
          minimum: 1
        page_size:
          type: integer
          example: 50
        articles:
          type: array
          items:
            "$ref": "#/components/schemas/Article"
    PublicArticleListResponse:
      type: object
      description: JSON Feed 1.1 formatted reading list.
      properties:
        version:
          type: string
          example: https://jsonfeed.org/version/1.1
        title:
          type: string
        home_page_url:
          type: string
          format: uri
        feed_url:
          type: string
          format: uri
        items:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
              url:
                type: string
                format: uri
              title:
                type: string
              content_text:
                type: string
              date_published:
                type: string
                format: date-time
              tags:
                type: array
                items:
                  type: string
    ArticleCreateOrShowResponse:
      type: object
      properties:
        message:
          type: string
          example: Article added!
        article:
          "$ref": "#/components/schemas/Article"
    MeResponse:
      type: object
      required:
      - handle
      - reading_list_url
      - article_count
      - feeds
      properties:
        handle:
          type: string
          pattern: "^[a-zA-Z0-9_-]+$"
        reading_list_url:
          type: string
          format: uri
        llms_txt_url:
          type: string
          format: uri
        docs_url:
          type: string
          format: uri
        article_count:
          type: integer
        feeds:
          type: object
          properties:
            rss:
              type: string
              format: uri
            atom:
              type: string
              format: uri
            json:
              type: string
              format: uri
            md:
              type: string
              format: uri
            text:
              type: string
              format: uri
    Error:
      type: object
      required:
      - error
      properties:
        error:
          type: string
          description: Human-readable error message.
        code:
          type: string
          description: Stable machine-readable error code.
          example: unauthorized
        resolution:
          type: string
          description: Suggested agent action to resolve the error.
        docs_url:
          type: string
          format: uri
          description: Link to relevant documentation.
    ValidationErrorBody:
      type: object
      required:
      - errors
      properties:
        errors:
          type: array
          items:
            type: string
        code:
          type: string
          example: validation_failed
        resolution:
          type: string
  responses:
    MeResponse:
      description: Token is valid; returns the owning handle and feed URLs.
      headers:
        X-RateLimit-Limit:
          description: Request quota per window.
          schema:
            type: integer
        X-RateLimit-Remaining:
          description: Requests remaining in the current window.
          schema:
            type: integer
        X-RateLimit-Reset:
          description: Epoch seconds when the window resets.
          schema:
            type: integer
      content:
        application/json:
          schema:
            "$ref": "#/components/schemas/MeResponse"
    ArticleListResponse:
      description: List of the authenticated user's articles.
      headers:
        X-RateLimit-Limit:
          description: Request quota per window.
          schema:
            type: integer
        X-RateLimit-Remaining:
          description: Requests remaining in the current window.
          schema:
            type: integer
        X-RateLimit-Reset:
          description: Epoch seconds when the window resets.
          schema:
            type: integer
      content:
        application/json:
          schema:
            "$ref": "#/components/schemas/ArticleListResponse"
    PublicArticleListResponse:
      description: Public JSON Feed for the given handle.
      headers:
        X-RateLimit-Limit:
          description: Request quota per window.
          schema:
            type: integer
        X-RateLimit-Remaining:
          description: Requests remaining in the current window.
          schema:
            type: integer
        X-RateLimit-Reset:
          description: Epoch seconds when the window resets.
          schema:
            type: integer
      content:
        application/json:
          schema:
            "$ref": "#/components/schemas/PublicArticleListResponse"
    ArticleResponse:
      description: A single article with optional message.
      headers:
        X-RateLimit-Limit:
          description: Request quota per window.
          schema:
            type: integer
        X-RateLimit-Remaining:
          description: Requests remaining in the current window.
          schema:
            type: integer
        X-RateLimit-Reset:
          description: Epoch seconds when the window resets.
          schema:
            type: integer
      content:
        application/json:
          schema:
            "$ref": "#/components/schemas/ArticleCreateOrShowResponse"
    UnauthorizedError:
      description: Missing or invalid bearer token.
      headers:
        X-RateLimit-Limit:
          description: Request quota per window.
          schema:
            type: integer
        X-RateLimit-Remaining:
          description: Requests remaining in the current window.
          schema:
            type: integer
        X-RateLimit-Reset:
          description: Epoch seconds when the window resets.
          schema:
            type: integer
      content:
        application/json:
          schema:
            "$ref": "#/components/schemas/Error"
    NotFoundError:
      description: Resource does not exist or is not visible to this token.
      headers:
        X-RateLimit-Limit:
          description: Request quota per window.
          schema:
            type: integer
        X-RateLimit-Remaining:
          description: Requests remaining in the current window.
          schema:
            type: integer
        X-RateLimit-Reset:
          description: Epoch seconds when the window resets.
          schema:
            type: integer
      content:
        application/json:
          schema:
            "$ref": "#/components/schemas/Error"
    ValidationError:
      description: Request failed validation.
      headers:
        X-RateLimit-Limit:
          description: Request quota per window.
          schema:
            type: integer
        X-RateLimit-Remaining:
          description: Requests remaining in the current window.
          schema:
            type: integer
        X-RateLimit-Reset:
          description: Epoch seconds when the window resets.
          schema:
            type: integer
      content:
        application/json:
          schema:
            "$ref": "#/components/schemas/ValidationErrorBody"
  headers:
    X-RateLimit-Limit:
      description: Request quota per window.
      schema:
        type: integer
    X-RateLimit-Remaining:
      description: Requests remaining in the current window.
      schema:
        type: integer
    X-RateLimit-Reset:
      description: Epoch seconds when the window resets.
      schema:
        type: integer
