{
  "openapi": "3.0.3",
  "info": {
    "title": "Dexi Public API",
    "version": "1.1.0",
    "description": "Dexi's public REST surface: save a bookmark with a personal access token, and read published feeds without authentication. The richer per-user interface (search, notes CRUD, spaced repetition) is the MCP server at https://mcp.dexi.net/mcp (OAuth 2.1) — see https://docs.dexi.net.\n\n## Errors\nAll errors are JSON with a top-level `detail` — either a plain message string or a structured object with a machine-readable `code`, a human-readable `message`, and context fields (see the `Error` schema). Validation failures use FastAPI's standard 422 shape (`HTTPValidationError`).\n\n## Rate limits\nRate-limited endpoints return draft-ietf-httpapi-ratelimit-headers response headers (`RateLimit-Limit`, `RateLimit-Remaining`, `RateLimit-Reset`, `RateLimit-Policy`) on every response, and a 429 with `Retry-After` when exceeded — self-throttle from these rather than retrying blindly.\n\n## Versioning and deprecation\nThe API is versioned in the URL path (`/api/v1/…`, `/api/v2/…`). Endpoints documented here are stable within their major version: breaking changes ship as a new path version, and a retiring endpoint announces it with `Deprecation` and `Sunset` (RFC 8594) response headers at least 6 months before removal, alongside a notice at https://docs.dexi.net/api/versioning. Undocumented `/api/*` endpoints are internal and carry no stability promise.",
    "contact": {
      "name": "Dexi",
      "url": "https://dexi.net",
      "email": "team@dexi.net"
    },
    "termsOfService": "https://dexi.net/terms",
    "license": {
      "name": "Proprietary — see terms",
      "url": "https://dexi.net/terms"
    }
  },
  "externalDocs": {
    "description": "Developer documentation",
    "url": "https://docs.dexi.net"
  },
  "servers": [
    {
      "url": "https://api.dexi.net"
    }
  ],
  "paths": {
    "/api/v1/bookmarks/save": {
      "post": {
        "operationId": "saveBookmark",
        "summary": "Save a URL as a bookmark (creates or appends to its note)",
        "description": "Saving a new URL creates a bookmark and its note. Saving a URL already in the workspace reuses the existing note and appends any selection to it as a quoted highlight. Authenticate with a dxi_ personal access token (Settings → API tokens in the app).",
        "security": [
          {
            "personalAccessToken": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "url"
                ],
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "description": "http(s) URL to save"
                  },
                  "title": {
                    "type": "string",
                    "nullable": true
                  },
                  "selection": {
                    "type": "string",
                    "nullable": true,
                    "description": "Highlighted text to store in the note body"
                  },
                  "comment": {
                    "type": "string",
                    "nullable": true,
                    "description": "Optional comment appended after the quoted highlight"
                  },
                  "description": {
                    "type": "string",
                    "nullable": true,
                    "maxLength": 500,
                    "description": "The page's meta description, if the caller has it. On a first save it seeds the note body above the source line; longer values are truncated to 500 characters. Ignored when the URL is already saved. When omitted, Dexi scrapes it from the page in the background."
                  },
                  "append": {
                    "type": "boolean",
                    "default": false,
                    "description": "Explicitly request append behavior when the URL is already saved"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Bookmark created or note appended",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BookmarkSaveResult"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid token — mint a dxi_ token in the app under Settings → API tokens and send it as `Authorization: Bearer dxi_…`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "Free-plan note limit reached — upgrade at https://app.dexi.net/account/upgrade or delete notes to free space",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NoteLimitError"
                }
              }
            }
          },
          "422": {
            "description": "Validation error (e.g. url is not http/https)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api/v2/publisher/feeds/{feed_id}/public": {
      "get": {
        "operationId": "getPublishedFeed",
        "summary": "Published feed as JSON (no authentication)",
        "parameters": [
          {
            "name": "feed_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Feed metadata and entries",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublishedFeed"
                }
              }
            }
          },
          "404": {
            "description": "Unknown or unpublished feed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "feed_id is not a UUID",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "security": []
      }
    },
    "/api/v2/publisher/feeds/{feed_id}/rss": {
      "get": {
        "operationId": "getPublishedFeedRss",
        "summary": "Published feed as RSS 2.0 (no authentication)",
        "parameters": [
          {
            "name": "feed_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "RSS 2.0 XML",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/rss+xml": {
                "schema": {
                  "type": "string",
                  "description": "RSS 2.0 document"
                }
              }
            }
          },
          "404": {
            "description": "Unknown or unpublished feed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "feed_id is not a UUID",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        },
        "security": []
      }
    }
  },
  "components": {
    "securitySchemes": {
      "personalAccessToken": {
        "type": "http",
        "scheme": "bearer",
        "description": "dxi_-prefixed personal access token, minted in the Dexi app under Settings → API tokens. Scoped to bookmark saving only."
      }
    },
    "headers": {
      "RateLimit-Limit": {
        "description": "Requests allowed in the current window (draft-ietf-httpapi-ratelimit-headers)",
        "schema": {
          "type": "integer"
        }
      },
      "RateLimit-Remaining": {
        "description": "Requests remaining in the current window",
        "schema": {
          "type": "integer"
        }
      },
      "RateLimit-Reset": {
        "description": "Seconds until the window resets",
        "schema": {
          "type": "integer"
        }
      },
      "RateLimit-Policy": {
        "description": "The quota policy the numbers refer to, e.g. \"120;w=60\" (120 requests per 60-second window)",
        "schema": {
          "type": "string"
        }
      },
      "Retry-After": {
        "description": "Seconds to wait before retrying (RFC 9110)",
        "schema": {
          "type": "integer"
        }
      }
    },
    "responses": {
      "RateLimited": {
        "description": "Rate limit exceeded — wait Retry-After seconds, then retry",
        "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"
          },
          "RateLimit-Policy": {
            "$ref": "#/components/headers/RateLimit-Policy"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/RateLimitError"
            },
            "example": {
              "detail": {
                "code": "rate_limited",
                "message": "Too many requests — slow down and try again shortly.",
                "retry_after": 12
              }
            }
          }
        }
      }
    },
    "schemas": {
      "ErrorDetail": {
        "type": "object",
        "description": "Structured error: `code` is machine-readable and stable; `message` is human-readable; extra fields add context (see NoteLimitError). Known codes: not_found, rate_limited, note_limit.",
        "required": [
          "code",
          "message"
        ],
        "properties": {
          "code": {
            "type": "string",
            "description": "Stable machine-readable error code",
            "example": "not_found"
          },
          "message": {
            "type": "string",
            "description": "Human-readable explanation"
          },
          "hint": {
            "type": "string",
            "description": "Where to look or what to do next"
          }
        },
        "additionalProperties": true
      },
      "Error": {
        "type": "object",
        "description": "Standard error envelope: every non-2xx JSON response carries `detail` — a structured object on machine-actionable errors, a plain string on simple ones.",
        "required": [
          "detail"
        ],
        "properties": {
          "detail": {
            "oneOf": [
              {
                "type": "string",
                "description": "Plain error message"
              },
              {
                "$ref": "#/components/schemas/ErrorDetail"
              }
            ]
          }
        }
      },
      "RateLimitError": {
        "type": "object",
        "required": [
          "detail"
        ],
        "properties": {
          "detail": {
            "type": "object",
            "required": [
              "code",
              "message",
              "retry_after"
            ],
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "rate_limited"
                ]
              },
              "message": {
                "type": "string"
              },
              "retry_after": {
                "type": "integer",
                "description": "Seconds until a slot frees (mirrors the Retry-After header)"
              }
            }
          }
        }
      },
      "NoteLimitError": {
        "type": "object",
        "required": [
          "detail"
        ],
        "properties": {
          "detail": {
            "type": "object",
            "required": [
              "code",
              "message",
              "used",
              "limit"
            ],
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "note_limit"
                ]
              },
              "message": {
                "type": "string"
              },
              "used": {
                "type": "integer",
                "description": "Notes currently used"
              },
              "limit": {
                "type": "integer",
                "description": "The account's note cap"
              }
            }
          }
        }
      },
      "HTTPValidationError": {
        "type": "object",
        "description": "FastAPI's standard request-validation error shape",
        "properties": {
          "detail": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "loc",
                "msg",
                "type"
              ],
              "properties": {
                "loc": {
                  "type": "array",
                  "items": {
                    "oneOf": [
                      {
                        "type": "string"
                      },
                      {
                        "type": "integer"
                      }
                    ]
                  },
                  "description": "Path to the invalid field"
                },
                "msg": {
                  "type": "string"
                },
                "type": {
                  "type": "string",
                  "description": "Validation error type identifier"
                }
              }
            }
          }
        }
      },
      "Bookmark": {
        "type": "object",
        "required": [
          "id",
          "url",
          "created",
          "updated"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "note_id": {
            "type": "string",
            "format": "uuid",
            "nullable": true,
            "description": "The note that backs this bookmark"
          },
          "title": {
            "type": "string",
            "nullable": true
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "icon": {
            "type": "string",
            "nullable": true,
            "description": "Site favicon path, served relative to https://api.dexi.net"
          },
          "created": {
            "type": "string",
            "format": "date-time"
          },
          "updated": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "BookmarkSaveResult": {
        "type": "object",
        "required": [
          "bookmark",
          "note_id",
          "is_new_bookmark",
          "selection_appended"
        ],
        "properties": {
          "bookmark": {
            "$ref": "#/components/schemas/Bookmark"
          },
          "note_id": {
            "type": "string",
            "format": "uuid",
            "description": "The note created for (or already backing) this URL"
          },
          "is_new_bookmark": {
            "type": "boolean",
            "description": "false when the URL was already saved and was reused"
          },
          "selection_appended": {
            "type": "boolean",
            "description": "true when the selection was appended to an existing note as a quoted highlight"
          }
        }
      },
      "FeedEntry": {
        "type": "object",
        "required": [
          "id",
          "title",
          "text"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "title": {
            "type": "string"
          },
          "text": {
            "type": "string",
            "description": "Entry body as plain text"
          },
          "updated": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        }
      },
      "PublishedFeed": {
        "type": "object",
        "required": [
          "id",
          "title",
          "rss_url",
          "entries"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "title": {
            "type": "string"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "author": {
            "type": "string",
            "description": "Publisher's display name; may be empty"
          },
          "rss_url": {
            "type": "string",
            "format": "uri",
            "description": "RSS 2.0 rendering of the same feed"
          },
          "entries": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FeedEntry"
            }
          }
        }
      }
    }
  }
}