{
  "openapi": "3.1.0",
  "info": {
    "title": "Canada Fee Signal — experimental municipal fee components",
    "version": "1.0.0",
    "description": "Selected 2026 Canadian municipal fee components, not complete permit/project costs. Verify source on date of use. Paid API access is optional and off by default until ledger, webhook and Stripe are configured and tested. Public research endpoints remain available during pilot."
  },
  "servers": [
    {
      "url": "https://canada-fee-signal.pages.dev",
      "description": "Current Canada Fee Signal deployment"
    }
  ],
  "paths": {
    "/api/v1/health": {
      "get": {
        "summary": "Service status",
        "responses": {
          "200": {
            "description": "Status JSON"
          }
        }
      }
    },
    "/api/v1/catalog": {
      "get": {
        "summary": "Full public fee/rules catalog",
        "responses": {
          "200": {
            "description": "Complete catalog with official source links"
          }
        }
      }
    },
    "/api/v1/jurisdictions": {
      "get": {
        "summary": "Covered municipal sources",
        "parameters": [
          {
            "name": "province",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Optional two-letter province/territory code"
          }
        ],
        "responses": {
          "200": {
            "description": "Jurisdiction list"
          }
        }
      }
    },
    "/api/v1/fees": {
      "get": {
        "summary": "Filter available fee rules",
        "parameters": [
          {
            "name": "jurisdiction",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "category",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Fee rules and required fields"
          }
        }
      }
    },
    "/api/v1/calculate": {
      "get": {
        "summary": "Calculate one published fee component",
        "description": "Select fee ID via /api/v1/fees. If required, provide input named by fee.parameter.key (e.g. construction_value_cad). Returns is_total_project_fee:false.",
        "parameters": [
          {
            "name": "fee",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "surrey-plan"
          },
          {
            "name": "jurisdiction",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "example": "surrey"
          },
          {
            "name": "construction_value_cad",
            "in": "query",
            "schema": {
              "type": "number"
            },
            "description": "Example only; parameter varies by selected fee ID"
          }
        ],
        "responses": {
          "200": {
            "description": "One source-linked component"
          },
          "400": {
            "description": "Missing or invalid parameter"
          },
          "404": {
            "description": "Unknown fee"
          }
        }
      }
    },
    "/api/v1/bulk": {
      "post": {
        "summary": "Batch 1–25 independent fee component calculations",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "requests"
                ],
                "properties": {
                  "requests": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 25,
                    "items": {
                      "type": "object",
                      "required": [
                        "fee"
                      ],
                      "additionalProperties": true
                    }
                  }
                }
              },
              "example": {
                "requests": [
                  {
                    "fee": "surrey-plan",
                    "construction_value_cad": 25000
                  },
                  {
                    "fee": "winnipeg-simple-deck"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Results array with individual success/error flags"
          },
          "400": {
            "description": "Invalid request"
          },
          "413": {
            "description": "Too large"
          }
        }
      }
    },
    "/api/v1/billing/status": {
      "get": {
        "summary": "Check whether Stripe checkout, credit ledger, and paid API are enabled",
        "responses": {
          "200": {
            "description": "Safe public read-only billing status; no secrets."
          }
        }
      }
    },
    "/api/v1/billing/checkout": {
      "post": {
        "summary": "Start $5 CAD Stripe-hosted checkout for 100 lookup credits (disabled until tested)",
        "responses": {
          "200": {
            "description": "Stripe checkout URL"
          },
          "503": {
            "description": "Payments safely disabled"
          }
        }
      }
    },
    "/api/v1/billing/claim": {
      "post": {
        "summary": "Exchange confirmed Stripe Checkout session for private API key (same purchase never adds credits twice)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "session_id"
                ],
                "properties": {
                  "session_id": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "API key and credit balance, only for verified paid sessions"
          },
          "403": {
            "description": "Not paid or wrong product"
          }
        }
      }
    },
    "/api/v1/paid/calculate": {
      "get": {
        "summary": "Authenticated source-linked fee calculation, one credit per successful result",
        "security": [
          {
            "AgentApiKey": []
          }
        ],
        "parameters": [
          {
            "name": "fee",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "construction_value_cad",
            "in": "query",
            "schema": {
              "type": "number"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Fee result + credits charged and remaining"
          },
          "400": {
            "description": "Invalid input, no credit charged"
          },
          "401": {
            "description": "API key missing or invalid"
          },
          "402": {
            "description": "Credits exhausted"
          },
          "503": {
            "description": "Paid API offline"
          }
        }
      }
    },
    "/api/v1/paid/credits": {
      "get": {
        "summary": "Authenticated remaining-credit balance (free of charge)",
        "security": [
          {
            "AgentApiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "Remaining credits"
          },
          "401": {
            "description": "Invalid API key"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "AgentApiKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "Private API key issued after Stripe payment is verified; never put this in a URL."
      }
    }
  }
}