{
  "openapi": "3.1.0",
  "info": {
    "title": "BudgetBro Public Content API",
    "version": "1.0.0",
    "summary": "Read-only public API for BudgetBro travel-budgeting content and machine-readable facts.",
    "description": "Public, unauthenticated, read-only endpoints that expose BudgetBro's published blog content, curated third-party travel feed, and structured product facts. Intended for AI agents, aggregators, and integrations. No API key is required. All responses are cached at the edge. For programmatic access to a user's own expenses and budgets, use the BudgetBro MCP server (see https://budgetbro.app/.well-known/mcp).",
    "termsOfService": "https://budgetbro.app/terms",
    "contact": {
      "name": "BudgetBro Support",
      "email": "support@budgetbro.app",
      "url": "https://budgetbro.app/developers"
    },
    "license": {
      "name": "Proprietary",
      "url": "https://budgetbro.app/terms"
    }
  },
  "servers": [
    { "url": "https://budgetbro.app", "description": "Production" }
  ],
  "externalDocs": {
    "description": "BudgetBro developer & AI-agent resources",
    "url": "https://budgetbro.app/developers"
  },
  "tags": [
    { "name": "Facts", "description": "Structured product facts for AI grounding." },
    { "name": "Content", "description": "Published BudgetBro blog articles." },
    { "name": "Feed", "description": "Merged and third-party travel content feeds." }
  ],
  "paths": {
    "/api/ai": {
      "get": {
        "operationId": "getAiProfile",
        "summary": "Get structured product facts",
        "description": "Returns a machine-readable JSON profile of BudgetBro: description, company, launch status, pricing, platforms, key features, competitor comparison, app-store links, machine-readable resource URLs, and a full index of published articles (with per-article markdown URLs). Read-only, no user data, no auth. Ideal for grounding an AI assistant's answers about the product.",
        "tags": ["Facts"],
        "responses": {
          "200": {
            "description": "The structured product profile.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/AiProfile" }
              }
            }
          },
          "500": {
            "description": "Server error generating the profile.",
            "content": {
              "application/json": { "schema": { "$ref": "#/components/schemas/Error" } }
            }
          }
        }
      }
    },
    "/api/blog/posts": {
      "get": {
        "operationId": "listBlogPosts",
        "summary": "List published blog posts",
        "description": "Paginated list of BudgetBro's own published articles (destination guides, travel news, budgeting tips). Supports category and tag filters and an optional recency window. Press posts are excluded. Returns full post documents.",
        "tags": ["Content"],
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "description": "1-based page number.",
            "required": false,
            "schema": { "type": "integer", "minimum": 1, "default": 1 }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Posts per page.",
            "required": false,
            "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 12 }
          },
          {
            "name": "category",
            "in": "query",
            "description": "Filter by category slug (e.g. 'guides', 'news', 'blogs').",
            "required": false,
            "schema": { "type": "string" }
          },
          {
            "name": "tag",
            "in": "query",
            "description": "Filter by tag slug.",
            "required": false,
            "schema": { "type": "string" }
          },
          {
            "name": "recent",
            "in": "query",
            "description": "When true, only return posts published, authored, or content-updated within the recency window (default 30 days).",
            "required": false,
            "schema": { "type": "string", "enum": ["true", "1"] }
          },
          {
            "name": "recentDays",
            "in": "query",
            "description": "Override the recency window, in days. Implies recent=true.",
            "required": false,
            "schema": { "type": "integer", "minimum": 1 }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of published posts.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["success", "data", "pagination"],
                  "properties": {
                    "success": { "type": "boolean", "const": true },
                    "data": { "type": "array", "items": { "$ref": "#/components/schemas/BlogPost" } },
                    "pagination": { "$ref": "#/components/schemas/Pagination" }
                  }
                }
              }
            }
          },
          "500": {
            "description": "Server error.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          }
        }
      }
    },
    "/api/blog/feed": {
      "get": {
        "operationId": "getMergedFeed",
        "summary": "Get the merged recent article feed",
        "description": "Newest-first stream of BudgetBro's own posts interleaved with curated third-party 'around the web' travel link-outs. Each item carries a 'type' discriminator: 'post' (BudgetBro content, open via slug) or 'external' (excerpt-only link-out, open via url).",
        "tags": ["Feed"],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "description": "Maximum items to return.",
            "required": false,
            "schema": { "type": "integer", "minimum": 1, "maximum": 50, "default": 20 }
          },
          {
            "name": "recentDays",
            "in": "query",
            "description": "Recency window (days) for BudgetBro's own posts.",
            "required": false,
            "schema": { "type": "integer", "minimum": 1, "default": 30 }
          }
        ],
        "responses": {
          "200": {
            "description": "The merged feed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["success", "data"],
                  "properties": {
                    "success": { "type": "boolean", "const": true },
                    "data": { "type": "array", "items": { "$ref": "#/components/schemas/FeedCard" } }
                  }
                }
              }
            }
          },
          "500": {
            "description": "Server error.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          }
        }
      }
    },
    "/api/blog/around-the-web": {
      "get": {
        "operationId": "listAroundTheWeb",
        "summary": "List curated third-party travel articles",
        "description": "Curated third-party travel articles (title, short excerpt, thumbnail, source, and outbound link) drawn from an RSS aggregation. These are link-outs, never BudgetBro's own content.",
        "tags": ["Feed"],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "description": "Maximum items to return.",
            "required": false,
            "schema": { "type": "integer", "minimum": 1, "maximum": 50, "default": 20 }
          }
        ],
        "responses": {
          "200": {
            "description": "Curated external items.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["success", "data"],
                  "properties": {
                    "success": { "type": "boolean", "const": true },
                    "data": { "type": "array", "items": { "$ref": "#/components/schemas/ExternalFeedItem" } }
                  }
                }
              }
            }
          },
          "500": {
            "description": "Server error.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          }
        }
      }
    },
    "/blog/{slug}.md": {
      "get": {
        "operationId": "getBlogPostMarkdown",
        "summary": "Get a blog article as Markdown",
        "description": "Returns a single published article rendered as plain Markdown, for AI grounding and retrieval. The same content is available at /blog/{slug} as HTML. Requesting /blog/{slug} with 'Accept: text/markdown' also returns Markdown (content negotiation).",
        "tags": ["Content"],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "description": "The article slug (without the .md suffix in {slug}).",
            "required": true,
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": {
            "description": "The article as Markdown.",
            "content": {
              "text/markdown": { "schema": { "type": "string" } },
              "text/plain": { "schema": { "type": "string" } }
            }
          },
          "404": {
            "description": "No published article with that slug.",
            "content": { "text/plain": { "schema": { "type": "string" } } }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Error": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "success": { "type": "boolean" },
          "error": { "type": "string", "description": "Human-readable error message." }
        }
      },
      "Pagination": {
        "type": "object",
        "description": "Cursor-free page metadata.",
        "required": ["page", "totalPages", "totalDocs", "hasNextPage", "hasPrevPage"],
        "properties": {
          "page": { "type": "integer" },
          "totalPages": { "type": "integer" },
          "totalDocs": { "type": "integer" },
          "hasNextPage": { "type": "boolean" },
          "hasPrevPage": { "type": "boolean" }
        }
      },
      "BlogPost": {
        "type": "object",
        "description": "A published BudgetBro article. Extra fields beyond those listed may be present.",
        "required": ["id", "title", "slug"],
        "properties": {
          "id": { "type": "string", "description": "Unique post id." },
          "title": { "type": "string" },
          "slug": { "type": "string", "description": "URL slug. Article lives at /blog/{slug}." },
          "excerpt": { "type": "string" },
          "postType": {
            "type": "string",
            "description": "Editorial type.",
            "enum": ["article", "news", "guide", "event"]
          },
          "publishedDate": { "type": "string", "format": "date-time" },
          "createdAt": { "type": "string", "format": "date-time" },
          "contentUpdatedDate": { "type": ["string", "null"], "format": "date-time" },
          "category": {
            "description": "Primary category (populated object) or its id.",
            "oneOf": [
              { "type": "string" },
              { "type": "object", "properties": { "name": { "type": "string" }, "slug": { "type": "string" } } }
            ]
          },
          "seo": {
            "type": "object",
            "properties": {
              "metaTitle": { "type": "string" },
              "metaDescription": { "type": "string" }
            }
          }
        }
      },
      "FeedCard": {
        "oneOf": [
          { "$ref": "#/components/schemas/PostFeedCard" },
          { "$ref": "#/components/schemas/ExternalFeedItem" }
        ],
        "discriminator": {
          "propertyName": "type",
          "mapping": {
            "post": "#/components/schemas/PostFeedCard",
            "external": "#/components/schemas/ExternalFeedItem"
          }
        }
      },
      "PostFeedCard": {
        "type": "object",
        "description": "A BudgetBro-owned article card. Open via /blog/{slug}.",
        "required": ["type", "title", "excerpt", "publishedAt", "slug"],
        "properties": {
          "type": { "type": "string", "const": "post" },
          "title": { "type": "string" },
          "excerpt": { "type": "string" },
          "imageUrl": { "type": ["string", "null"], "format": "uri" },
          "publishedAt": { "type": "string", "format": "date-time" },
          "slug": { "type": "string" },
          "category": { "type": ["string", "null"] }
        }
      },
      "ExternalFeedItem": {
        "type": "object",
        "description": "A curated third-party article. Excerpt-only link-out; open via url.",
        "required": ["title", "url"],
        "properties": {
          "type": { "type": "string", "const": "external" },
          "title": { "type": "string" },
          "excerpt": { "type": "string" },
          "imageUrl": { "type": ["string", "null"], "format": "uri" },
          "publishedAt": { "type": "string", "format": "date-time" },
          "url": { "type": "string", "format": "uri", "description": "The original article on the source site." },
          "sourceName": { "type": "string" },
          "sourceHomepage": { "type": ["string", "null"], "format": "uri" }
        }
      },
      "AiProfile": {
        "type": "object",
        "description": "Structured product facts for AI grounding.",
        "required": ["name", "description", "url"],
        "properties": {
          "name": { "type": "string" },
          "description": { "type": "string" },
          "url": { "type": "string", "format": "uri" },
          "company": { "type": "string" },
          "status": { "type": "string" },
          "support": { "type": "string" },
          "machine_readable": {
            "type": "object",
            "properties": {
              "profile": { "type": "string", "format": "uri" },
              "full_knowledge_base": { "type": "string", "format": "uri" },
              "article_markdown_pattern": { "type": "string" },
              "sitemap": { "type": "string", "format": "uri" },
              "rss": { "type": "string", "format": "uri" }
            }
          },
          "pricing": { "type": "object" },
          "platforms": { "type": "array", "items": { "type": "string" } },
          "key_features": { "type": "array", "items": { "type": "string" } },
          "comparison_vs_travelspend": { "type": "object" },
          "app_stores": {
            "type": "object",
            "properties": {
              "ios": { "type": "string", "format": "uri" },
              "android": { "type": "string", "format": "uri" }
            }
          },
          "articles": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "title": { "type": "string" },
                "type": { "type": "string" },
                "description": { "type": "string" },
                "url": { "type": "string", "format": "uri" },
                "markdown_url": { "type": "string", "format": "uri" },
                "published": { "type": "string", "format": "date-time" },
                "updated": { "type": ["string", "null"], "format": "date-time" },
                "category": { "type": ["string", "null"] }
              }
            }
          }
        }
      }
    }
  }
}
