{
  "openapi": "3.1.0",
  "info": {
    "title": "ValueHub public API",
    "version": "1.1.0",
    "summary": "JSON rate feeds and calculators behind ValueHub's NZ personal finance tools.",
    "description": "Public, unauthenticated, read-only. Two families: GET rate feeds under /api/rates/ that serve ValueHub's stored rate tables, and POST calculators under /calculators/api/ that compute from inputs. All amounts are NZD. Calculators take rates and prices as inputs, not live lookups \u2014 supply current values, or read them from the rate feeds. Outputs are estimates, not financial advice.",
    "contact": {
      "name": "ValueHub",
      "url": "https://valuehub.co.nz/about/"
    },
    "license": {
      "name": "Free to read, quote and cite with attribution",
      "identifier": "valuehub-attribution",
      "url": "https://valuehub.co.nz/editorial-standards/"
    }
  },
  "servers": [
    {
      "url": "https://valuehub.co.nz"
    }
  ],
  "tags": [
    {
      "name": "rates",
      "description": "Stored rate tables, refreshed and cross-checked daily. GET only, takes no body. One row per institution and term for term deposits (the institution's STANDARD product from that row's minimum deposit \u2014 balance tiers and variant rates are not listed), one row per lender, product and term for mortgages (eligibility and conditions are not modelled, so a rate is not a quote), and one row per bond for bonds. Free to read, quote and cite \u2014 please attribute ValueHub (valuehub.co.nz) and link to the matching page. Ask us first before republishing a table in bulk or commercially."
    },
    {
      "name": "calculators",
      "description": "NZ personal finance calculators."
    }
  ],
  "components": {
    "schemas": {
      "Error": {
        "type": "object",
        "description": "Every error response has this shape. `code` is always present and is the field to branch on; `error` is a human-readable message and is not stable enough to match against.",
        "properties": {
          "error": {
            "type": "string",
            "description": "Human-readable message."
          },
          "code": {
            "type": "string",
            "description": "Machine-readable. One of: missing_required_fields, invalid_request, not_found, method_not_allowed, rate_limited, server_error, service_unavailable."
          },
          "missing": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Present only on `missing_required_fields`: the required fields that were omitted."
          },
          "hint": {
            "type": "string",
            "description": "How to resolve it."
          }
        },
        "required": [
          "error",
          "code"
        ]
      }
    }
  },
  "paths": {
    "/calculators/api/mortgage": {
      "post": {
        "summary": "Mortgage repayment",
        "description": "Monthly repayment, total interest and total repayment for an NZ home loan, plus a year-by-year amortisation schedule.",
        "operationId": "mortgage",
        "tags": [
          "calculators"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "loan_amount": {
                    "type": "number",
                    "description": "Loan principal in NZD."
                  },
                  "interest_rate": {
                    "type": "number",
                    "description": "Annual interest rate as a percentage, e.g. 6.5."
                  },
                  "loan_term": {
                    "type": "integer",
                    "description": "Loan term in years. Clamped to 1-50."
                  }
                },
                "additionalProperties": false,
                "required": [
                  "loan_amount",
                  "interest_rate",
                  "loan_term"
                ],
                "examples": [
                  {
                    "loan_amount": 600000,
                    "interest_rate": 6.5,
                    "loan_term": 30
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Calculated result. Top-level keys: monthly_payment, total_repayment, total_interest, schedule[]. An optional `warnings` array may also be present: it names any input that was supplied but **not used**, so a figure can be wrong in a way that looks right. It is absent when there is nothing to report.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "warnings": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Inputs supplied but ignored. Present only when non-empty \u2014 do not treat its absence as meaning every input was used."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Rejected request. `code` is `missing_required_fields` when a declared required field was omitted (see `missing`), otherwise `invalid_request` for a malformed body or a non-numeric or non-finite value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "405": {
            "description": "This endpoint is POST-only.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited (60 requests per minute, 500 per day, per IP).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/calculators/api/term-deposit": {
      "post": {
        "summary": "Term deposit maturity",
        "description": "Interest earned and maturity value for an NZ term deposit paying interest at maturity.",
        "operationId": "term_deposit",
        "tags": [
          "calculators"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "amount": {
                    "type": "number",
                    "description": "Deposit amount in NZD."
                  },
                  "rate": {
                    "type": "number",
                    "description": "Annual interest rate as a percentage, e.g. 4.5."
                  },
                  "term_months": {
                    "type": "integer",
                    "description": "Term in months. Clamped to 1-600."
                  }
                },
                "additionalProperties": false,
                "required": [
                  "amount",
                  "rate",
                  "term_months"
                ],
                "examples": [
                  {
                    "amount": 10000,
                    "rate": 4.5,
                    "term_months": 12
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Calculated result. Top-level keys: deposit_amount, interest_rate, term_months, interest_earned, maturity_value. An optional `warnings` array may also be present: it names any input that was supplied but **not used**, so a figure can be wrong in a way that looks right. It is absent when there is nothing to report.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "warnings": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Inputs supplied but ignored. Present only when non-empty \u2014 do not treat its absence as meaning every input was used."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Rejected request. `code` is `missing_required_fields` when a declared required field was omitted (see `missing`), otherwise `invalid_request` for a malformed body or a non-numeric or non-finite value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "405": {
            "description": "This endpoint is POST-only.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited (60 requests per minute, 500 per day, per IP).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/calculators/api/compound-interest": {
      "post": {
        "summary": "Compound interest projection",
        "description": "Growth of savings or an investment with optional monthly contributions, with a year-by-year breakdown.",
        "operationId": "compound_interest",
        "tags": [
          "calculators"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "principal": {
                    "type": "number",
                    "description": "Starting amount in NZD."
                  },
                  "rate": {
                    "type": "number",
                    "description": "Annual return as a percentage, e.g. 5."
                  },
                  "years": {
                    "type": "integer",
                    "description": "Investment period in years. Clamped to 1-100."
                  },
                  "monthly_contribution": {
                    "type": "number",
                    "description": "Amount added each month in NZD. Defaults to 0."
                  },
                  "compounding": {
                    "type": "integer",
                    "description": "Compounding periods per year. Defaults to 12 (monthly)."
                  }
                },
                "additionalProperties": false,
                "required": [
                  "principal",
                  "rate",
                  "years"
                ],
                "examples": [
                  {
                    "principal": 10000,
                    "rate": 5,
                    "years": 10,
                    "monthly_contribution": 200
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Calculated result. Top-level keys: final_total, final_interest, results[] (year, total, contributions, interest_earned). An optional `warnings` array may also be present: it names any input that was supplied but **not used**, so a figure can be wrong in a way that looks right. It is absent when there is nothing to report.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "warnings": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Inputs supplied but ignored. Present only when non-empty \u2014 do not treat its absence as meaning every input was used."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Rejected request. `code` is `missing_required_fields` when a declared required field was omitted (see `missing`), otherwise `invalid_request` for a malformed body or a non-numeric or non-finite value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "405": {
            "description": "This endpoint is POST-only.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited (60 requests per minute, 500 per day, per IP).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/calculators/api/savings-goal": {
      "post": {
        "summary": "Savings goal time",
        "description": "How many months it takes to reach a savings target.",
        "operationId": "savings_goal",
        "tags": [
          "calculators"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "target": {
                    "type": "number",
                    "description": "Savings target in NZD."
                  },
                  "current": {
                    "type": "number",
                    "description": "Amount already saved in NZD."
                  },
                  "monthly": {
                    "type": "number",
                    "description": "Amount added each month in NZD."
                  },
                  "rate": {
                    "type": "number",
                    "description": "Annual interest rate as a percentage. Defaults to 0."
                  }
                },
                "additionalProperties": false,
                "required": [
                  "target",
                  "monthly"
                ],
                "examples": [
                  {
                    "target": 30000,
                    "current": 5000,
                    "monthly": 600,
                    "rate": 3.5
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Calculated result. Top-level keys: months, total_saved, monthly_contribution. An optional `warnings` array may also be present: it names any input that was supplied but **not used**, so a figure can be wrong in a way that looks right. It is absent when there is nothing to report.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "warnings": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Inputs supplied but ignored. Present only when non-empty \u2014 do not treat its absence as meaning every input was used."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Rejected request. `code` is `missing_required_fields` when a declared required field was omitted (see `missing`), otherwise `invalid_request` for a malformed body or a non-numeric or non-finite value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "405": {
            "description": "This endpoint is POST-only.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited (60 requests per minute, 500 per day, per IP).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/calculators/api/credit-card": {
      "post": {
        "summary": "Credit card real cost",
        "description": "The true cost of paying only the minimum on a credit card balance, including new spending and the months to clear it.",
        "operationId": "credit_card",
        "tags": [
          "calculators"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "balance": {
                    "type": "number",
                    "description": "Current balance in NZD."
                  },
                  "annual_rate": {
                    "type": "number",
                    "description": "Annual interest rate as a percentage, e.g. 20.95."
                  },
                  "min_payment_pct": {
                    "type": "number",
                    "description": "Minimum payment as a percentage of balance, e.g. 3."
                  },
                  "min_payment_flat": {
                    "type": "number",
                    "description": "Minimum payment floor in NZD, e.g. 25."
                  },
                  "monthly_spend": {
                    "type": "number",
                    "description": "New spending added each month in NZD. Defaults to 0."
                  }
                },
                "additionalProperties": false,
                "required": [
                  "balance",
                  "annual_rate",
                  "min_payment_pct"
                ],
                "examples": [
                  {
                    "balance": 8000,
                    "annual_rate": 20.95,
                    "min_payment_pct": 3,
                    "min_payment_flat": 25,
                    "monthly_spend": 0
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Calculated result. Top-level keys: months, years, total_interest, total_paid, original_balance, schedule[]. An optional `warnings` array may also be present: it names any input that was supplied but **not used**, so a figure can be wrong in a way that looks right. It is absent when there is nothing to report.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "warnings": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Inputs supplied but ignored. Present only when non-empty \u2014 do not treat its absence as meaning every input was used."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Rejected request. `code` is `missing_required_fields` when a declared required field was omitted (see `missing`), otherwise `invalid_request` for a malformed body or a non-numeric or non-finite value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "405": {
            "description": "This endpoint is POST-only.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited (60 requests per minute, 500 per day, per IP).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/calculators/api/retirement": {
      "post": {
        "summary": "Retirement savings",
        "description": "Projected savings at retirement and whether that meets a desired income, using a 4% safe-withdrawal assumption.",
        "operationId": "retirement",
        "tags": [
          "calculators"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "current_age": {
                    "type": "integer",
                    "description": "Current age in years."
                  },
                  "retirement_age": {
                    "type": "integer",
                    "description": "Target retirement age. Must exceed current_age."
                  },
                  "current_balance": {
                    "type": "number",
                    "description": "Non-KiwiSaver savings in NZD."
                  },
                  "kiwisaver_balance": {
                    "type": "number",
                    "description": "KiwiSaver balance in NZD."
                  },
                  "monthly_contribution": {
                    "type": "number",
                    "description": "Total monthly saving in NZD."
                  },
                  "annual_return": {
                    "type": "number",
                    "description": "Expected annual return as a percentage."
                  },
                  "desired_income": {
                    "type": "number",
                    "description": "Desired annual retirement income in NZD."
                  }
                },
                "additionalProperties": false,
                "required": [
                  "current_age",
                  "retirement_age"
                ],
                "examples": [
                  {
                    "current_age": 35,
                    "retirement_age": 65,
                    "current_balance": 50000,
                    "kiwisaver_balance": 40000,
                    "monthly_contribution": 800,
                    "annual_return": 5,
                    "desired_income": 45000
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Calculated result. Top-level keys: retirement_savings, annual_withdrawal, desired_income, shortfall, on_track, projection[]. An optional `warnings` array may also be present: it names any input that was supplied but **not used**, so a figure can be wrong in a way that looks right. It is absent when there is nothing to report.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "warnings": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Inputs supplied but ignored. Present only when non-empty \u2014 do not treat its absence as meaning every input was used."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Rejected request. `code` is `missing_required_fields` when a declared required field was omitted (see `missing`), otherwise `invalid_request` for a malformed body or a non-numeric or non-finite value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "405": {
            "description": "This endpoint is POST-only.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited (60 requests per minute, 500 per day, per IP).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/calculators/api/break-fee": {
      "post": {
        "summary": "Mortgage break fee",
        "description": "Estimated interest-rate-differential break fee for leaving a fixed NZ mortgage early.",
        "operationId": "break_fee",
        "tags": [
          "calculators"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "remaining_balance": {
                    "type": "number",
                    "description": "Outstanding balance in NZD."
                  },
                  "original_rate": {
                    "type": "number",
                    "description": "Your fixed rate as a percentage."
                  },
                  "current_rate": {
                    "type": "number",
                    "description": "Current market rate as a percentage."
                  },
                  "months_remaining": {
                    "type": "integer",
                    "description": "Months left on the fixed term. Clamped to 1-600."
                  }
                },
                "additionalProperties": false,
                "required": [
                  "remaining_balance",
                  "original_rate",
                  "current_rate",
                  "months_remaining"
                ],
                "examples": [
                  {
                    "remaining_balance": 400000,
                    "original_rate": 6.9,
                    "current_rate": 5.9,
                    "months_remaining": 18
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Calculated result. Top-level keys: estimated_fee, rate_differential, months_remaining, admin_fee, total_estimated_cost. An optional `warnings` array may also be present: it names any input that was supplied but **not used**, so a figure can be wrong in a way that looks right. It is absent when there is nothing to report.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "warnings": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Inputs supplied but ignored. Present only when non-empty \u2014 do not treat its absence as meaning every input was used."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Rejected request. `code` is `missing_required_fields` when a declared required field was omitted (see `missing`), otherwise `invalid_request` for a malformed body or a non-numeric or non-finite value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "405": {
            "description": "This endpoint is POST-only.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited (60 requests per minute, 500 per day, per IP).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/calculators/api/home-loan-compare": {
      "post": {
        "summary": "Home loan offer comparison",
        "description": "True-cost comparison of two home-loan offers including cash contributions, legal contributions and fees.",
        "operationId": "home_loan_compare",
        "tags": [
          "calculators"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "rate_a": {
                    "type": "number",
                    "description": "Offer A annual rate as a percentage."
                  },
                  "cash_a": {
                    "type": "number",
                    "description": "Offer A cash contribution in NZD."
                  },
                  "legal_a": {
                    "type": "number",
                    "description": "Offer A legal fees covered in NZD."
                  },
                  "monthly_fee_a": {
                    "type": "number",
                    "description": "Offer A monthly account fee in NZD."
                  },
                  "annual_fee_a": {
                    "type": "number",
                    "description": "Offer A annual fee in NZD."
                  },
                  "rate_b": {
                    "type": "number",
                    "description": "Offer B annual rate as a percentage."
                  },
                  "cash_b": {
                    "type": "number",
                    "description": "Offer B cash contribution in NZD."
                  },
                  "legal_b": {
                    "type": "number",
                    "description": "Offer B legal fees covered in NZD."
                  },
                  "monthly_fee_b": {
                    "type": "number",
                    "description": "Offer B monthly account fee in NZD."
                  },
                  "annual_fee_b": {
                    "type": "number",
                    "description": "Offer B annual fee in NZD."
                  },
                  "loan_amount": {
                    "type": "number",
                    "description": "Loan principal in NZD. Defaults to 500000."
                  },
                  "loan_term": {
                    "type": "integer",
                    "description": "Loan term in years. Defaults to 30."
                  }
                },
                "additionalProperties": false,
                "required": [
                  "rate_a",
                  "rate_b",
                  "loan_amount",
                  "loan_term"
                ],
                "examples": [
                  {
                    "rate_a": 5.99,
                    "cash_a": 5000,
                    "legal_a": 1000,
                    "monthly_fee_a": 10,
                    "annual_fee_a": 0,
                    "rate_b": 5.79,
                    "cash_b": 2000,
                    "legal_b": 0,
                    "monthly_fee_b": 12,
                    "annual_fee_b": 50,
                    "loan_amount": 600000,
                    "loan_term": 30
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Calculated result. Top-level keys: offer_a{}, offer_b{}, difference{better_offer}, comparison{}. An optional `warnings` array may also be present: it names any input that was supplied but **not used**, so a figure can be wrong in a way that looks right. It is absent when there is nothing to report.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "warnings": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Inputs supplied but ignored. Present only when non-empty \u2014 do not treat its absence as meaning every input was used."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Rejected request. `code` is `missing_required_fields` when a declared required field was omitted (see `missing`), otherwise `invalid_request` for a malformed body or a non-numeric or non-finite value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "405": {
            "description": "This endpoint is POST-only.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited (60 requests per minute, 500 per day, per IP).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/calculators/api/currency-convert": {
      "post": {
        "summary": "Currency conversion",
        "description": "Convert between USD and NZD at the latest stored rate, with 30 days of rate history. Only the USD/NZD pair is supported.",
        "operationId": "currency_convert",
        "tags": [
          "calculators"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "amount": {
                    "type": "number",
                    "description": "Amount to convert."
                  },
                  "from_currency": {
                    "type": "string",
                    "description": "ISO code, \"USD\" or \"NZD\" only."
                  },
                  "to_currency": {
                    "type": "string",
                    "description": "ISO code, \"USD\" or \"NZD\" only."
                  }
                },
                "additionalProperties": false,
                "required": [
                  "amount",
                  "from_currency",
                  "to_currency"
                ],
                "examples": [
                  {
                    "amount": 100,
                    "from_currency": "USD",
                    "to_currency": "NZD"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Calculated result. Top-level keys: amount, from_currency, to_currency, converted_amount, rate, rate_date, history[]. An optional `warnings` array may also be present: it names any input that was supplied but **not used**, so a figure can be wrong in a way that looks right. It is absent when there is nothing to report.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "warnings": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Inputs supplied but ignored. Present only when non-empty \u2014 do not treat its absence as meaning every input was used."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Rejected request. `code` is `missing_required_fields` when a declared required field was omitted (see `missing`), otherwise `invalid_request` for a malformed body or a non-numeric or non-finite value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "405": {
            "description": "This endpoint is POST-only.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited (60 requests per minute, 500 per day, per IP).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/calculators/api/kiwisaver": {
      "post": {
        "summary": "KiwiSaver projection",
        "description": "Projected KiwiSaver balance at retirement including your own, your employer's and the government contribution.",
        "operationId": "kiwisaver",
        "tags": [
          "calculators"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "salary": {
                    "type": "number",
                    "description": "Gross annual salary in NZD."
                  },
                  "contribution_rate": {
                    "type": "number",
                    "description": "Employee contribution rate as a percentage (3, 4, 6, 8 or 10)."
                  },
                  "current_age": {
                    "type": "integer",
                    "description": "Current age in years. Clamped to 18-64."
                  },
                  "retirement_age": {
                    "type": "integer",
                    "description": "Target retirement age. Clamped to current_age+1 .. 70."
                  },
                  "current_balance": {
                    "type": "number",
                    "description": "Existing KiwiSaver balance in NZD."
                  },
                  "fund_type": {
                    "type": "string",
                    "description": "One of conservative, balanced, growth, aggressive."
                  }
                },
                "additionalProperties": false,
                "required": [
                  "salary",
                  "contribution_rate",
                  "current_age"
                ],
                "examples": [
                  {
                    "salary": 80000,
                    "contribution_rate": 3,
                    "current_age": 35,
                    "retirement_age": 65,
                    "current_balance": 25000,
                    "fund_type": "balanced"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Calculated result. Top-level keys: retirement_savings, annual_withdrawal, total_your_contributions, total_employer_contributions, total_government_contributions, total_returns, projection[]. An optional `warnings` array may also be present: it names any input that was supplied but **not used**, so a figure can be wrong in a way that looks right. It is absent when there is nothing to report.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "warnings": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Inputs supplied but ignored. Present only when non-empty \u2014 do not treat its absence as meaning every input was used."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Rejected request. `code` is `missing_required_fields` when a declared required field was omitted (see `missing`), otherwise `invalid_request` for a malformed body or a non-numeric or non-finite value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "405": {
            "description": "This endpoint is POST-only.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited (60 requests per minute, 500 per day, per IP).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/calculators/api/net-worth": {
      "post": {
        "summary": "Net worth",
        "description": "Total assets minus total liabilities, compared against the Stats NZ median for the chosen age band.",
        "operationId": "net_worth",
        "tags": [
          "calculators"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "assets": {
                    "type": "object",
                    "description": "Map of asset name to value in NZD, e.g. {\"house\": 800000}."
                  },
                  "liabilities": {
                    "type": "object",
                    "description": "Map of liability name to value in NZD, e.g. {\"mortgage\": 500000}."
                  },
                  "age_band": {
                    "type": "string",
                    "description": "One of 15-24, 25-34, 35-44, 45-54, 55-64, 65+."
                  }
                },
                "additionalProperties": false,
                "required": [
                  "assets",
                  "liabilities"
                ],
                "examples": [
                  {
                    "assets": {
                      "house": 800000,
                      "savings": 20000
                    },
                    "liabilities": {
                      "mortgage": 500000
                    },
                    "age_band": "35-44"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Calculated result. Top-level keys: total_assets, total_liabilities, net_worth, age_band, median_net_worth, comparison. An optional `warnings` array may also be present: it names any input that was supplied but **not used**, so a figure can be wrong in a way that looks right. It is absent when there is nothing to report.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "warnings": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Inputs supplied but ignored. Present only when non-empty \u2014 do not treat its absence as meaning every input was used."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Rejected request. `code` is `missing_required_fields` when a declared required field was omitted (see `missing`), otherwise `invalid_request` for a malformed body or a non-numeric or non-finite value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "405": {
            "description": "This endpoint is POST-only.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited (60 requests per minute, 500 per day, per IP).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/calculators/api/ev-range": {
      "post": {
        "summary": "EV real-world range",
        "description": "Real-world EV range from WLTP range, temperature, driving style and battery health.",
        "operationId": "ev_range",
        "tags": [
          "calculators"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "model": {
                    "type": "string",
                    "description": "A model name from ValueHub's curated EV catalogue (see /calculators/ev-range/)."
                  },
                  "wltp_range": {
                    "type": "number",
                    "description": "WLTP range in km. Used only if model is not matched."
                  },
                  "battery_kwh": {
                    "type": "number",
                    "description": "Usable battery in kWh. Used only if model is not matched."
                  },
                  "temperature": {
                    "type": "string",
                    "description": "One of cold_inland, cold_coast, mild, hot."
                  },
                  "driving": {
                    "type": "string",
                    "description": "One of city, mixed, motorway."
                  },
                  "battery_health": {
                    "type": "number",
                    "description": "Battery health as a percentage. Clamped to 50-100."
                  }
                },
                "additionalProperties": false,
                "examples": [
                  {
                    "model": "Tesla Model Y (RWD)",
                    "temperature": "mild",
                    "driving": "mixed",
                    "battery_health": 100
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Calculated result. Top-level keys: model, estimated_range, efficiency_wh_km, temp_factor, driving_factor. An optional `warnings` array may also be present: it names any input that was supplied but **not used**, so a figure can be wrong in a way that looks right. It is absent when there is nothing to report.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "warnings": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Inputs supplied but ignored. Present only when non-empty \u2014 do not treat its absence as meaning every input was used."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Rejected request. `code` is `missing_required_fields` when a declared required field was omitted (see `missing`), otherwise `invalid_request` for a malformed body or a non-numeric or non-finite value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "405": {
            "description": "This endpoint is POST-only.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited (60 requests per minute, 500 per day, per IP).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/calculators/api/ev-running-cost": {
      "post": {
        "summary": "EV vs petrol running cost",
        "description": "Annual running cost of an EV against a petrol car \u2014 electricity plus RUC versus fuel.",
        "operationId": "ev_running_cost",
        "tags": [
          "calculators"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "model": {
                    "type": "string",
                    "description": "A model name from ValueHub's curated EV catalogue."
                  },
                  "ev_efficiency": {
                    "type": "number",
                    "description": "EV efficiency in Wh/km, if no model is matched."
                  },
                  "electricity_price": {
                    "type": "number",
                    "description": "Electricity price in NZD per kWh."
                  },
                  "ruc_per_1000km": {
                    "type": "number",
                    "description": "Road user charges in NZD per 1,000 km."
                  },
                  "km_per_year": {
                    "type": "number",
                    "description": "Annual distance in km. Defaults to 14000."
                  },
                  "petrol_efficiency": {
                    "type": "number",
                    "description": "Petrol car consumption in L/100km. Defaults to 7.5."
                  },
                  "petrol_price": {
                    "type": "number",
                    "description": "Petrol price in NZD per litre."
                  }
                },
                "additionalProperties": false,
                "examples": [
                  {
                    "model": "Tesla Model Y (RWD)",
                    "electricity_price": 0.4,
                    "ruc_per_1000km": 76,
                    "km_per_year": 14000,
                    "petrol_efficiency": 7.5,
                    "petrol_price": 3.4
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Calculated result. Top-level keys: ev_annual, petrol_annual, annual_saving, ev_cost_per_100km, petrol_cost_per_100km. An optional `warnings` array may also be present: it names any input that was supplied but **not used**, so a figure can be wrong in a way that looks right. It is absent when there is nothing to report.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "warnings": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Inputs supplied but ignored. Present only when non-empty \u2014 do not treat its absence as meaning every input was used."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Rejected request. `code` is `missing_required_fields` when a declared required field was omitted (see `missing`), otherwise `invalid_request` for a malformed body or a non-numeric or non-finite value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "405": {
            "description": "This endpoint is POST-only.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited (60 requests per minute, 500 per day, per IP).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/calculators/api/ev-charging": {
      "post": {
        "summary": "EV charging time and cost",
        "description": "Home AC charging time and cost for a charge window, compared with a public DC fast charge.",
        "operationId": "ev_charging",
        "tags": [
          "calculators"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "model": {
                    "type": "string",
                    "description": "A model name from ValueHub's curated EV catalogue."
                  },
                  "battery_kwh": {
                    "type": "number",
                    "description": "Usable battery in kWh, if no model is matched."
                  },
                  "start_pct": {
                    "type": "number",
                    "description": "Starting charge as a percentage."
                  },
                  "end_pct": {
                    "type": "number",
                    "description": "Target charge as a percentage."
                  },
                  "home_power": {
                    "type": "number",
                    "description": "Home charger power in kW. Defaults to 7.4."
                  },
                  "electricity_price": {
                    "type": "number",
                    "description": "Electricity price in NZD per kWh."
                  },
                  "off_peak_price": {
                    "type": "number",
                    "description": "Off-peak price per kWh, if you have one."
                  }
                },
                "additionalProperties": false,
                "examples": [
                  {
                    "model": "Tesla Model Y (RWD)",
                    "start_pct": 20,
                    "end_pct": 80,
                    "home_power": 7.4,
                    "electricity_price": 0.4,
                    "off_peak_price": 0
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Calculated result. Top-level keys: energy_needed, home_hours, home_cost, home_cost_offpeak, dc_hours, dc_cost. An optional `warnings` array may also be present: it names any input that was supplied but **not used**, so a figure can be wrong in a way that looks right. It is absent when there is nothing to report.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "warnings": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Inputs supplied but ignored. Present only when non-empty \u2014 do not treat its absence as meaning every input was used."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Rejected request. `code` is `missing_required_fields` when a declared required field was omitted (see `missing`), otherwise `invalid_request` for a malformed body or a non-numeric or non-finite value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "405": {
            "description": "This endpoint is POST-only.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited (60 requests per minute, 500 per day, per IP).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/calculators/api/paye": {
      "post": {
        "summary": "PAYE take-home pay",
        "description": "NZ take-home pay after PAYE tax, ACC levy, KiwiSaver and student loan, as annual and per-period figures.",
        "operationId": "paye",
        "tags": [
          "calculators"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "annual_income": {
                    "type": "number",
                    "description": "Gross annual salary in NZD."
                  },
                  "kiwisaver_rate": {
                    "type": "number",
                    "description": "KiwiSaver rate as a percentage. Defaults to 3."
                  },
                  "hours_per_week": {
                    "type": "integer",
                    "description": "Hours worked per week, for the hourly figure. Defaults to 40."
                  },
                  "apply_kiwisaver": {
                    "type": "boolean",
                    "description": "Deduct KiwiSaver. Defaults to false."
                  },
                  "apply_student_loan": {
                    "type": "boolean",
                    "description": "Deduct the student loan repayment. Defaults to false."
                  },
                  "apply_ietc": {
                    "type": "boolean",
                    "description": "Apply the Independent Earner Tax Credit. Defaults to false."
                  }
                },
                "additionalProperties": false,
                "required": [
                  "annual_income"
                ],
                "examples": [
                  {
                    "annual_income": 80000,
                    "kiwisaver_rate": 3,
                    "hours_per_week": 40,
                    "apply_kiwisaver": true
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Calculated result. Top-level keys: annual{}, percentages{}, periods{hour, day, week, fortnight, month}. An optional `warnings` array may also be present: it names any input that was supplied but **not used**, so a figure can be wrong in a way that looks right. It is absent when there is nothing to report.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "warnings": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Inputs supplied but ignored. Present only when non-empty \u2014 do not treat its absence as meaning every input was used."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Rejected request. `code` is `missing_required_fields` when a declared required field was omitted (see `missing`), otherwise `invalid_request` for a malformed body or a non-numeric or non-finite value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "405": {
            "description": "This endpoint is POST-only.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited (60 requests per minute, 500 per day, per IP).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/rates/term-deposits.json": {
      "get": {
        "summary": "Term deposit rates",
        "description": "Every term deposit rate ValueHub publishes, one row per institution and term, with the credit rating and its agency, the minimum deposit, a link to the institution's own page for the rate, and the date the figure was read.",
        "operationId": "term-deposits",
        "tags": [
          "rates"
        ],
        "responses": {
          "200": {
            "description": "Current term deposit rates. Top-level keys: as_at, timezone, count, currency, rates[] (institution, institution_id, term, term_months, rate, min_deposit, credit_rating, credit_rating_agency, source_url, last_updated). `count` is the length of `rates`. An empty `rates` array with HTTP 200 means the table is genuinely empty; a failure returns a non-200 status.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "feed": {
                      "type": "string"
                    },
                    "as_at": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Newest read time anywhere in the feed, ISO 8601. null only if the feed is empty."
                    },
                    "timezone": {
                      "type": "string",
                      "description": "States that these timestamps are naive Pacific/Auckland time with no offset."
                    },
                    "count": {
                      "type": "integer",
                      "description": "Number of rows in `rates`."
                    },
                    "currency": {
                      "type": "string"
                    },
                    "page": {
                      "type": "string",
                      "description": "The human-readable page showing the same data."
                    },
                    "notes": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "rates": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "institution": {
                            "type": "string",
                            "description": "Institution name as published, e.g. \"ANZ\"."
                          },
                          "institution_id": {
                            "type": "string",
                            "description": "Stable slug for the institution, e.g. \"anz\"."
                          },
                          "term": {
                            "type": "string",
                            "description": "Term label as published, e.g. \"6 months\"."
                          },
                          "term_months": {
                            "type": "integer",
                            "description": "The same term in months, for arithmetic."
                          },
                          "rate": {
                            "type": "number",
                            "description": "Annual rate as a percentage, e.g. 3.5. NULL means the term is not priced \u2014 it is not zero."
                          },
                          "min_deposit": {
                            "type": "number",
                            "description": "The minimum deposit in NZD from which this rate applies. See notes \u2014 higher balances or variant products may be priced differently and are not listed."
                          },
                          "credit_rating": {
                            "type": "string",
                            "description": "Credit rating, or NULL where the institution holds none."
                          },
                          "credit_rating_agency": {
                            "type": "string",
                            "description": "Which agency issued the rating \u2014 the table spans more than one, so it travels with the value."
                          },
                          "source_url": {
                            "type": "string",
                            "description": "The institution's own page for this rate, so you can check it. See notes."
                          },
                          "last_updated": {
                            "type": "string",
                            "description": "When this figure was last read, ISO 8601, naive NZ time."
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "405": {
            "description": "This endpoint accepts GET only.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited (60 requests per minute, 500 per day, per IP).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "The feed could not be served. `code` is `server_error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/rates/mortgages.json": {
      "get": {
        "summary": "Mortgage rates",
        "description": "Every mortgage rate ValueHub publishes, one row per lender, product and term, with a link to the lender's own page and the date the figure was read.",
        "operationId": "mortgages",
        "tags": [
          "rates"
        ],
        "responses": {
          "200": {
            "description": "Current mortgage rates. Top-level keys: as_at, timezone, count, currency, rates[] (lender, institution_id, product, term, term_months, rate, source_url, last_updated). `count` is the length of `rates`. An empty `rates` array with HTTP 200 means the table is genuinely empty; a failure returns a non-200 status.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "feed": {
                      "type": "string"
                    },
                    "as_at": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Newest read time anywhere in the feed, ISO 8601. null only if the feed is empty."
                    },
                    "timezone": {
                      "type": "string",
                      "description": "States that these timestamps are naive Pacific/Auckland time with no offset."
                    },
                    "count": {
                      "type": "integer",
                      "description": "Number of rows in `rates`."
                    },
                    "currency": {
                      "type": "string"
                    },
                    "page": {
                      "type": "string",
                      "description": "The human-readable page showing the same data."
                    },
                    "notes": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "rates": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "lender": {
                            "type": "string",
                            "description": "Lender name as published, e.g. \"ANZ\"."
                          },
                          "institution_id": {
                            "type": "string",
                            "description": "The rate feed's own institution key, e.g. \"institution:anz\"."
                          },
                          "product": {
                            "type": "string",
                            "description": "Product name as published, e.g. \"Standard\" or \"Special\"."
                          },
                          "term": {
                            "type": "string",
                            "description": "Term label as published, e.g. \"1 year\" or \"Floating\"."
                          },
                          "term_months": {
                            "type": "integer",
                            "description": "The same term in months; NULL for floating."
                          },
                          "rate": {
                            "type": "number",
                            "description": "Annual rate as a percentage. A 0 is a real product, not a missing value."
                          },
                          "source_url": {
                            "type": "string",
                            "description": "The lender's own page for this rate, so you can check it. See notes."
                          },
                          "last_updated": {
                            "type": "string",
                            "description": "When this figure was last read, ISO 8601, naive NZ time."
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "405": {
            "description": "This endpoint accepts GET only.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited (60 requests per minute, 500 per day, per IP).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "The feed could not be served. `code` is `server_error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/rates/bonds.json": {
      "get": {
        "summary": "Bond rates",
        "description": "Every bond ValueHub publishes on its NZX debt-market page \u2014 code, issuer, coupon, current yield, the daily yield movement and the instrument's final maturity date \u2014 one row per bond, with a link to the bond's own NZX page.",
        "operationId": "bonds",
        "tags": [
          "rates"
        ],
        "responses": {
          "200": {
            "description": "Current bond rates. Top-level keys: as_at, timezone, count, currency, rates[] (code, isin, issuer, name, coupon_rate, maturity_date, yield, yield_change, snapshot_date, source_url, last_updated). `count` is the length of `rates`. An empty `rates` array with HTTP 200 means the table is genuinely empty; a failure returns a non-200 status.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "feed": {
                      "type": "string"
                    },
                    "as_at": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Newest read time anywhere in the feed, ISO 8601. null only if the feed is empty."
                    },
                    "timezone": {
                      "type": "string",
                      "description": "States that these timestamps are naive Pacific/Auckland time with no offset."
                    },
                    "count": {
                      "type": "integer",
                      "description": "Number of rows in `rates`."
                    },
                    "currency": {
                      "type": "string"
                    },
                    "page": {
                      "type": "string",
                      "description": "The human-readable page showing the same data."
                    },
                    "notes": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "rates": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "code": {
                            "type": "string",
                            "description": "NZX instrument code, e.g. \"MCY050\". Unique within a snapshot."
                          },
                          "isin": {
                            "type": "string",
                            "description": "ISIN, for matching a bond against another source."
                          },
                          "issuer": {
                            "type": "string",
                            "description": "Issuer name as published, e.g. \"Mercury NZ Limited (NS)\"."
                          },
                          "name": {
                            "type": "string",
                            "description": "The instrument's own NZX description, which carries its maturity date and coupon, e.g. \"MCY 13/05/2052 5.73% Mercury NZ Limited (NS) Capital Bonds\"."
                          },
                          "coupon_rate": {
                            "type": "number",
                            "description": "Interest rate on the face value, as a percentage. Fixed for the life of the bond. NULL if the instrument does not state one."
                          },
                          "maturity_date": {
                            "type": "string",
                            "description": "FINAL maturity, ISO date, read from the instrument's own name. Some issues can be repaid early \u2014 see notes."
                          },
                          "yield": {
                            "type": "number",
                            "description": "Current yield as a percentage, from the last traded price. NULL if no price is published. Can be negative."
                          },
                          "yield_change": {
                            "type": "number",
                            "description": "Movement in yield since the previous session, in percentage points. May be negative."
                          },
                          "snapshot_date": {
                            "type": "string",
                            "description": "The market session this row was read from, ISO date."
                          },
                          "source_url": {
                            "type": "string",
                            "description": "This bond's NZX instrument page, from which the coupon, maturity and last price are read."
                          },
                          "last_updated": {
                            "type": "string",
                            "description": "When this row was last written, ISO 8601, naive NZ time."
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "405": {
            "description": "This endpoint accepts GET only.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited (60 requests per minute, 500 per day, per IP).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "The feed could not be served. `code` is `server_error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  }
}