{
  "openapi": "3.1.0",
  "info": {
    "title": "Budget by OpenApps",
    "version": "1.0.0",
    "summary": "Free zero-based budgeting: log receipts and read what is left in each category.",
    "description": "Budget is a free zero-based (envelope) budget app, a free alternative to YNAB and Monarch Money. With an API key an assistant can add transactions (log a receipt) and read ready to assign, every category's balance, account balances and recent transactions. Every route except GET / needs a key in the x-budget-key header. Only the budget's owner can create one: in the app, Settings, then Assistant API, then Create key. A key works for its one budget; anyone with it can add transactions and read balances, so keep it private. A 401 means the key is missing, malformed or revoked: ask the person for a new one. Amounts are decimal strings in the budget's currency (\"23.40\"); a minus sign means money out. Dates are YYYY-MM-DD, months YYYY-MM. Moving money between categories, editing the plan, splits, transfers and scheduled transactions happen in the app. Errors are JSON with a plain error sentence that says what to change. More notes for assistants: https://budget-free.openapps.xyz/llms.txt",
    "contact": { "name": "OpenApps", "url": "https://free-apps.openapps.xyz/" }
  },
  "externalDocs": { "description": "Notes for assistants (llms.txt)", "url": "https://budget-free.openapps.xyz/llms.txt" },
  "servers": [{ "url": "https://functions.bounded.sh/apps/e4fe6272d152c03d11432fc6/api" }],
  "security": [{ "budgetKey": [] }],
  "paths": {
    "/": {
      "get": {
        "operationId": "listRoutes",
        "summary": "List the routes",
        "description": "Needs no key.",
        "security": [],
        "responses": {
          "200": {
            "description": "The routes, one line each.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/RouteList" },
                "example": {
                  "name": "Budget by OpenApps",
                  "about": "Free zero-based budgeting. Every route but this one needs an API key from the budget's Settings, sent as the x-budget-key header. Amounts are decimal strings in the budget's currency.",
                  "docs": "https://budget-free.openapps.xyz/llms.txt",
                  "routes": [
                    "GET /budget?month=YYYY-MM -> ready to assign, every category's assigned, activity and available, card payments and account balances",
                    "GET /categories?name=groceries&month=YYYY-MM -> matching categories with what is available",
                    "GET /transactions?since=YYYY-MM-DD&limit=50 -> recent transactions, newest first",
                    "POST /transactions {outflow | inflow, payee, category?, account?, date?, memo?, cleared?} -> the new transaction and its category's balance"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/categories": {
      "get": {
        "operationId": "getCategories",
        "summary": "How much is left in a category",
        "description": "Categories with their assigned, activity and available amounts for a month, and ready to assign. name matches case-insensitively and partly (\"groc\" finds Groceries); leave it out for every category. To answer \"how much is left for groceries\", read available; if overspent is true, say by how much and suggest moving money in the app. Scheduled transactions that came due are entered before the answer.",
        "parameters": [
          { "name": "name", "in": "query", "required": false, "description": "Part of a category name.", "schema": { "type": "string" }, "example": "groceries" },
          { "$ref": "#/components/parameters/month" }
        ],
        "responses": {
          "200": {
            "description": "The matching categories.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/CategoriesAnswer" },
                "example": {
                  "month": "2026-10",
                  "currency": "USD",
                  "readyToAssign": "2470.00",
                  "categories": [
                    { "group": "Everyday", "name": "Groceries", "assigned": "300.00", "activity": "-105.67", "available": "194.33", "availableDisplay": "$194.33", "overspent": false, "target": { "type": "monthly", "amount": "400.00", "stillNeeded": "100.00" } }
                  ]
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadInput" },
          "401": { "$ref": "#/components/responses/BadKey" }
        }
      }
    },
    "/budget": {
      "get": {
        "operationId": "getBudget",
        "summary": "The whole budget for a month",
        "description": "Ready to assign, how many transactions still need a category, every category, what is set aside for each credit card, and every open account's balance. Scheduled transactions that came due are entered before the answer.",
        "parameters": [{ "$ref": "#/components/parameters/month" }],
        "responses": {
          "200": {
            "description": "The budget.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/BudgetAnswer" },
                "example": {
                  "budget": { "name": "Our budget", "currency": "USD", "url": "https://budget-free.openapps.xyz/b/Ab3dE5fG7hJ9kL2mN4pQ" },
                  "month": "2026-10",
                  "readyToAssign": "2470.00",
                  "readyToAssignDisplay": "$2,470.00",
                  "needsCategory": 0,
                  "categories": [
                    { "group": "Bills", "name": "Rent or mortgage", "assigned": "700.00", "activity": "0.00", "available": "700.00", "availableDisplay": "$700.00", "overspent": false },
                    { "group": "Everyday", "name": "Groceries", "assigned": "300.00", "activity": "-105.67", "available": "194.33", "availableDisplay": "$194.33", "overspent": false }
                  ],
                  "creditCardPayments": [{ "card": "Visa", "setAside": "30.00", "balance": "-80.00" }],
                  "accounts": [
                    { "name": "Checking", "type": "checking", "onBudget": true, "balance": "2691.83", "cleared": "1000.00" },
                    { "name": "Visa", "type": "credit", "onBudget": true, "balance": "-80.00", "cleared": "-200.00" }
                  ]
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadInput" },
          "401": { "$ref": "#/components/responses/BadKey" }
        }
      }
    },
    "/transactions": {
      "get": {
        "operationId": "listTransactions",
        "summary": "Recent transactions",
        "description": "Newest first: the newest limit transactions on or after since (up to 500). Deleted transactions are left out.",
        "parameters": [
          { "name": "since", "in": "query", "required": false, "description": "Only transactions on or after this date.", "schema": { "type": "string", "format": "date" }, "example": "2026-10-01" },
          { "name": "limit", "in": "query", "required": false, "description": "How many to return, 1 to 500.", "schema": { "type": "integer", "minimum": 1, "maximum": 500, "default": 50 }, "example": 50 }
        ],
        "responses": {
          "200": {
            "description": "The transactions.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/TransactionList" },
                "example": {
                  "currency": "USD",
                  "transactions": [
                    { "id": "79UP3652ameW", "date": "2026-10-19", "account": "Checking", "payee": "Corner Market", "category": "Groceries", "amount": "-23.40", "memo": "", "cleared": false },
                    { "id": "hJpLr7dtbFNj", "date": "2026-10-18", "account": "Checking", "payee": "", "category": "", "amount": "-150.00", "memo": "", "cleared": false, "transferTo": "Visa" }
                  ]
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadInput" },
          "401": { "$ref": "#/components/responses/BadKey" }
        }
      },
      "post": {
        "operationId": "addTransaction",
        "summary": "Add a transaction (log a receipt)",
        "description": "Send exactly one of outflow (money spent) or inflow (money received, like a paycheck or refund). category is a category name (case-insensitive; a partial name works when it matches one category) or \"income\" for money that goes to ready to assign. Without category, Budget uses the payee's last category; with neither, the transaction is saved as needing a category and the answer has a note. account defaults to the account last used with this payee, then the first checking, savings or cash account. date defaults to today in UTC, so send the person's local date when you know it.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/NewTransaction" },
              "example": { "outflow": "23.40", "payee": "Corner Market", "category": "Groceries", "date": "2026-10-19" }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Saved. Includes the category's balance after it and ready to assign.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Added" },
                "example": {
                  "created": { "id": "79UP3652ameW", "date": "2026-10-19", "account": "Checking", "payee": "Corner Market", "category": "Groceries", "amount": "-23.40", "memo": "", "cleared": false },
                  "category": { "name": "Groceries", "month": "2026-10", "available": "170.93", "availableDisplay": "$170.93", "overspent": false },
                  "readyToAssign": "2470.00"
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadInput" },
          "401": { "$ref": "#/components/responses/BadKey" },
          "409": {
            "description": "The budget has no open account yet.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": "This budget has no open account yet. Add one in the app first." } } }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "budgetKey": {
        "type": "apiKey",
        "in": "header",
        "name": "x-budget-key",
        "description": "A key the budget's owner creates in Settings, then Assistant API. It looks like bk_<20 characters>_<32 characters> and works for that one budget."
      }
    },
    "parameters": {
      "month": { "name": "month", "in": "query", "required": false, "description": "The month to read, YYYY-MM. Defaults to this month.", "schema": { "type": "string", "pattern": "^\\d{4}-(0[1-9]|1[0-2])$" }, "example": "2026-10" }
    },
    "responses": {
      "BadInput": {
        "description": "Bad input. The sentence says what to fix, and lists the budget's categories or accounts when a name did not match.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": "No category called \"Food\". Categories: Rent or mortgage, Groceries, Eating out, or \"income\" for ready to assign." } } }
      },
      "BadKey": {
        "description": "The key is missing, malformed or revoked.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": "That key is not valid. It may have been revoked; the budget's owner can create a new one in Settings." } } }
      }
    },
    "schemas": {
      "Money": { "type": "string", "description": "A decimal amount in the budget's currency, like \"23.40\"; a minus sign means money out or overspent.", "examples": ["194.33", "-23.40"] },
      "Error": { "type": "object", "required": ["error"], "properties": { "error": { "type": "string", "description": "A sentence for a person that says what to change." } } },
      "RouteList": {
        "type": "object",
        "required": ["name", "about", "docs", "routes"],
        "properties": { "name": { "type": "string" }, "about": { "type": "string" }, "docs": { "type": "string", "format": "uri" }, "routes": { "type": "array", "items": { "type": "string" } } }
      },
      "Category": {
        "type": "object",
        "required": ["group", "name", "assigned", "activity", "available", "availableDisplay", "overspent"],
        "properties": {
          "group": { "type": "string" },
          "name": { "type": "string" },
          "assigned": { "$ref": "#/components/schemas/Money", "description": "Money given to the category this month." },
          "activity": { "$ref": "#/components/schemas/Money", "description": "Spending this month (negative) and refunds (positive)." },
          "available": { "$ref": "#/components/schemas/Money", "description": "What is left: last month's leftover + assigned + activity." },
          "availableDisplay": { "type": "string", "description": "available formatted for reading out, like \"$84.20\"." },
          "overspent": { "type": "boolean" },
          "target": {
            "type": "object",
            "description": "Present when the category has a target.",
            "required": ["type", "amount", "stillNeeded"],
            "properties": {
              "type": { "type": "string", "enum": ["monthly", "date", "balance"] },
              "amount": { "$ref": "#/components/schemas/Money" },
              "by": { "type": "string", "description": "For a date target, the month it is due, YYYY-MM." },
              "stillNeeded": { "$ref": "#/components/schemas/Money", "description": "What the target still asks for this month." }
            }
          }
        }
      },
      "CategoriesAnswer": {
        "type": "object",
        "required": ["month", "currency", "readyToAssign", "categories"],
        "properties": {
          "month": { "type": "string" },
          "currency": { "type": "string", "description": "ISO 4217 code, like USD." },
          "readyToAssign": { "$ref": "#/components/schemas/Money", "description": "Money with no job yet. Negative means more was assigned than there is." },
          "categories": { "type": "array", "items": { "$ref": "#/components/schemas/Category" } }
        }
      },
      "BudgetAnswer": {
        "type": "object",
        "required": ["budget", "month", "readyToAssign", "readyToAssignDisplay", "needsCategory", "categories", "creditCardPayments", "accounts"],
        "properties": {
          "budget": {
            "type": "object",
            "required": ["name", "currency", "url"],
            "properties": { "name": { "type": "string" }, "currency": { "type": "string" }, "url": { "type": "string", "format": "uri", "description": "Where the person opens this budget." } }
          },
          "month": { "type": "string" },
          "readyToAssign": { "$ref": "#/components/schemas/Money" },
          "readyToAssignDisplay": { "type": "string" },
          "needsCategory": { "type": "integer", "description": "How many transactions still need a category." },
          "categories": { "type": "array", "items": { "$ref": "#/components/schemas/Category" } },
          "creditCardPayments": {
            "type": "array",
            "items": {
              "type": "object",
              "required": ["card", "setAside", "balance"],
              "properties": {
                "card": { "type": "string" },
                "setAside": { "$ref": "#/components/schemas/Money", "description": "Money in the card's payment category, ready to pay the card." },
                "balance": { "$ref": "#/components/schemas/Money", "description": "The card's balance; negative is owed." }
              }
            }
          },
          "accounts": {
            "type": "array",
            "items": {
              "type": "object",
              "required": ["name", "type", "onBudget", "balance", "cleared"],
              "properties": {
                "name": { "type": "string" },
                "type": { "type": "string", "enum": ["checking", "savings", "cash", "credit", "asset", "liability"] },
                "onBudget": { "type": "boolean", "description": "False for asset and liability accounts, which count toward net worth only." },
                "balance": { "$ref": "#/components/schemas/Money" },
                "cleared": { "$ref": "#/components/schemas/Money" }
              }
            }
          }
        }
      },
      "Transaction": {
        "type": "object",
        "required": ["id", "date", "account", "payee", "category", "amount", "memo", "cleared"],
        "properties": {
          "id": { "type": "string" },
          "date": { "type": "string", "format": "date" },
          "account": { "type": "string" },
          "payee": { "type": "string" },
          "category": { "type": "string", "description": "A category name, \"Ready to assign\", \"Starting balance\", \"Split\", or empty when it needs a category or is a transfer between budget accounts." },
          "amount": { "$ref": "#/components/schemas/Money", "description": "From the account's side: negative is money out." },
          "memo": { "type": "string" },
          "cleared": { "type": "boolean" },
          "transferTo": { "type": "string", "description": "For a transfer, the other account, which sees the opposite amount." }
        }
      },
      "TransactionList": {
        "type": "object",
        "required": ["currency", "transactions"],
        "properties": { "currency": { "type": "string" }, "transactions": { "type": "array", "items": { "$ref": "#/components/schemas/Transaction" } } }
      },
      "NewTransaction": {
        "type": "object",
        "description": "Send exactly one of outflow or inflow.",
        "properties": {
          "outflow": { "type": "string", "description": "Money spent, a positive amount like \"23.40\"." },
          "inflow": { "type": "string", "description": "Money received, a positive amount like \"1500.00\"." },
          "payee": { "type": "string", "maxLength": 100, "description": "Who was paid or who paid. Budget remembers each payee's last category and account." },
          "category": { "type": "string", "description": "A category name, or \"income\" for ready to assign." },
          "account": { "type": "string", "description": "An open account's name." },
          "date": { "type": "string", "format": "date", "description": "Defaults to today in UTC." },
          "memo": { "type": "string", "maxLength": 300 },
          "cleared": { "type": "boolean", "description": "True if it has already shown up at the bank." }
        }
      },
      "Added": {
        "type": "object",
        "required": ["created", "readyToAssign"],
        "properties": {
          "created": { "$ref": "#/components/schemas/Transaction" },
          "category": {
            "type": "object",
            "description": "The category's balance after this transaction, when it has a spending category.",
            "required": ["name", "month", "available", "availableDisplay", "overspent"],
            "properties": {
              "name": { "type": "string" },
              "month": { "type": "string" },
              "available": { "$ref": "#/components/schemas/Money" },
              "availableDisplay": { "type": "string" },
              "overspent": { "type": "boolean" }
            }
          },
          "note": { "type": "string", "description": "Present when the transaction was saved as needing a category." },
          "readyToAssign": { "$ref": "#/components/schemas/Money" }
        }
      }
    }
  }
}
