{
  "openapi": "3.0.3",
  "info": {
    "title": "UbuntuSoft Finance Integration API",
    "version": "1.0.0",
    "description": "Read-only access to a municipality's indigent register, shaped for import into a municipal financial system.\n\nEvery household is keyed on **AccountNumber** \u2014 the identifier the billing system and the indigent register both hold.\n\n## Two calls that matter\n\n- `GET /register` \u2014 every household that should currently be subsidised. A full snapshot, safe to poll repeatedly.\n- `GET /changes` \u2014 every account whose subsidy state should change since a date. `SUBSIDISE` for newly approved households, `CEASE` for those rejected, deceased or no longer qualifying. **This is the call that stops a municipality paying for households that came off the register.**\n\n## What this API does not tell you\n\nIt says *who* qualifies, not *what* they receive. Tariff policy decides the benefit and lives in the financial system. mSCOA segments are configured by the municipality's own finance office and stamped onto every record; they are never derived or guessed.\n\n## Authentication\n\nAn integration key issued by UbuntuSoft on the municipality's request, scoped to that municipality and to `finance:read`. Keys are revocable and may expire. Send as `Authorization: Bearer <key>` or `X-Api-Key: <key>`. Start with `GET /whoami` to confirm the key and see what it reaches.",
    "contact": {
      "name": "UbuntuSoft Support",
      "email": "support@ubuntusoft.co.za"
    }
  },
  "servers": [
    {
      "url": "https://{subdomain}.ubuntusoft.co.za/api/Finance/v1",
      "description": "A municipality's own instance",
      "variables": {
        "subdomain": {
          "default": "pixley",
          "description": "The municipality's subdomain"
        }
      }
    }
  ],
  "security": [
    {
      "BearerKey": []
    },
    {
      "ApiKeyHeader": []
    }
  ],
  "components": {
    "securitySchemes": {
      "BearerKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "Authorization: Bearer usk_live_..."
      },
      "ApiKeyHeader": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Api-Key",
        "description": "X-Api-Key: usk_live_..."
      }
    },
    "schemas": {
      "MscoaSegments": {
        "type": "object",
        "description": "Segment values as configured by the municipality's finance office, validated against the National Treasury chart. Null where not configured \u2014 never guessed.",
        "properties": {
          "project": {
            "type": "string",
            "nullable": true,
            "example": "PROJ-FBS-001"
          },
          "funding": {
            "type": "string",
            "nullable": true,
            "example": "FUND-EQUITABLE-SHARE"
          },
          "function": {
            "type": "string",
            "nullable": true,
            "example": "FUNC-COMMUNITY-SVC"
          },
          "item": {
            "type": "string",
            "nullable": true,
            "example": "ITEM-TRANSFERS-SUBSIDIES"
          },
          "region": {
            "type": "string",
            "nullable": true
          },
          "costing": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "Household": {
        "type": "object",
        "required": [
          "accountNumber",
          "indigentStatus"
        ],
        "properties": {
          "accountNumber": {
            "type": "string",
            "description": "The billing account. Leading zeros are significant.",
            "example": "0001011793"
          },
          "action": {
            "type": "string",
            "nullable": true,
            "enum": [
              "SUBSIDISE",
              "CEASE",
              null
            ],
            "description": "Present on /changes only."
          },
          "idNumber": {
            "type": "string",
            "nullable": true,
            "example": "5704065351084"
          },
          "surname": {
            "type": "string",
            "nullable": true
          },
          "firstNames": {
            "type": "string",
            "nullable": true
          },
          "ward": {
            "type": "integer",
            "nullable": true
          },
          "standNumber": {
            "type": "string",
            "nullable": true
          },
          "electricityMeterNumber": {
            "type": "string",
            "nullable": true
          },
          "waterMeterNumber": {
            "type": "string",
            "nullable": true
          },
          "indigentStatus": {
            "type": "string",
            "example": "Approved",
            "description": "Approved means subsidise. Rejected, Deceased and Unmatched mean cease."
          },
          "registeredDate": {
            "type": "string",
            "format": "date"
          },
          "origin": {
            "type": "string",
            "enum": [
              "Captured",
              "Migrated"
            ],
            "description": "Migrated households were inherited from a previous register, not decided in this system."
          },
          "applicationReference": {
            "type": "string",
            "format": "uuid"
          },
          "mscoa": {
            "$ref": "#/components/schemas/MscoaSegments"
          }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          },
          "message": {
            "type": "string"
          }
        }
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Missing, invalid, revoked or expired key.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "invalid_key",
              "message": "Provide a valid integration key."
            }
          }
        }
      },
      "Forbidden": {
        "description": "The key does not grant the required scope.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "insufficient_scope",
              "message": "This key does not grant 'finance:read'."
            }
          }
        }
      }
    },
    "parameters": {
      "Page": {
        "name": "page",
        "in": "query",
        "schema": {
          "type": "integer",
          "default": 1,
          "minimum": 1
        }
      },
      "PageSize": {
        "name": "pageSize",
        "in": "query",
        "schema": {
          "type": "integer",
          "default": 500,
          "minimum": 1,
          "maximum": 5000
        }
      }
    }
  },
  "paths": {
    "/whoami": {
      "get": {
        "summary": "Confirm the key and see what it reaches",
        "description": "The first call to make. Confirms the key is valid without needing to know a municipality id.",
        "responses": {
          "200": {
            "description": "Key is valid.",
            "content": {
              "application/json": {
                "example": {
                  "municipality": "Pixley Ka Seme Local Municipality",
                  "subdomain": "pixley",
                  "keyName": "Munsoft nightly pull",
                  "scopes": [
                    "finance:read"
                  ],
                  "expiresAt": null,
                  "apiVersion": "v1"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/register": {
      "get": {
        "summary": "Households that should currently be subsidised",
        "description": "A full snapshot. Safe to poll repeatedly \u2014 importing it twice changes nothing.",
        "parameters": [
          {
            "$ref": "#/components/parameters/Page"
          },
          {
            "$ref": "#/components/parameters/PageSize"
          }
        ],
        "responses": {
          "200": {
            "description": "The current register.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "municipality": {
                      "type": "string"
                    },
                    "generatedAt": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "apiVersion": {
                      "type": "string"
                    },
                    "mscoaChartVersion": {
                      "type": "string",
                      "example": "7.1",
                      "description": "The National Treasury chart version the segments were configured against."
                    },
                    "page": {
                      "type": "integer"
                    },
                    "pageSize": {
                      "type": "integer"
                    },
                    "totalRows": {
                      "type": "integer"
                    },
                    "totalPages": {
                      "type": "integer"
                    },
                    "households": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Household"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/changes": {
      "get": {
        "summary": "Accounts whose subsidy state should change",
        "description": "Poll this on your billing cycle. CEASE rows include deceased households \u2014 the ones that otherwise stay subsidised indefinitely because nobody tells finance.",
        "parameters": [
          {
            "name": "since",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "Defaults to one month ago.",
            "example": "2026-07-01"
          },
          {
            "$ref": "#/components/parameters/Page"
          },
          {
            "$ref": "#/components/parameters/PageSize"
          }
        ],
        "responses": {
          "200": {
            "description": "Changes since the given date.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "municipality": {
                      "type": "string"
                    },
                    "since": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "generatedAt": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "totalRows": {
                      "type": "integer"
                    },
                    "changes": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Household"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Unparseable date.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/summary": {
      "get": {
        "summary": "Counts, without pulling the data",
        "description": "Useful as a health check and for reconciliation before a billing run.",
        "responses": {
          "200": {
            "description": "Counts.",
            "content": {
              "application/json": {
                "example": {
                  "municipality": "Pixley Ka Seme Local Municipality",
                  "toSubsidise": 2404,
                  "toCease": 1172,
                  "deceasedStillOnFile": 23,
                  "approvedWithoutAccountNumber": 0
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    }
  }
}