{
    "openapi": "3.0.3",
    "info": {
        "title": "UtilityBillReader Partner API",
        "description": "Upload a utility bill PDF, fetch the parsed JSON, list every bill on your account, and check your remaining page quota. Response objects use the same canonical column names as the CSV / Excel exports. This spec is generated from the canonical field dictionary, so it always matches the live export schema.",
        "version": "1.0.0",
        "x-schema-version": 1,
        "contact": {
            "email": "support@utilitybillreader.com"
        }
    },
    "servers": [
        {
            "url": "https://utilitybillreader.com/api/v1",
            "description": "This deployment"
        },
        {
            "url": "https://utilitybillreader.com/api/v1",
            "description": "Production"
        }
    ],
    "security": [
        {
            "bearerAuth": []
        }
    ],
    "tags": [
        {
            "name": "Bills",
            "description": "Upload, fetch, and list parsed bills."
        },
        {
            "name": "Account",
            "description": "Current user, team, and quota."
        }
    ],
    "paths": {
        "/bill": {
            "post": {
                "tags": [
                    "Bills"
                ],
                "summary": "Upload a bill",
                "operationId": "uploadBill",
                "description": "Upload one PDF. Returns 202 Accepted with the new extraction id; poll GET /bill/{id} to track status and fetch the parsed result.",
                "requestBody": {
                    "required": true,
                    "content": {
                        "multipart/form-data": {
                            "schema": {
                                "type": "object",
                                "required": [
                                    "file"
                                ],
                                "properties": {
                                    "file": {
                                        "type": "string",
                                        "format": "binary",
                                        "description": "The bill PDF to parse."
                                    }
                                }
                            }
                        }
                    }
                },
                "responses": {
                    "202": {
                        "description": "Accepted and queued for extraction.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/BillAccepted"
                                }
                            }
                        }
                    },
                    "400": {
                        "description": "Validation failure — missing `file` or wrong mimetype.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "Missing or invalid Authorization token.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "402": {
                        "description": "Upload would exceed your team's remaining pages.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "413": {
                        "description": "File larger than the per-upload size cap.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                }
            },
            "get": {
                "tags": [
                    "Bills"
                ],
                "summary": "List bills",
                "operationId": "listBills",
                "description": "Paginated list of every bill on your team, newest first. In-flight bills appear with `rows` and `line_items` null.",
                "parameters": [
                    {
                        "name": "limit",
                        "in": "query",
                        "required": false,
                        "description": "Page size, 1–100.",
                        "schema": {
                            "type": "integer",
                            "minimum": 1,
                            "maximum": 100,
                            "default": 50
                        }
                    },
                    {
                        "name": "since",
                        "in": "query",
                        "required": false,
                        "description": "ISO 8601 timestamp; only return bills created on or after this moment.",
                        "schema": {
                            "type": "string",
                            "format": "date-time"
                        }
                    },
                    {
                        "name": "cursor",
                        "in": "query",
                        "required": false,
                        "description": "Opaque pagination token returned as `next_cursor` on the previous page.",
                        "schema": {
                            "type": "string"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "A page of bills.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/BillList"
                                }
                            }
                        }
                    },
                    "400": {
                        "description": "Malformed `since` or `cursor` parameter.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "Missing or invalid Authorization token.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/bill/{id}": {
            "get": {
                "tags": [
                    "Bills"
                ],
                "summary": "Fetch a bill",
                "operationId": "getBill",
                "description": "One endpoint covers status polling and result retrieval. Always 200 OK while the bill exists — check `status` (or `rows !== null`), not the HTTP code. `rows` and `line_items` are null until the extraction reaches a terminal state.",
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "description": "Extraction id returned by POST /bill.",
                        "schema": {
                            "type": "string",
                            "format": "uuid"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "The bill, in any state.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Extraction"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "Missing or invalid Authorization token.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "Bill belongs to a different team.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "No bill with that id, or past your plan's retention window.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/user": {
            "get": {
                "tags": [
                    "Account"
                ],
                "summary": "Current user and quota",
                "operationId": "getUser",
                "description": "Current user, team, and remaining page quota. Poll before a bulk upload to confirm you have enough pages.",
                "responses": {
                    "200": {
                        "description": "The authenticated user and quota.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/User"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "Missing or invalid Authorization token.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                }
            }
        }
    },
    "components": {
        "securitySchemes": {
            "bearerAuth": {
                "type": "http",
                "scheme": "bearer",
                "description": "Personal API token minted from the API Tokens page. Sent as `Authorization: Bearer <token>` on every request."
            }
        },
        "schemas": {
            "BillAccepted": {
                "type": "object",
                "required": [
                    "id",
                    "status",
                    "source_filename"
                ],
                "properties": {
                    "id": {
                        "type": "string",
                        "format": "uuid"
                    },
                    "status": {
                        "type": "string",
                        "example": "queued"
                    },
                    "source_filename": {
                        "type": "string"
                    },
                    "source_page_count": {
                        "type": "integer",
                        "nullable": true
                    },
                    "created_at": {
                        "type": "string",
                        "format": "date-time",
                        "nullable": true
                    }
                }
            },
            "Extraction": {
                "type": "object",
                "description": "The response envelope for a single bill. `rows` and `line_items` are null until the extraction reaches a terminal state, then become arrays.",
                "required": [
                    "id",
                    "status",
                    "source_filename",
                    "warnings",
                    "rows",
                    "line_items"
                ],
                "properties": {
                    "id": {
                        "type": "string",
                        "format": "uuid"
                    },
                    "status": {
                        "type": "string",
                        "enum": [
                            "queued",
                            "processing",
                            "completed",
                            "partial",
                            "failed",
                            "needs_review"
                        ]
                    },
                    "source_filename": {
                        "type": "string"
                    },
                    "source_page_count": {
                        "type": "integer",
                        "nullable": true
                    },
                    "extraction_confidence": {
                        "type": "number",
                        "nullable": true,
                        "description": "Overall extractor confidence, 0–1."
                    },
                    "warnings": {
                        "type": "array",
                        "items": {
                            "type": "string"
                        },
                        "description": "Warning codes; see the Warning codes table on the docs page."
                    },
                    "error_code": {
                        "type": "string",
                        "nullable": true
                    },
                    "error_message": {
                        "type": "string",
                        "nullable": true
                    },
                    "created_at": {
                        "type": "string",
                        "format": "date-time",
                        "nullable": true
                    },
                    "extracted_at": {
                        "type": "string",
                        "format": "date-time",
                        "nullable": true
                    },
                    "rows": {
                        "type": "array",
                        "nullable": true,
                        "description": "One object per metered service. Null until terminal.",
                        "items": {
                            "$ref": "#/components/schemas/BillRow"
                        }
                    },
                    "line_items": {
                        "type": "array",
                        "nullable": true,
                        "description": "One object per charge line across every meter. Null until terminal; empty array on bills with no itemized lines.",
                        "items": {
                            "$ref": "#/components/schemas/LineItem"
                        }
                    }
                }
            },
            "BillList": {
                "type": "object",
                "required": [
                    "data",
                    "next_cursor"
                ],
                "properties": {
                    "data": {
                        "type": "array",
                        "items": {
                            "$ref": "#/components/schemas/Extraction"
                        }
                    },
                    "next_cursor": {
                        "type": "string",
                        "nullable": true,
                        "description": "Echo back as `cursor` to fetch the next page. Null at the end of the list."
                    }
                }
            },
            "BillRow": {
                "type": "object",
                "description": "A single metered-service row, keyed by canonical CSV column names.",
                "properties": {
                    "source_filename": {
                        "type": "string",
                        "description": "Original filename uploaded by the user. Used to trace each CSV row back to its source PDF."
                    },
                    "source_page_count": {
                        "type": "number",
                        "description": "Number of pages in the source PDF.",
                        "nullable": true
                    },
                    "extraction_status": {
                        "type": "string",
                        "enum": [
                            "completed",
                            "partial",
                            "failed"
                        ],
                        "description": "Status of the extraction run. 'partial' means critical fields were missing or overall confidence fell below threshold; 'failed' means the bill was unreadable or extraction errored out."
                    },
                    "extraction_error": {
                        "type": "string",
                        "description": "Error code when extraction_status is 'failed' (e.g. 'unreadable_file', 'extraction_failed'). Null otherwise.",
                        "nullable": true
                    },
                    "extraction_warnings": {
                        "type": "string",
                        "description": "Semicolon-joined warning codes emitted during extraction (e.g. 'MULTI_METER;COST_SHARED_ACROSS_METERS'). See warning_codes section of this dictionary for the canonical set.",
                        "nullable": true
                    },
                    "extraction_confidence": {
                        "type": "number",
                        "description": "Mean confidence (0-1) across all populated fields for this row, rounded to 4 decimal places.",
                        "nullable": true
                    },
                    "transmission_region": {
                        "type": "string",
                        "enum": [
                            "CAISO",
                            "ERCOT",
                            "NYISO",
                            "ISONE",
                            "PJM",
                            "MISO",
                            "SPP",
                            "SERC",
                            "WECC_SW",
                            "WECC_PC",
                            "NEM",
                            "WEM",
                            "OTHER"
                        ],
                        "description": "Electric-only. Wholesale market / ISO-RTO that operates the transmission grid serving this meter. US ISO/RTOs: CAISO, ERCOT, NYISO, ISONE, PJM, MISO, SPP, plus the SERC reliability region and WECC_SW / WECC_PC for non-CAISO western US. Australian markets (AEMO-operated): NEM (eastern + southern states — NSW/QLD/VIC/SA/TAS/ACT) and WEM (Western Australia's SWIS). Use OTHER for jurisdictions outside these (e.g. Pilbara NWIS, Northern Territory, non-AU/US bills) and null for gas and water bills.",
                        "nullable": true
                    },
                    "load_zone": {
                        "type": "string",
                        "description": "Electric-only. ISO/RTO sub-zone where the meter is located. Format varies by market: CAISO uses SLAP identifiers like 'SLAP_SCEW'; ERCOT uses 'LZ_HOUSTON' / 'LZ_NORTH'; NYISO uses single-letter zones like 'J' or 'K'; PJM uses zone names like 'AEP', 'COMED'; NEM (Australia) uses AEMO region codes 'NSW1' / 'QLD1' / 'VIC1' / 'SA1' / 'TAS1' (the digit-1 suffix is part of the canonical code, never strip it); WEM (Western Australia) is a single region so populate 'SWIS' when applicable. Gas and water bills leave this null.",
                        "nullable": true
                    },
                    "utility_name": {
                        "type": "string",
                        "description": "Name of the regulated utility / delivery company that owns the wires to this meter, as printed on the bill. Never normalized or abbreviated by the extractor."
                    },
                    "utility_phone": {
                        "type": "string",
                        "description": "Customer-service phone number for the utility, as printed on the bill.",
                        "nullable": true
                    },
                    "utility_vendor_id": {
                        "type": "string",
                        "description": "Utility's internal vendor/supplier code if printed on the bill. Rare.",
                        "nullable": true
                    },
                    "utility_tax_id": {
                        "type": "string",
                        "description": "Federal tax ID (FEIN) of the utility if printed on the bill. Mostly seen on commercial bills.",
                        "nullable": true
                    },
                    "commodity_supplier": {
                        "type": "string",
                        "description": "Competitive supplier the customer buys their commodity from, when distinct from the delivery utility. Electric markets call this the Load-Serving Entity (LSE), Competitive Retail Electric Service (CRES) provider, or Community Choice Aggregator (CCA). Gas markets call this a marketer or natural-gas supplier. Null on bundled / regulated-monopoly bills where the utility is both supplier and deliverer.",
                        "nullable": true
                    },
                    "service_voltage": {
                        "type": "string",
                        "description": "Electric-only. Service voltage level / category as printed on the bill. May be a tier name ('Primary', 'Secondary', 'Transmission'), a phase descriptor ('Secondary Three Phase'), or a kV range ('>= 600 V', '< 35 kV'). Capture verbatim. Gas/water bills leave this null.",
                        "nullable": true
                    },
                    "service_account_number": {
                        "type": "string",
                        "description": "Canonical site/service identifier used by the utility to refer to the specific service point. Works for electric, gas, water, and other utility commodities. Goes by different names across regions and commodities: CAISO calls it Service Agreement ID / Service Account Number (SAN); ERCOT uses ESIID; ISO-NE uses Electric Account Number; NYISO uses TO Account Number; AES Ohio calls it Choice Service ID. All variants are the same concept under different labels — they identify the service/site at the utility's grid level.",
                        "nullable": true
                    },
                    "utility_meter_serial_number": {
                        "type": "string",
                        "description": "Serial number of the physical meter device, as engraved on the meter and printed on the bill. One row is emitted per meter on multi-meter bills, each with its own utility_meter_serial_number.",
                        "nullable": true
                    },
                    "point_of_delivery_id": {
                        "type": "string",
                        "description": "Premise-level identifier that stays stable across customer changes (e.g. SCE's 17-digit POD ID; an apartment building's identifier doesn't change when a new tenant moves in). Different from service_account_number, which can change when the customer's account changes. Populated only when the bill prints an explicit Point-of-Delivery / Premise / POD identifier distinct from the service-account number.",
                        "nullable": true
                    },
                    "customer_name": {
                        "type": "string",
                        "description": "Customer / account-holder name as printed on the bill (the entity that pays for service at this account).",
                        "nullable": true
                    },
                    "service_street1": {
                        "type": "string",
                        "description": "Street number and street name of the service address (where the commodity is delivered).",
                        "nullable": true
                    },
                    "service_street2": {
                        "type": "string",
                        "description": "Apartment, suite, unit, or floor on the service address (when present).",
                        "nullable": true
                    },
                    "service_city": {
                        "type": "string",
                        "description": "City of the service address.",
                        "nullable": true
                    },
                    "service_state": {
                        "type": "string",
                        "description": "State / province of the service address. 2-letter postal abbreviation for US/Canadian bills; full name otherwise.",
                        "nullable": true
                    },
                    "service_zip": {
                        "type": "string",
                        "description": "Postal code of the service address as printed on the bill (US 5-digit, ZIP+4, or international format).",
                        "nullable": true
                    },
                    "service_country": {
                        "type": "string",
                        "description": "ISO country name for the service address. Inferred from postal-code format / utility if not explicitly stated.",
                        "nullable": true
                    },
                    "billing_street1": {
                        "type": "string",
                        "description": "Street number and name of the remittance / mailing address (where bills are sent).",
                        "nullable": true
                    },
                    "billing_street2": {
                        "type": "string",
                        "description": "Apartment, suite, unit, or floor on the remittance address.",
                        "nullable": true
                    },
                    "billing_city": {
                        "type": "string",
                        "description": "City of the remittance address.",
                        "nullable": true
                    },
                    "billing_state": {
                        "type": "string",
                        "description": "State / province of the remittance address.",
                        "nullable": true
                    },
                    "billing_zip": {
                        "type": "string",
                        "description": "Postal code of the remittance address.",
                        "nullable": true
                    },
                    "billing_country": {
                        "type": "string",
                        "description": "ISO country name for the remittance address.",
                        "nullable": true
                    },
                    "bill_customer_account": {
                        "type": "string",
                        "description": "Customer-level account number printed on the bill, used for remittance. Distinct from service_account_number, which identifies the grid-side service point. Some utilities (e.g. SCE residential) use similar formats for both; others (e.g. AES Ohio) use very different numbers."
                    },
                    "bill_service_agreement_number": {
                        "type": "string",
                        "description": "Service agreement / supplier contract identifier on the bill side. Distinct from service_account_number (grid-side) and bill_customer_account (customer-level). For SCE this often equals service_account_number; for deregulated supplier bills (AES Ohio + Dynegy, CCA territories), it's a separate identifier issued by the supplier.",
                        "nullable": true
                    },
                    "bill_service_agreement_start_date": {
                        "type": "string",
                        "format": "date",
                        "description": "Effective date of the supplier-side service agreement / contract, when printed on the bill. ISO 8601 YYYY-MM-DD.",
                        "nullable": true
                    },
                    "bill_service_agreement_end_date": {
                        "type": "string",
                        "format": "date",
                        "description": "Expiration date of the supplier-side service agreement / contract, when printed on the bill. ISO 8601 YYYY-MM-DD.",
                        "nullable": true
                    },
                    "bill_statement_date": {
                        "type": "string",
                        "format": "date",
                        "description": "Date the bill was issued by the utility. ISO 8601 YYYY-MM-DD.",
                        "nullable": true
                    },
                    "bill_start_date": {
                        "type": "string",
                        "format": "date",
                        "description": "First day of the billing period covered by this row. ISO 8601 YYYY-MM-DD.",
                        "nullable": true
                    },
                    "bill_end_date": {
                        "type": "string",
                        "format": "date",
                        "description": "Last day of the billing period covered by this row. ISO 8601 YYYY-MM-DD.",
                        "nullable": true
                    },
                    "bill_due_date": {
                        "type": "string",
                        "format": "date",
                        "description": "Payment due date for the bill. ISO 8601 YYYY-MM-DD.",
                        "nullable": true
                    },
                    "bill_frequency": {
                        "type": "string",
                        "enum": [
                            "monthly",
                            "bi-monthly",
                            "quarterly",
                            "annual"
                        ],
                        "description": "How often the bill is issued. Taken from the bill if explicitly stated; otherwise inferred from (bill_end_date - bill_start_date): ~30 days = monthly, ~60 = bi-monthly, ~90 = quarterly.",
                        "nullable": true
                    },
                    "days_in_period": {
                        "type": "number",
                        "description": "Number of days the billing period covers. Use the value the bill prints directly when it states one (e.g. 'Service Period: 30 days', 'Number of Days: 31'); otherwise compute it from (bill_end_date - bill_start_date). Row-repeated: every meter row on the same invoice shares this value. Null when neither a stated days value nor both period dates are available. When the stated value disagrees with the computed value by more than one day, prefer the stated value and emit DATE_MISMATCH.",
                        "nullable": true
                    },
                    "meter_total_volume": {
                        "type": "number",
                        "description": "Usage attributed to this single meter for the billing period. Per-meter scope: each meter on a multi-meter invoice gets its own value. Raw number with no units or commas. The unit is in meter_total_unit.",
                        "nullable": true
                    },
                    "meter_total_unit": {
                        "type": "string",
                        "enum": [
                            "kwh",
                            "therms",
                            "ccf",
                            "gallons",
                            "kw",
                            "mj",
                            "m3",
                            "kl"
                        ],
                        "description": "Unit of meter_total_volume for this single meter. US-style units: kwh (electric), therms (gas energy), ccf (gas/water volume), gallons (water), kw (demand). Metric/AU-style units: mj (gas or electric energy in megajoules), m3 (gas or water volume in cubic metres), kl (water in kilolitres). Lowercase canonical form regardless of how the bill prints it (e.g. 'MJ' on the bill -> 'mj').",
                        "nullable": true
                    },
                    "read_type": {
                        "type": "string",
                        "enum": [
                            "actual",
                            "estimated",
                            "customer_read"
                        ],
                        "description": "How the meter reading at the end of the billing period was obtained: 'actual' (utility read the physical meter or AMI sent a reading), 'estimated' (utility estimated usage because no read was taken — common for skipped months, storm interruptions, or inaccessible meters), or 'customer_read' (customer self-read and reported the value). Lowercase canonical form regardless of how the bill prints it (e.g. 'Estimated Reading' / 'EST' -> 'estimated'). Null when the bill doesn't state how the read was obtained.",
                        "nullable": true
                    },
                    "meter_total_cost": {
                        "type": "number",
                        "description": "Cost attributed to this single meter for the billing period. Per-meter scope: each meter on a multi-meter invoice gets its own value. Sum across all meter rows on the same invoice equals bill_new_charges (modulo invoice-level fees, discounts, or rounding). Negative values are valid - credits and solar feed-in show as negative. For the invoice's headline 'amount due' figure see bill_total. Raw number, no currency symbols; currency lives in the currency field. When a multi-meter bill reports only one combined cost with no per-meter breakdown, that single value is repeated on every row and the COST_SHARED_ACROSS_METERS warning is emitted.",
                        "nullable": true
                    },
                    "bill_total": {
                        "type": "number",
                        "description": "Invoice-level headline amount the customer is asked to pay (the 'amount due' / 'pay this' figure printed at the top of the bill). Row-repeated: every meter row on the same invoice shares this value. Relationship: bill_total = previous_balance - payments_received + bill_new_charges + late_fee. When the bill shows no previous balance and no in-period payment, bill_total equals bill_new_charges. Positive when something is owed; negative when the account is in credit.",
                        "nullable": true
                    },
                    "bill_new_charges": {
                        "type": "number",
                        "description": "Total charges for the current billing period, before previous-balance or payment adjustments. The sum of meter_total_cost across every meter row on the same invoice equals this value (modulo invoice-level fees, discounts, rounding, or flat-fee services that have no meter row of their own). On a single-service bill, a gap over 10% emits COST_MISMATCH; on a multi-service bill the gap is expected and emits MULTI_SERVICE instead. Row-repeated. Null when the bill does not separately show a current-charges subtotal distinct from the headline total.",
                        "nullable": true
                    },
                    "previous_balance": {
                        "type": "number",
                        "description": "Outstanding balance carried over from the previous invoice. Positive when the prior bill was unpaid; negative when the customer was in credit. Null on first bills or when the bill does not show a carry-forward line (do not default to 0 - null preserves the distinction between 'not stated' and 'explicitly zero'). Row-repeated.",
                        "nullable": true
                    },
                    "payments_received": {
                        "type": "number",
                        "description": "Sum of payments received from the customer since the previous invoice. Always reported as a positive magnitude; the summing relationship subtracts it from the running balance. Null when the bill does not show a payments line (do not default to 0 - null preserves the distinction between 'not stated' and 'explicitly zero'). Row-repeated.",
                        "nullable": true
                    },
                    "late_fee": {
                        "type": "number",
                        "description": "Penalty charge applied at the invoice level for late payment of the prior balance. Positive when present; null when no late fee applies (the common case - do not default to 0). Distinct from a meter-level disconnection or reconnection fee, which belongs to its meter's meter_total_cost rather than here. Row-repeated.",
                        "nullable": true
                    },
                    "max_kw": {
                        "type": "number",
                        "description": "Electric-only. Peak / maximum demand for this meter during the billing period, in the unit declared by demand_unit (typically kW; some commercial bills report kVA). Null when the bill doesn't report demand. Gas analog (max therms/hour or peak Dth) and water analog (max GPM) are not currently captured; would be separate fields if needed.",
                        "nullable": true
                    },
                    "demand_unit": {
                        "type": "string",
                        "enum": [
                            "kw",
                            "kva"
                        ],
                        "description": "Unit of max_kw for this meter: 'kw' (real power demand) or 'kva' (apparent power demand, common on industrial / large-commercial bills where the utility prices power-factor as well as real power). Electric-only. Lowercase canonical form regardless of how the bill prints it (e.g. 'kVA' -> 'kva'). Null when max_kw is null. When max_kw is populated but the bill prints only 'Demand' without further qualification, default to 'kw'.",
                        "nullable": true
                    },
                    "currency": {
                        "type": "string",
                        "description": "ISO 4217 currency code for monetary fields. Inferred from utility/address when not explicitly stated; the CURRENCY_ASSUMED warning fires in that case."
                    },
                    "tariff": {
                        "type": "string",
                        "description": "Rate / tariff / schedule name as printed on the bill. Never normalized.",
                        "nullable": true
                    },
                    "service_class": {
                        "type": "string",
                        "enum": [
                            "res-electric",
                            "comm-electric",
                            "ind-electric",
                            "agr-electric",
                            "res-gas",
                            "comm-gas",
                            "ind-gas",
                            "agr-gas",
                            "res-water",
                            "comm-water",
                            "ind-water",
                            "res-sewer",
                            "comm-sewer",
                            "res-steam",
                            "comm-steam",
                            "unknown"
                        ],
                        "description": "Customer classification + commodity. Derived from the tariff name and customer profile on the bill."
                    },
                    "service_type": {
                        "type": "string",
                        "enum": [
                            "Electric",
                            "Gas",
                            "Water",
                            "Sewer",
                            "Steam",
                            "Telecom"
                        ],
                        "description": "Single-word service category from the bill: 'Electric', 'Gas', 'Water', 'Sewer', 'Steam', or 'Telecom'.",
                        "nullable": true
                    },
                    "is_tou_rate": {
                        "type": "boolean",
                        "description": "Electric-only. True if the bill shows any time-of-use rate windows (on-peak / off-peak / mid-peak / shoulder); false otherwise. The detailed per-band breakdown lives in meters[i].line_items[] (kind=consumption, with name carrying the verbatim band label) rather than in flat columns. Gas bills rarely use TOU rates; leave null when not applicable.",
                        "nullable": true
                    },
                    "baseline_allowance": {
                        "type": "number",
                        "description": "Electric-only. The kWh threshold that determines tier crossover on a tiered rate (e.g. SCE 'Your winter baseline allowance: 215.0 kWh', PG&E equivalent). Lets a customer reason about 'am I close to crossing into Tier 2?' Null on flat-rate bills (single-rate AU retailers, simple commercial flat tariffs). On a quarterly or cross-season bill that prints two allowances (winter and summer portions), leave null and emit MULTI_SEASON_BILL — the per-segment values surface on the corresponding tier line items via start_date / end_date.",
                        "nullable": true
                    },
                    "baseline_season": {
                        "type": "string",
                        "enum": [
                            "winter",
                            "summer"
                        ],
                        "description": "Electric-only. Which seasonal baseline allowance applies for this bill. Implicit on SCE ('Your winter baseline allowance'); explicit on some utilities. Null on bills with no seasonal baseline. Same MULTI_SEASON_BILL caveat as baseline_allowance — null when the bill spans both seasons.",
                        "nullable": true
                    },
                    "tax_in_total": {
                        "type": "number",
                        "description": "Region-neutral. The bill's stated total tax embedded in meter_total_cost — populated when the bill prints a single headline tax figure ('GST in total $59.46', 'Total includes GST component of $10.77', 'Includes Sales Tax $X', UK 'VAT total'). Null when the bill itemises tax as discrete line items instead (US bills like SCE itemise State tax as a line_item with kind=tax — the line item is the source of truth). Never both: if line items are tax-inclusive and the bill also prints a headline figure, capture the headline here; if line items are tax-exclusive (with a separate kind=tax line), leave this null.",
                        "nullable": true
                    },
                    "extracted_at": {
                        "type": "string",
                        "description": "ISO 8601 datetime when the extraction completed. Falls back to the Extraction model's updated_at if the lambda didn't set it.",
                        "nullable": true
                    }
                },
                "required": [
                    "source_filename",
                    "extraction_status",
                    "utility_name",
                    "bill_customer_account",
                    "currency",
                    "service_class"
                ]
            },
            "LineItem": {
                "type": "object",
                "description": "A single charge line, keyed the same as the Excel \"Line items\" sheet.",
                "required": [
                    "extraction_id",
                    "meter_index",
                    "line_index",
                    "kind",
                    "section",
                    "name"
                ],
                "properties": {
                    "extraction_id": {
                        "type": "string",
                        "format": "uuid",
                        "description": "Foreign key joining back to Sheet 1's extraction_id column. Same value on every line item of the same bill."
                    },
                    "meter_index": {
                        "type": "integer",
                        "description": "Zero-based index of the meter this line came from. Joins with Sheet 1's meter_index to pick out the row this charge contributes to."
                    },
                    "line_index": {
                        "type": "integer",
                        "description": "Zero-based position of this line within its meter's line_items[] — preserves the bill's printed order."
                    },
                    "kind": {
                        "type": "string",
                        "enum": [
                            "consumption",
                            "export",
                            "supply",
                            "credit",
                            "tier",
                            "demand",
                            "flat_fee",
                            "tax",
                            "other"
                        ],
                        "description": "Normalized charge type so you can pivot without parsing names."
                    },
                    "section": {
                        "type": "string",
                        "enum": [
                            "delivery",
                            "generation",
                            "tax",
                            "other"
                        ],
                        "description": "Normalized section the line was filed under."
                    },
                    "source_section": {
                        "type": "string",
                        "description": "Verbatim section header from the bill (e.g. \"Delivery Charges\", \"Generation Charges\", \"Taxes & Fees\"). Useful when section's four-bucket enum is too coarse.",
                        "nullable": true
                    },
                    "supplier": {
                        "type": "string",
                        "description": "Verbatim supplier name when the bill splits charges by supplier (CCA / community choice aggregator bills do this). Null on single-supplier bills.",
                        "nullable": true
                    },
                    "name": {
                        "type": "string",
                        "description": "Verbatim charge name as printed on the bill — never normalized. The raw text downstream tooling pivots and reconciles on."
                    },
                    "volume": {
                        "type": "number",
                        "description": "Quantity charged for. Null for flat fees and most taxes.",
                        "nullable": true
                    },
                    "unit": {
                        "type": "string",
                        "description": "Unit volume is denominated in (kwh, therms, ccf, mj, m3, kl, day, etc.). Null when no volume.",
                        "nullable": true
                    },
                    "rate": {
                        "type": "number",
                        "description": "Per-unit price applied. Negative for credits and feed-in. Null when the bill prints only a total.",
                        "nullable": true
                    },
                    "rate_unit": {
                        "type": "string",
                        "description": "How the rate is denominated — e.g. \"$/kwh\", \"$/day\", \"$/mj\". Null when rate is null.",
                        "nullable": true
                    },
                    "cost": {
                        "type": "number",
                        "description": "Dollar amount the line contributes. Negative for credits, rebates, and net feed-in. Sums up to meter_total_cost within the floored tolerance — see COST_MISMATCH.",
                        "nullable": true
                    },
                    "start_date": {
                        "type": "string",
                        "format": "date",
                        "description": "ISO 8601 service-window start, when the bill prints per-line dates (rare). Usually null.",
                        "nullable": true
                    },
                    "end_date": {
                        "type": "string",
                        "format": "date",
                        "description": "ISO 8601 service-window end. Usually null — defer to meter's bill_start_date / bill_end_date.",
                        "nullable": true
                    },
                    "time_window": {
                        "type": "string",
                        "description": "TOU window the line covers, verbatim (e.g. \"4pm–9pm Mon–Fri\"). Populated only on time-of-use rates that print per-band figures.",
                        "nullable": true
                    },
                    "plan_label": {
                        "type": "string",
                        "description": "Verbatim plan or product name when the line was filed under one (e.g. \"ZEROHERO\"). Mostly null on US bills.",
                        "nullable": true
                    },
                    "confidence": {
                        "type": "number",
                        "description": "Per-line extractor confidence, 0–1.",
                        "nullable": true
                    }
                }
            },
            "User": {
                "type": "object",
                "required": [
                    "id",
                    "name",
                    "email",
                    "remaining_pages",
                    "pages_limit",
                    "pages_used_this_period"
                ],
                "properties": {
                    "id": {
                        "type": "string",
                        "format": "uuid"
                    },
                    "name": {
                        "type": "string"
                    },
                    "email": {
                        "type": "string",
                        "format": "email"
                    },
                    "team": {
                        "type": "object",
                        "nullable": true,
                        "properties": {
                            "id": {
                                "type": "string",
                                "format": "uuid"
                            },
                            "name": {
                                "type": "string"
                            },
                            "plan": {
                                "type": "string"
                            }
                        }
                    },
                    "remaining_pages": {
                        "type": "integer"
                    },
                    "pages_limit": {
                        "type": "integer"
                    },
                    "pages_used_this_period": {
                        "type": "integer"
                    },
                    "period_resets_at": {
                        "type": "string",
                        "format": "date-time",
                        "nullable": true
                    }
                }
            },
            "Error": {
                "type": "object",
                "required": [
                    "error"
                ],
                "properties": {
                    "error": {
                        "type": "string",
                        "description": "Human-readable error message."
                    }
                }
            }
        }
    }
}