{
  "openapi": "3.0.3",
  "info": {
    "title": "Nine API & MCP Platform",
    "description": "Programmatic API and Model Context Protocol specification for Nine (ninehoang.com). Powers AI agents and integrations to manage workspace expenses, track group fund budgets, record transactions, and query member rosters.",
    "version": "1.0.0"
  },
  "servers": [
    {
      "url": "https://ninehoang.com/api/v1",
      "description": "Nine REST wrapper -- authenticate with your nh_ws_... token"
    }
  ],
  "components": {
    "securitySchemes": {
      "workspaceToken": {
        "type": "http",
        "scheme": "bearer",
        "description": "The workspace token minted in Workspace Settings > API (nh_ws_...). Send it on every request as `Authorization: Bearer nh_ws_...`. It does not expire; revoke it from the same screen and access stops on the next statement."
      }
    }
  },
  "security": [
    {
      "workspaceToken": []
    }
  ],
  "paths": {
    "/token": {
      "post": {
        "summary": "Exchange a workspace token for a short-lived JWT",
        "description": "Trades a long-lived nh_ws_... workspace token for a 1-hour JWT scoped to that workspace. Call this first; send the returned access_token as the bearer credential on every other operation. The token may be presented in the Authorization header, in an x-api-token header, or in the request body -- all three are accepted.",
        "operationId": "exchangeToken",
        "requestBody": {
          "required": false,
          "description": "Optional. Use this when the client cannot set an Authorization header.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "token": {
                    "type": "string",
                    "description": "The workspace token, e.g. nh_ws_1f3c..."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A short-lived JWT scoped to the token's workspace",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "access_token": {
                      "type": "string",
                      "description": "Send as `Authorization: Bearer <access_token>`"
                    },
                    "token_type": {
                      "type": "string",
                      "example": "Bearer"
                    },
                    "expires_in": {
                      "type": "integer",
                      "example": 3600,
                      "description": "Seconds until the JWT expires"
                    },
                    "scope": {
                      "type": "string",
                      "enum": [
                        "read",
                        "write"
                      ],
                      "description": "What the workspace token was minted for"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Token missing, malformed, revoked, or expired"
          },
          "403": {
            "description": "The workspace API switch is off"
          },
          "429": {
            "description": "Rate limited. Honour the Retry-After header before trying again."
          }
        }
      }
    },
    "/personal_transactions": {
      "get": {
        "summary": "List personal expenses",
        "description": "Retrieve recent personal expenses, outlays, and category records.",
        "operationId": "listExpenses",
        "parameters": [
          {
            "name": "select",
            "in": "query",
            "schema": {
              "type": "string",
              "default": "*"
            },
            "description": "Columns to select"
          },
          {
            "name": "order",
            "in": "query",
            "schema": {
              "type": "string",
              "default": "occurred_at.desc"
            },
            "description": "Order clause (e.g. occurred_at.desc)"
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 20
            },
            "description": "Maximum records to return"
          }
        ],
        "responses": {
          "200": {
            "description": "Array of expense records"
          }
        }
      },
      "post": {
        "summary": "Record a new personal expense",
        "description": "Create a new personal expense entry.",
        "operationId": "createExpense",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name",
                  "amount",
                  "category_id",
                  "method_id"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Expense title / vendor description"
                  },
                  "amount": {
                    "type": "number",
                    "description": "Expense amount (positive number in workspace currency)"
                  },
                  "category_id": {
                    "type": "string",
                    "enum": [
                      "beauty-care",
                      "clothing",
                      "credit-repayment",
                      "entertainment",
                      "essentials",
                      "family-support",
                      "fixed-expenses",
                      "food-drinks",
                      "installment-payment",
                      "investments",
                      "local-transport",
                      "other",
                      "sports",
                      "travel",
                      "work-expenses"
                    ],
                    "description": "Expense category slug, from transaction_categories (rows offered to Expenses)"
                  },
                  "method_id": {
                    "type": "string",
                    "enum": [
                      "bank-transfer",
                      "cash",
                      "credit-card",
                      "installment",
                      "mobile-wallet"
                    ],
                    "description": "Payment method used"
                  },
                  "occurred_at": {
                    "type": "string",
                    "format": "date-time",
                    "description": "ISO timestamp when the expense occurred"
                  },
                  "note": {
                    "type": "string",
                    "description": "Optional contextual notes"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Expense successfully recorded"
          }
        }
      }
    },
    "/funds": {
      "get": {
        "summary": "List collective funds and budgets",
        "description": "Retrieve group funds — name, kind, status, dates and contribution schedule. A balance is computed by summing fund_transactions.amount; there is no stored total.",
        "operationId": "listFunds",
        "parameters": [
          {
            "name": "select",
            "in": "query",
            "schema": {
              "type": "string",
              "default": "*,fund_transactions(*)"
            },
            "description": "Columns and relations to select"
          }
        ],
        "responses": {
          "200": {
            "description": "Array of fund objects"
          }
        }
      }
    },
    "/fund_transactions": {
      "get": {
        "summary": "List group fund transactions",
        "description": "Retrieve transactions recorded against collective funds.",
        "operationId": "listFundTransactions",
        "parameters": [
          {
            "name": "fund_id",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Filter by fund UUID (e.g. eq.<uuid>)"
          },
          {
            "name": "order",
            "in": "query",
            "schema": {
              "type": "string",
              "default": "occurred_at.desc"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Array of transactions"
          }
        }
      },
      "post": {
        "summary": "Record a transaction in a group fund",
        "operationId": "createFundTransaction",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "fund_id",
                  "name",
                  "amount",
                  "category_id",
                  "method_id"
                ],
                "properties": {
                  "fund_id": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "name": {
                    "type": "string"
                  },
                  "amount": {
                    "type": "number",
                    "description": "Negative for expense, positive for income"
                  },
                  "category_id": {
                    "type": "string",
                    "enum": [
                      "clothing",
                      "currency-exchange",
                      "entertainment",
                      "fine",
                      "flight-tickets",
                      "food-drinks",
                      "fund-income",
                      "hotel",
                      "local-transport",
                      "other",
                      "refund"
                    ],
                    "description": "Fund category slug, from transaction_categories (rows offered to Funds)"
                  },
                  "method_id": {
                    "type": "string",
                    "enum": [
                      "bank-transfer",
                      "cash",
                      "credit-card",
                      "installment",
                      "mobile-wallet"
                    ]
                  },
                  "occurred_at": {
                    "type": "string",
                    "format": "date-time"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Transaction created"
          }
        }
      }
    },
    "/contacts": {
      "get": {
        "summary": "List members and contacts roster",
        "operationId": "listContacts",
        "responses": {
          "200": {
            "description": "Array of contacts"
          }
        }
      }
    }
  }
}